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