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 static androidx.appsearch.app.SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_COSINE; 20 import static androidx.appsearch.app.SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_EUCLIDEAN; 21 22 import android.os.Bundle; 23 import android.os.Parcel; 24 import android.os.Parcelable; 25 26 import androidx.annotation.NonNull; 27 import androidx.annotation.Nullable; 28 import androidx.annotation.OptIn; 29 import androidx.annotation.RequiresFeature; 30 import androidx.annotation.RestrictTo; 31 import androidx.appsearch.annotation.CanIgnoreReturnValue; 32 import androidx.appsearch.exceptions.AppSearchException; 33 import androidx.appsearch.flags.FlaggedApi; 34 import androidx.appsearch.flags.Flags; 35 import androidx.appsearch.safeparcel.AbstractSafeParcelable; 36 import androidx.appsearch.safeparcel.GenericDocumentParcel; 37 import androidx.appsearch.safeparcel.SafeParcelable; 38 import androidx.appsearch.safeparcel.stub.StubCreators.EmbeddingMatchInfoCreator; 39 import androidx.appsearch.safeparcel.stub.StubCreators.MatchInfoCreator; 40 import androidx.appsearch.safeparcel.stub.StubCreators.MatchRangeCreator; 41 import androidx.appsearch.safeparcel.stub.StubCreators.SearchResultCreator; 42 import androidx.appsearch.safeparcel.stub.StubCreators.TextMatchInfoCreator; 43 import androidx.appsearch.util.BundleUtil; 44 import androidx.collection.ArrayMap; 45 import androidx.core.util.ObjectsCompat; 46 import androidx.core.util.Preconditions; 47 48 import java.util.ArrayList; 49 import java.util.Collections; 50 import java.util.List; 51 import java.util.Map; 52 import java.util.Set; 53 54 /** 55 * This class represents one of the results obtained from an AppSearch query. 56 * 57 * <p>This allows clients to obtain: 58 * <ul> 59 * <li>The document which matched, using {@link #getGenericDocument} 60 * <li>Information about which properties in the document matched, and "snippet" information 61 * containing textual summaries of the document's matches, using {@link #getMatchInfos} 62 * </ul> 63 * 64 * <p>"Snippet" refers to a substring of text from the content of document that is returned as a 65 * part of search result. 66 * 67 * @see SearchResults 68 */ 69 @SafeParcelable.Class(creator = "SearchResultCreator") 70 // TODO(b/384721898): Switch to JSpecify annotations 71 @SuppressWarnings({"HiddenSuperclass", "JSpecifyNullness"}) 72 public final class SearchResult extends AbstractSafeParcelable { 73 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 74 @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2) 75 public static final @NonNull Parcelable.Creator<SearchResult> CREATOR = 76 new SearchResultCreator(); 77 78 @Field(id = 1) 79 final GenericDocumentParcel mDocument; 80 @Field(id = 2) 81 final List<MatchInfo> mMatchInfos; 82 @Field(id = 3, getter = "getPackageName") 83 private final String mPackageName; 84 @Field(id = 4, getter = "getDatabaseName") 85 private final String mDatabaseName; 86 @Field(id = 5, getter = "getRankingSignal") 87 private final double mRankingSignal; 88 @Field(id = 6, getter = "getJoinedResults") 89 private final List<SearchResult> mJoinedResults; 90 @Field(id = 7, getter = "getInformationalRankingSignals") 91 private final @NonNull List<Double> mInformationalRankingSignals; 92 /** 93 * Holds the map from schema type names to the list of their parent types. 94 * 95 * <p>The map includes entries for the {@link GenericDocument}'s own type and all of the 96 * nested documents' types. Child types are guaranteed to appear before parent types in each 97 * list. 98 * 99 * <p>Parent types include transitive parents. 100 * 101 * <p>All schema names in this map are un-prefixed, for both keys and values. 102 */ 103 @Field(id = 8) 104 final @NonNull Bundle mParentTypeMap; 105 106 107 /** Cache of the {@link GenericDocument}. Comes from mDocument at first use. */ 108 private @Nullable GenericDocument mDocumentCached; 109 110 /** Cache of the inflated {@link MatchInfo}. Comes from inflating mMatchInfos at first use. */ 111 private @Nullable List<MatchInfo> mMatchInfosCached; 112 113 /** @exportToFramework:hide */ 114 @Constructor SearchResult( @aramid = 1) @onNull GenericDocumentParcel document, @Param(id = 2) @NonNull List<MatchInfo> matchInfos, @Param(id = 3) @NonNull String packageName, @Param(id = 4) @NonNull String databaseName, @Param(id = 5) double rankingSignal, @Param(id = 6) @NonNull List<SearchResult> joinedResults, @Param(id = 7) @Nullable List<Double> informationalRankingSignals, @Param(id = 8) @Nullable Bundle parentTypeMap)115 SearchResult( 116 @Param(id = 1) @NonNull GenericDocumentParcel document, 117 @Param(id = 2) @NonNull List<MatchInfo> matchInfos, 118 @Param(id = 3) @NonNull String packageName, 119 @Param(id = 4) @NonNull String databaseName, 120 @Param(id = 5) double rankingSignal, 121 @Param(id = 6) @NonNull List<SearchResult> joinedResults, 122 @Param(id = 7) @Nullable List<Double> informationalRankingSignals, 123 @Param(id = 8) @Nullable Bundle parentTypeMap) { 124 mDocument = Preconditions.checkNotNull(document); 125 mMatchInfos = Preconditions.checkNotNull(matchInfos); 126 mPackageName = Preconditions.checkNotNull(packageName); 127 mDatabaseName = Preconditions.checkNotNull(databaseName); 128 mRankingSignal = rankingSignal; 129 mJoinedResults = Collections.unmodifiableList(Preconditions.checkNotNull(joinedResults)); 130 if (informationalRankingSignals != null) { 131 mInformationalRankingSignals = Collections.unmodifiableList( 132 informationalRankingSignals); 133 } else { 134 mInformationalRankingSignals = Collections.emptyList(); 135 } 136 if (parentTypeMap != null) { 137 mParentTypeMap = parentTypeMap; 138 } else { 139 mParentTypeMap = Bundle.EMPTY; 140 } 141 } 142 143 // @exportToFramework:startStrip() 144 /** 145 * Contains the matching document, converted to the given document class. 146 * 147 * <p>This is equivalent to calling {@code getGenericDocument().toDocumentClass(T.class)}. 148 * 149 * <p>Calling this function repeatedly is inefficient. Prefer to retain the object returned 150 * by this function, rather than calling it multiple times. 151 * 152 * @param documentClass the document class to be passed to 153 * {@link GenericDocument#toDocumentClass(java.lang.Class)}. 154 * @return Document object which matched the query. 155 * @throws AppSearchException if no factory for this document class could be found on the 156 * classpath. 157 * @see GenericDocument#toDocumentClass(java.lang.Class) 158 */ getDocument(@onNull java.lang.Class<T> documentClass)159 public <T> @NonNull T getDocument(@NonNull java.lang.Class<T> documentClass) 160 throws AppSearchException { 161 return getDocument(documentClass, /* documentClassMap= */null); 162 } 163 164 /** 165 * Contains the matching document, converted to the given document class. 166 * 167 * <p>This is equivalent to calling {@code getGenericDocument().toDocumentClass(T.class, 168 * new DocumentClassMappingContext(documentClassMap, getParentTypeMap()))}. 169 * 170 * <p>Calling this function repeatedly is inefficient. Prefer to retain the object returned 171 * by this function, rather than calling it multiple times. 172 * 173 * @param documentClass the document class to be passed to 174 * {@link GenericDocument#toDocumentClass(java.lang.Class, DocumentClassMappingContext)}. 175 * @param documentClassMap A map from AppSearch's type name specified by 176 * {@link androidx.appsearch.annotation.Document#name()} 177 * to the list of the fully qualified names of the corresponding 178 * document classes. In most cases, passing the value returned by 179 * {@link AppSearchDocumentClassMap#getGlobalMap()} will be sufficient. 180 * @return Document object which matched the query. 181 * @throws AppSearchException if no factory for this document class could be found on the 182 * classpath. 183 * @see GenericDocument#toDocumentClass(java.lang.Class, DocumentClassMappingContext) 184 */ 185 @OptIn(markerClass = ExperimentalAppSearchApi.class) getDocument(@onNull java.lang.Class<T> documentClass, @Nullable Map<String, List<String>> documentClassMap)186 public <T> @NonNull T getDocument(@NonNull java.lang.Class<T> documentClass, 187 @Nullable Map<String, List<String>> documentClassMap) throws AppSearchException { 188 Preconditions.checkNotNull(documentClass); 189 return getGenericDocument().toDocumentClass(documentClass, 190 new DocumentClassMappingContext(documentClassMap, 191 getParentTypeMap())); 192 } 193 // @exportToFramework:endStrip() 194 195 /** 196 * Contains the matching {@link GenericDocument}. 197 * 198 * @return Document object which matched the query. 199 */ getGenericDocument()200 public @NonNull GenericDocument getGenericDocument() { 201 if (mDocumentCached == null) { 202 mDocumentCached = new GenericDocument(mDocument); 203 } 204 return mDocumentCached; 205 } 206 207 /** 208 * Returns a list of {@link MatchInfo}s providing information about how the document in 209 * {@link #getGenericDocument} matched the query. 210 * 211 * @return List of matches based on {@link SearchSpec}. If snippeting is disabled using 212 * {@link SearchSpec.Builder#setSnippetCount} or 213 * {@link SearchSpec.Builder#setSnippetCountPerProperty}, for all results after that 214 * value, this method returns an empty list. 215 */ 216 @OptIn(markerClass = ExperimentalAppSearchApi.class) getMatchInfos()217 public @NonNull List<MatchInfo> getMatchInfos() { 218 if (mMatchInfosCached == null) { 219 mMatchInfosCached = new ArrayList<>(mMatchInfos.size()); 220 for (int i = 0; i < mMatchInfos.size(); i++) { 221 MatchInfo matchInfo = mMatchInfos.get(i); 222 matchInfo.setDocument(getGenericDocument()); 223 if (matchInfo.getTextMatch() != null) { 224 // This is necessary in order to use the TextMatchInfo after IPC, since 225 // TextMatch.mPropertyPath is private and is not retained by SafeParcelable 226 // across IPC. 227 matchInfo.mTextMatch.setPropertyPath(matchInfo.getPropertyPath()); 228 } 229 if (mMatchInfosCached != null) { 230 // This additional check is added for NullnessChecker. 231 mMatchInfosCached.add(matchInfo); 232 } 233 } 234 mMatchInfosCached = Collections.unmodifiableList(mMatchInfosCached); 235 } 236 // This check is added for NullnessChecker, mMatchInfos will always be NonNull. 237 return Preconditions.checkNotNull(mMatchInfosCached); 238 } 239 240 /** 241 * Contains the package name of the app that stored the {@link GenericDocument}. 242 * 243 * @return Package name that stored the document 244 */ getPackageName()245 public @NonNull String getPackageName() { 246 return mPackageName; 247 } 248 249 /** 250 * Contains the database name that stored the {@link GenericDocument}. 251 * 252 * @return Name of the database within which the document is stored 253 */ getDatabaseName()254 public @NonNull String getDatabaseName() { 255 return mDatabaseName; 256 } 257 258 /** 259 * Returns the ranking signal of the {@link GenericDocument}, according to the 260 * ranking strategy set in {@link SearchSpec.Builder#setRankingStrategy(int)}. 261 * 262 * The meaning of the ranking signal and its value is determined by the selected ranking 263 * strategy: 264 * <ul> 265 * <li>{@link SearchSpec#RANKING_STRATEGY_NONE} - this value will be 0</li> 266 * <li>{@link SearchSpec#RANKING_STRATEGY_DOCUMENT_SCORE} - the value returned by calling 267 * {@link GenericDocument#getScore()} on the document returned by 268 * {@link #getGenericDocument()}</li> 269 * <li>{@link SearchSpec#RANKING_STRATEGY_CREATION_TIMESTAMP} - the value returned by calling 270 * {@link GenericDocument#getCreationTimestampMillis()} on the document returned by 271 * {@link #getGenericDocument()}</li> 272 * <li>{@link SearchSpec#RANKING_STRATEGY_RELEVANCE_SCORE} - an arbitrary double value where 273 * a higher value means more relevant</li> 274 * <li>{@link SearchSpec#RANKING_STRATEGY_USAGE_COUNT} - the number of times usage has been 275 * reported for the document returned by {@link #getGenericDocument()}</li> 276 * <li>{@link SearchSpec#RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP} - the timestamp of the 277 * most recent usage that has been reported for the document returned by 278 * {@link #getGenericDocument()}</li> 279 * </ul> 280 * 281 * @return Ranking signal of the document 282 */ getRankingSignal()283 public double getRankingSignal() { 284 return mRankingSignal; 285 } 286 287 /** 288 * Returns the informational ranking signals of the {@link GenericDocument}, according to the 289 * expressions added in {@link SearchSpec.Builder#addInformationalRankingExpressions}. 290 */ 291 @FlaggedApi(Flags.FLAG_ENABLE_INFORMATIONAL_RANKING_EXPRESSIONS) getInformationalRankingSignals()292 public @NonNull List<Double> getInformationalRankingSignals() { 293 return mInformationalRankingSignals; 294 } 295 296 /** 297 * Returns the map from schema type names to the list of their parent types. 298 * 299 * <p>The map includes entries for the {@link GenericDocument}'s own type and all of the 300 * nested documents' types. Child types are guaranteed to appear before parent types in each 301 * list. 302 * 303 * <p>Parent types include transitive parents. 304 * 305 * <p>Calling this function repeatedly is inefficient. Prefer to retain the Map returned 306 * by this function, rather than calling it multiple times. 307 */ 308 @ExperimentalAppSearchApi 309 @FlaggedApi(Flags.FLAG_ENABLE_SEARCH_RESULT_PARENT_TYPES) getParentTypeMap()310 public @NonNull Map<String, List<String>> getParentTypeMap() { 311 Set<String> schemaTypes = mParentTypeMap.keySet(); 312 Map<String, List<String>> parentTypeMap = new ArrayMap<>(schemaTypes.size()); 313 for (String schemaType : schemaTypes) { 314 ArrayList<String> parentTypes = mParentTypeMap.getStringArrayList(schemaType); 315 if (parentTypes != null) { 316 parentTypeMap.put(schemaType, parentTypes); 317 } 318 } 319 return parentTypeMap; 320 } 321 322 /** 323 * Gets a list of {@link SearchResult} joined from the join operation. 324 * 325 * <p> These joined documents match the outer document as specified in the {@link JoinSpec} 326 * with parentPropertyExpression and childPropertyExpression. They are ordered according to the 327 * {@link JoinSpec#getNestedSearchSpec}, and as many SearchResults as specified by 328 * {@link JoinSpec#getMaxJoinedResultCount} will be returned. If no {@link JoinSpec} was 329 * specified, this returns an empty list. 330 * 331 * <p> This method is inefficient to call repeatedly, as new {@link SearchResult} objects are 332 * created each time. 333 * 334 * @return a List of SearchResults containing joined documents. 335 */ getJoinedResults()336 public @NonNull List<SearchResult> getJoinedResults() { 337 return mJoinedResults; 338 } 339 340 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 341 @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2) 342 @Override writeToParcel(@onNull Parcel dest, int flags)343 public void writeToParcel(@NonNull Parcel dest, int flags) { 344 SearchResultCreator.writeToParcel(this, dest, flags); 345 } 346 347 /** Builder for {@link SearchResult} objects. */ 348 public static final class Builder { 349 private final String mPackageName; 350 private final String mDatabaseName; 351 private List<MatchInfo> mMatchInfos = new ArrayList<>(); 352 private GenericDocument mGenericDocument; 353 private double mRankingSignal; 354 private List<Double> mInformationalRankingSignals = new ArrayList<>(); 355 private Bundle mParentTypeMap = new Bundle(); 356 private List<SearchResult> mJoinedResults = new ArrayList<>(); 357 private boolean mBuilt = false; 358 359 /** 360 * Constructs a new builder for {@link SearchResult} objects. 361 * 362 * @param packageName the package name the matched document belongs to 363 * @param databaseName the database name the matched document belongs to. 364 */ Builder(@onNull String packageName, @NonNull String databaseName)365 public Builder(@NonNull String packageName, @NonNull String databaseName) { 366 mPackageName = Preconditions.checkNotNull(packageName); 367 mDatabaseName = Preconditions.checkNotNull(databaseName); 368 } 369 370 /** @exportToFramework:hide */ 371 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 372 @OptIn(markerClass = ExperimentalAppSearchApi.class) Builder(@onNull SearchResult searchResult)373 public Builder(@NonNull SearchResult searchResult) { 374 Preconditions.checkNotNull(searchResult); 375 mPackageName = searchResult.getPackageName(); 376 mDatabaseName = searchResult.getDatabaseName(); 377 mGenericDocument = searchResult.getGenericDocument(); 378 mRankingSignal = searchResult.getRankingSignal(); 379 mInformationalRankingSignals = new ArrayList<>( 380 searchResult.getInformationalRankingSignals()); 381 setParentTypeMap(searchResult.getParentTypeMap()); 382 List<MatchInfo> matchInfos = searchResult.getMatchInfos(); 383 for (int i = 0; i < matchInfos.size(); i++) { 384 addMatchInfo(new MatchInfo.Builder(matchInfos.get(i)).build()); 385 } 386 List<SearchResult> joinedResults = searchResult.getJoinedResults(); 387 for (int i = 0; i < joinedResults.size(); i++) { 388 addJoinedResult(joinedResults.get(i)); 389 } 390 } 391 392 // @exportToFramework:startStrip() 393 /** 394 * Sets the document which matched. 395 * 396 * @param document An instance of a class annotated with 397 * {@link androidx.appsearch.annotation.Document}. 398 * 399 * @throws AppSearchException if an error occurs converting a document class into a 400 * {@link GenericDocument}. 401 */ 402 @CanIgnoreReturnValue setDocument(@onNull Object document)403 public @NonNull Builder setDocument(@NonNull Object document) throws AppSearchException { 404 Preconditions.checkNotNull(document); 405 resetIfBuilt(); 406 return setGenericDocument(GenericDocument.fromDocumentClass(document)); 407 } 408 // @exportToFramework:endStrip() 409 410 /** Sets the document which matched. */ 411 @CanIgnoreReturnValue setGenericDocument(@onNull GenericDocument document)412 public @NonNull Builder setGenericDocument(@NonNull GenericDocument document) { 413 Preconditions.checkNotNull(document); 414 resetIfBuilt(); 415 mGenericDocument = document; 416 return this; 417 } 418 419 /** Adds another match to this SearchResult. */ 420 @CanIgnoreReturnValue addMatchInfo(@onNull MatchInfo matchInfo)421 public @NonNull Builder addMatchInfo(@NonNull MatchInfo matchInfo) { 422 Preconditions.checkState( 423 matchInfo.mDocument == null, 424 "This MatchInfo is already associated with a SearchResult and can't be " 425 + "reassigned"); 426 resetIfBuilt(); 427 mMatchInfos.add(matchInfo); 428 return this; 429 } 430 431 /** Sets the ranking signal of the matched document in this SearchResult. */ 432 @CanIgnoreReturnValue setRankingSignal(double rankingSignal)433 public @NonNull Builder setRankingSignal(double rankingSignal) { 434 resetIfBuilt(); 435 mRankingSignal = rankingSignal; 436 return this; 437 } 438 439 /** Adds the informational ranking signal of the matched document in this SearchResult. */ 440 @CanIgnoreReturnValue 441 @FlaggedApi(Flags.FLAG_ENABLE_INFORMATIONAL_RANKING_EXPRESSIONS) addInformationalRankingSignal(double rankingSignal)442 public @NonNull Builder addInformationalRankingSignal(double rankingSignal) { 443 resetIfBuilt(); 444 mInformationalRankingSignals.add(rankingSignal); 445 return this; 446 } 447 448 /** 449 * Sets the map from schema type names to the list of their parent types. 450 * 451 * <p>The map should include entries for the {@link GenericDocument}'s own type and all 452 * of the nested documents' types. 453 * 454 * <!--@exportToFramework:ifJetpack()--> 455 * <p>Child types must appear before parent types in each list. Otherwise, the 456 * {@link GenericDocument#toDocumentClass(java.lang.Class, DocumentClassMappingContext)} 457 * method may not correctly identify the most concrete type. This could lead to unintended 458 * deserialization into a more general type instead of a more specific type. 459 * <!--@exportToFramework:else() 460 * <p>Child types must appear before parent types in each list. Otherwise, the 461 * GenericDocument's toDocumentClass method (an AndroidX-only API) may not correctly 462 * identify the most concrete type. This could lead to unintended deserialization into a 463 * more general type instead of a 464 * more specific type. 465 * --> 466 * 467 * <p>Parent types should include transitive parents. 468 */ 469 @CanIgnoreReturnValue 470 @ExperimentalAppSearchApi 471 @FlaggedApi(Flags.FLAG_ENABLE_SEARCH_RESULT_PARENT_TYPES) setParentTypeMap(@onNull Map<String, List<String>> parentTypeMap)472 public @NonNull Builder setParentTypeMap(@NonNull Map<String, List<String>> parentTypeMap) { 473 Preconditions.checkNotNull(parentTypeMap); 474 resetIfBuilt(); 475 mParentTypeMap.clear(); 476 477 for (Map.Entry<String, List<String>> entry : parentTypeMap.entrySet()) { 478 Preconditions.checkNotNull(entry.getKey()); 479 Preconditions.checkNotNull(entry.getValue()); 480 481 ArrayList<String> parentTypes = new ArrayList<>(entry.getValue().size()); 482 for (int i = 0; i < entry.getValue().size(); i++) { 483 String parentType = entry.getValue().get(i); 484 parentTypes.add(Preconditions.checkNotNull(parentType)); 485 } 486 mParentTypeMap.putStringArrayList(entry.getKey(), parentTypes); 487 } 488 return this; 489 } 490 491 492 /** 493 * Adds a {@link SearchResult} that was joined by the {@link JoinSpec}. 494 * @param joinedResult The joined SearchResult to add. 495 */ 496 @CanIgnoreReturnValue addJoinedResult(@onNull SearchResult joinedResult)497 public @NonNull Builder addJoinedResult(@NonNull SearchResult joinedResult) { 498 resetIfBuilt(); 499 mJoinedResults.add(joinedResult); 500 return this; 501 } 502 503 /** 504 * Clears the {@link MatchInfo}s. 505 * 506 * @exportToFramework:hide 507 */ 508 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 509 @CanIgnoreReturnValue clearMatchInfos()510 public @NonNull Builder clearMatchInfos() { 511 resetIfBuilt(); 512 mMatchInfos.clear(); 513 return this; 514 } 515 516 517 /** 518 * Clears the {@link SearchResult}s that were joined. 519 * 520 * @exportToFramework:hide 521 */ 522 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 523 @CanIgnoreReturnValue clearJoinedResults()524 public @NonNull Builder clearJoinedResults() { 525 resetIfBuilt(); 526 mJoinedResults.clear(); 527 return this; 528 } 529 530 /** Constructs a new {@link SearchResult}. */ build()531 public @NonNull SearchResult build() { 532 mBuilt = true; 533 return new SearchResult( 534 mGenericDocument.getDocumentParcel(), 535 mMatchInfos, 536 mPackageName, 537 mDatabaseName, 538 mRankingSignal, 539 mJoinedResults, 540 mInformationalRankingSignals, 541 mParentTypeMap); 542 } 543 resetIfBuilt()544 private void resetIfBuilt() { 545 if (mBuilt) { 546 mMatchInfos = new ArrayList<>(mMatchInfos); 547 mJoinedResults = new ArrayList<>(mJoinedResults); 548 mInformationalRankingSignals = new ArrayList<>(mInformationalRankingSignals); 549 mParentTypeMap = BundleUtil.deepCopy(mParentTypeMap); 550 mBuilt = false; 551 } 552 } 553 } 554 555 /** 556 * This class represents match objects for any snippets that might be present in 557 * {@link SearchResults} from a query. 558 * 559 * <p> A {@link MatchInfo} contains either a {@link TextMatchInfo} representing a text match 560 * snippet, or an {@link EmbeddingMatchInfo} representing an embedding match snippet. 561 */ 562 @SafeParcelable.Class(creator = "MatchInfoCreator") 563 @SuppressWarnings("HiddenSuperclass") 564 @OptIn(markerClass = ExperimentalAppSearchApi.class) 565 public static final class MatchInfo extends AbstractSafeParcelable { 566 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 567 @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2) 568 public static final @NonNull Parcelable.Creator<MatchInfo> CREATOR = 569 new MatchInfoCreator(); 570 571 /** The path of the matching snippet property. */ 572 @Field(id = 1, getter = "getPropertyPath") 573 private final String mPropertyPath; 574 @Field(id = 2) 575 final int mExactMatchRangeStart; 576 @Field(id = 3) 577 final int mExactMatchRangeEnd; 578 @Field(id = 4) 579 final int mSubmatchRangeStart; 580 @Field(id = 5) 581 final int mSubmatchRangeEnd; 582 @Field(id = 6) 583 final int mSnippetRangeStart; 584 @Field(id = 7) 585 final int mSnippetRangeEnd; 586 587 /** Represents text-based match information. */ 588 @Field(id = 8, getter = "getTextMatch") 589 private @Nullable final TextMatchInfo mTextMatch; 590 591 /** Represents embedding-based match information. */ 592 @Field(id = 9, getter = "getEmbeddingMatch") 593 private @Nullable final EmbeddingMatchInfo mEmbeddingMatch; 594 595 private @Nullable PropertyPath mPropertyPathObject = null; 596 597 /** 598 * Document which the match comes from. 599 * 600 * <p>If this is {@code null}, methods which require access to the document, like 601 * {@link #getExactMatch}, will throw {@link NullPointerException}. 602 */ 603 private @Nullable GenericDocument mDocument = null; 604 605 @Constructor MatchInfo( @aramid = 1) @onNull String propertyPath, @Param(id = 2) int exactMatchRangeStart, @Param(id = 3) int exactMatchRangeEnd, @Param(id = 4) int submatchRangeStart, @Param(id = 5) int submatchRangeEnd, @Param(id = 6) int snippetRangeStart, @Param(id = 7) int snippetRangeEnd, @Param(id = 8) @Nullable TextMatchInfo textMatchInfo, @Param(id = 9) @Nullable EmbeddingMatchInfo embeddingMatchInfo)606 MatchInfo( 607 @Param(id = 1) @NonNull String propertyPath, 608 @Param(id = 2) int exactMatchRangeStart, 609 @Param(id = 3) int exactMatchRangeEnd, 610 @Param(id = 4) int submatchRangeStart, 611 @Param(id = 5) int submatchRangeEnd, 612 @Param(id = 6) int snippetRangeStart, 613 @Param(id = 7) int snippetRangeEnd, 614 @Param(id = 8) @Nullable TextMatchInfo textMatchInfo, 615 @Param(id = 9) @Nullable EmbeddingMatchInfo embeddingMatchInfo) { 616 mPropertyPath = Preconditions.checkNotNull(propertyPath); 617 mExactMatchRangeStart = exactMatchRangeStart; 618 mExactMatchRangeEnd = exactMatchRangeEnd; 619 mSubmatchRangeStart = submatchRangeStart; 620 mSubmatchRangeEnd = submatchRangeEnd; 621 mSnippetRangeStart = snippetRangeStart; 622 mSnippetRangeEnd = snippetRangeEnd; 623 mEmbeddingMatch = embeddingMatchInfo; 624 TextMatchInfo tempTextMatch = textMatchInfo; 625 if (tempTextMatch == null && mEmbeddingMatch == null) { 626 tempTextMatch = new TextMatchInfo( 627 new MatchRange(exactMatchRangeStart, exactMatchRangeEnd), 628 new MatchRange(submatchRangeStart, submatchRangeEnd), 629 new MatchRange(snippetRangeStart, snippetRangeEnd)); 630 tempTextMatch.setPropertyPath(mPropertyPath); 631 } 632 633 mTextMatch = tempTextMatch; 634 } 635 636 /** 637 * Gets the property path corresponding to the given entry. 638 * 639 * <p>A property path is a '.' - delimited sequence of property names indicating which 640 * property in the document these snippets correspond to. 641 * 642 * <p>Example properties: 'body', 'sender.name', 'sender.emailaddress', etc. 643 * For class example 1 this returns "subject" 644 */ getPropertyPath()645 public @NonNull String getPropertyPath() { 646 return mPropertyPath; 647 } 648 649 /** 650 * Gets a {@link PropertyPath} object representing the property path corresponding to the 651 * given entry. 652 * 653 * <p> Methods such as {@link GenericDocument#getPropertyDocument} accept a path as a 654 * string rather than a {@link PropertyPath} object. However, you may want to manipulate 655 * the path before getting a property document. This method returns a {@link PropertyPath} 656 * rather than a String for easier path manipulation, which can then be converted to a 657 * String. 658 * 659 * @see #getPropertyPath 660 * @see PropertyPath 661 */ getPropertyPathObject()662 public @NonNull PropertyPath getPropertyPathObject() { 663 if (mPropertyPathObject == null) { 664 mPropertyPathObject = new PropertyPath(mPropertyPath); 665 } 666 return mPropertyPathObject; 667 } 668 669 /** 670 * Retrieves the text-based match information. 671 * 672 * @return A {@link TextMatchInfo} instance, or null if the match is not text-based. 673 */ 674 @FlaggedApi(Flags.FLAG_ENABLE_EMBEDDING_MATCH_INFO) 675 @ExperimentalAppSearchApi getTextMatch()676 public @Nullable TextMatchInfo getTextMatch() { 677 return mTextMatch; 678 } 679 680 /** 681 * Retrieves the embedding-based match information. Only populated when 682 * {@link SearchSpec#shouldRetrieveEmbeddingMatchInfos()} is true. 683 * 684 * @return A {@link EmbeddingMatchInfo} instance, or null if the match is not an 685 * embedding match. 686 */ 687 @FlaggedApi(Flags.FLAG_ENABLE_EMBEDDING_MATCH_INFO) 688 @ExperimentalAppSearchApi getEmbeddingMatch()689 public @Nullable EmbeddingMatchInfo getEmbeddingMatch() { 690 return mEmbeddingMatch; 691 } 692 693 /** 694 * <p>Gets the full text corresponding to the given entry. Returns an empty string if the 695 * match is not text-based. 696 */ getFullText()697 public @NonNull String getFullText() { 698 if (mTextMatch == null) { 699 return ""; 700 } 701 return mTextMatch.getFullText(); 702 } 703 704 /** 705 * Gets the {@link MatchRange} of the exact term of the given entry that matched the query. 706 * Returns [0, 0] if the match is not text-based. 707 */ getExactMatchRange()708 public @NonNull MatchRange getExactMatchRange() { 709 if (mTextMatch == null) { 710 return new MatchRange(0, 0); 711 } 712 return mTextMatch.getExactMatchRange(); 713 } 714 715 /** 716 * Gets the exact term of the given entry that matched the query. Returns an empty 717 * CharSequence if the match is not text-based. 718 */ getExactMatch()719 public @NonNull CharSequence getExactMatch() { 720 if (mTextMatch == null) { 721 return ""; 722 } 723 return mTextMatch.getExactMatch(); 724 } 725 726 /** 727 * Gets the {@link MatchRange} of the submatch term subsequence of the given entry that 728 * matched the query. Returns [0, 0] if the match is not text-based. 729 * 730 * <!--@exportToFramework:ifJetpack()--> 731 * <p>This information may not be available depending on the backend and Android API 732 * level. To ensure it is available, call {@link Features#isFeatureSupported}. 733 * 734 * @throws UnsupportedOperationException if {@link Features#isFeatureSupported} is 735 * false. 736 * <!--@exportToFramework:else()--> 737 */ 738 @RequiresFeature( 739 enforcement = "androidx.appsearch.app.Features#isFeatureSupported", 740 name = Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH) getSubmatchRange()741 public @NonNull MatchRange getSubmatchRange() { 742 if (mTextMatch == null) { 743 return new MatchRange(0, 0); 744 } 745 return mTextMatch.getSubmatchRange(); 746 } 747 748 /** 749 * <p> Gets the exact term subsequence of the given entry that matched the query. Returns an 750 * empty CharSequence if the match is not text-based. 751 * 752 * <!--@exportToFramework:ifJetpack()--> 753 * <p>This information may not be available depending on the backend and Android API 754 * level. To ensure it is available, call {@link Features#isFeatureSupported}. 755 * 756 * @throws UnsupportedOperationException if {@link Features#isFeatureSupported} is 757 * false. 758 * <!--@exportToFramework:else()--> 759 */ 760 @RequiresFeature( 761 enforcement = "androidx.appsearch.app.Features#isFeatureSupported", 762 name = Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH) getSubmatch()763 public @NonNull CharSequence getSubmatch() { 764 if (mTextMatch == null) { 765 return ""; 766 } 767 return mTextMatch.getSubmatch(); 768 } 769 770 /** 771 * <p>Gets the snippet {@link MatchRange} corresponding to the given entry. Returns [0,0] 772 * if the match is not text-based. 773 * <p>Only populated when set maxSnippetSize > 0 in 774 * {@link SearchSpec.Builder#setMaxSnippetSize}. 775 */ getSnippetRange()776 public @NonNull MatchRange getSnippetRange() { 777 if (mTextMatch == null) { 778 return new MatchRange(0, 0); 779 } 780 return mTextMatch.getSnippetRange(); 781 } 782 783 /** 784 * <p>Gets the snippet corresponding to the given entry. Returns an empty CharSequence if 785 * the match is not text-based. 786 * <p>Snippet - Provides a subset of the content to display. Only populated when requested 787 * maxSnippetSize > 0. The size of this content can be changed by 788 * {@link SearchSpec.Builder#setMaxSnippetSize}. Windowing is centered around the middle of 789 * the matched token with content on either side clipped to token boundaries. 790 */ getSnippet()791 public @NonNull CharSequence getSnippet() { 792 if (mTextMatch == null) { 793 return ""; 794 } 795 return mTextMatch.getSnippet(); 796 } 797 798 /** 799 * Sets the {@link GenericDocument} for {@link MatchInfo}. 800 * 801 * <p>{@link MatchInfo} lacks a constructor that populates {@link MatchInfo#mDocument} 802 * This provides the ability to set {@link MatchInfo#mDocument} 803 */ setDocument(@onNull GenericDocument document)804 void setDocument(@NonNull GenericDocument document) { 805 mDocument = Preconditions.checkNotNull(document); 806 if (mTextMatch != null) { 807 mTextMatch.setDocument(document); 808 } 809 } 810 811 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 812 @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2) 813 @Override writeToParcel(@onNull Parcel dest, int flags)814 public void writeToParcel(@NonNull Parcel dest, int flags) { 815 MatchInfoCreator.writeToParcel(this, dest, flags); 816 } 817 818 /** Builder for {@link MatchInfo} objects. */ 819 public static final class Builder { 820 private final String mPropertyPath; 821 private EmbeddingMatchInfo mEmbeddingMatch = null; 822 private MatchRange mExactMatchRange = new MatchRange(0, 0); 823 private MatchRange mSubmatchRange = new MatchRange(-1, -1); 824 private MatchRange mSnippetRange = new MatchRange(0, 0); 825 826 /** 827 * Creates a new {@link MatchInfo.Builder} reporting a match with the given property 828 * path. 829 * 830 * <p>A property path is a dot-delimited sequence of property names indicating which 831 * property in the document these snippets correspond to. 832 * 833 * <p>Example properties: 'body', 'sender.name', 'sender.emailaddress', etc. 834 * For class example 1, this returns "subject". 835 * 836 * @param propertyPath A dot-delimited sequence of property names indicating which 837 * property in the document these snippets correspond to. 838 */ Builder(@onNull String propertyPath)839 public Builder(@NonNull String propertyPath) { 840 mPropertyPath = Preconditions.checkNotNull(propertyPath); 841 } 842 843 /** @exportToFramework:hide */ 844 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) Builder(@onNull MatchInfo matchInfo)845 public Builder(@NonNull MatchInfo matchInfo) { 846 Preconditions.checkNotNull(matchInfo); 847 mPropertyPath = matchInfo.mPropertyPath; 848 mEmbeddingMatch = matchInfo.getEmbeddingMatch(); 849 mExactMatchRange = matchInfo.getExactMatchRange(); 850 // Using the fields directly instead of getSubmatchRange() to bypass the 851 // checkSubmatchSupported check. 852 mSubmatchRange = new MatchRange(matchInfo.mSubmatchRangeStart, 853 matchInfo.mSubmatchRangeEnd); 854 mSnippetRange = matchInfo.getSnippetRange(); 855 } 856 857 /** 858 * Sets the {@link EmbeddingMatchInfo} corresponding to the given entry. 859 */ 860 @FlaggedApi(Flags.FLAG_ENABLE_EMBEDDING_MATCH_INFO) 861 @ExperimentalAppSearchApi 862 @CanIgnoreReturnValue setEmbeddingMatch(@ullable EmbeddingMatchInfo embeddingMatch)863 public @NonNull Builder setEmbeddingMatch(@Nullable EmbeddingMatchInfo embeddingMatch) { 864 mEmbeddingMatch = embeddingMatch; 865 return this; 866 } 867 868 /** 869 * Sets the exact {@link MatchRange} corresponding to the given entry. 870 */ 871 @CanIgnoreReturnValue setExactMatchRange(@onNull MatchRange matchRange)872 public @NonNull Builder setExactMatchRange(@NonNull MatchRange matchRange) { 873 mExactMatchRange = Preconditions.checkNotNull(matchRange); 874 return this; 875 } 876 877 878 /** 879 * Sets the submatch {@link MatchRange} corresponding to the given entry. 880 */ 881 @CanIgnoreReturnValue setSubmatchRange(@onNull MatchRange matchRange)882 public @NonNull Builder setSubmatchRange(@NonNull MatchRange matchRange) { 883 mSubmatchRange = Preconditions.checkNotNull(matchRange); 884 return this; 885 } 886 887 /** 888 * Sets the snippet {@link MatchRange} corresponding to the given entry. 889 */ 890 @CanIgnoreReturnValue setSnippetRange(@onNull MatchRange matchRange)891 public @NonNull Builder setSnippetRange(@NonNull MatchRange matchRange) { 892 mSnippetRange = Preconditions.checkNotNull(matchRange); 893 return this; 894 } 895 896 /** Constructs a new {@link MatchInfo}. */ build()897 public @NonNull MatchInfo build() { 898 TextMatchInfo textMatch = null; 899 if (mEmbeddingMatch == null) { 900 textMatch = new TextMatchInfo(mExactMatchRange, mSubmatchRange, mSnippetRange); 901 textMatch.setPropertyPath(mPropertyPath); 902 } 903 return new MatchInfo( 904 mPropertyPath, 905 mExactMatchRange.getStart(), 906 mExactMatchRange.getEnd(), 907 mSubmatchRange.getStart(), 908 mSubmatchRange.getEnd(), 909 mSnippetRange.getStart(), 910 mSnippetRange.getEnd(), 911 textMatch, 912 mEmbeddingMatch); 913 } 914 } 915 } 916 917 /** 918 * This class represents match objects for any text match snippets that might be present in 919 * {@link SearchResults} from a string query. Using this class, you can get: 920 * <ul> 921 * <li>the full text - all of the text in that String property</li> 922 * <li>the exact term match - the 'term' (full word) that matched the query</li> 923 * <li>the subterm match - the portion of the matched term that appears in the query</li> 924 * <li>a suggested text snippet - a portion of the full text surrounding the exact term 925 * match, set to term boundaries. The size of the snippet is specified in 926 * {@link SearchSpec.Builder#setMaxSnippetSize}</li> 927 * </ul> 928 * for each text match in the document. 929 * 930 * <p>Class Example 1: 931 * <p>A document contains the following text in property "subject": 932 * <p>"A commonly used fake word is foo. Another nonsense word that’s used a lot is bar." 933 * 934 * <p>If the queryExpression is "foo" and {@link SearchSpec#getMaxSnippetSize} is 10, 935 * <ul> 936 * <li>{@link TextMatchInfo#getFullText()} returns "A commonly used fake word is foo. 937 * Another nonsense word that’s used a lot is bar."</li> 938 * <li>{@link TextMatchInfo#getExactMatchRange()} returns [29, 32]</li> 939 * <li>{@link TextMatchInfo#getExactMatch()} returns "foo"</li> 940 * <li>{@link TextMatchInfo#getSubmatchRange()} returns [29, 32]</li> 941 * <li>{@link TextMatchInfo#getSubmatch()} returns "foo"</li> 942 * <li>{@link TextMatchInfo#getSnippetRange()} returns [26, 33]</li> 943 * <li>{@link TextMatchInfo#getSnippet()} returns "is foo."</li> 944 * </ul> 945 * <p> 946 * <p>Class Example 2: 947 * <p>A document contains one property named "subject" and one property named "sender" which 948 * contains a "name" property. 949 * 950 * In this case, we will have 2 property paths: {@code sender.name} and {@code subject}. 951 * <p>Let {@code sender.name = "Test Name Jr."} and 952 * {@code subject = "Testing 1 2 3"} 953 * 954 * <p>If the queryExpression is "Test" with {@link SearchSpec#TERM_MATCH_PREFIX} and 955 * {@link SearchSpec#getMaxSnippetSize} is 10. We will have 2 matches: 956 * 957 * <p> Match-1 958 * <ul> 959 * <li>{@link TextMatchInfo#getFullText()} returns "Test Name Jr."</li> 960 * <li>{@link TextMatchInfo#getExactMatchRange()} returns [0, 4]</li> 961 * <li>{@link TextMatchInfo#getExactMatch()} returns "Test"</li> 962 * <li>{@link TextMatchInfo#getSubmatchRange()} returns [0, 4]</li> 963 * <li>{@link TextMatchInfo#getSubmatch()} returns "Test"</li> 964 * <li>{@link TextMatchInfo#getSnippetRange()} returns [0, 9]</li> 965 * <li>{@link TextMatchInfo#getSnippet()} returns "Test Name"</li> 966 * </ul> 967 * <p> Match-2 968 * <ul> 969 * <li>{@link TextMatchInfo#getFullText()} returns "Testing 1 2 3"</li> 970 * <li>{@link TextMatchInfo#getExactMatchRange()} returns [0, 7]</li> 971 * <li>{@link TextMatchInfo#getExactMatch()} returns "Testing"</li> 972 * <li>{@link TextMatchInfo#getSubmatchRange()} returns [0, 4]</li> 973 * <li>{@link TextMatchInfo#getSubmatch()} returns "Test"</li> 974 * <li>{@link TextMatchInfo#getSnippetRange()} returns [0, 9]</li> 975 * <li>{@link TextMatchInfo#getSnippet()} returns "Testing 1"</li> 976 * </ul> 977 */ 978 @SafeParcelable.Class(creator = "TextMatchInfoCreator") 979 @SuppressWarnings("HiddenSuperclass") 980 @FlaggedApi(Flags.FLAG_ENABLE_EMBEDDING_MATCH_INFO) 981 @ExperimentalAppSearchApi 982 public static final class TextMatchInfo extends AbstractSafeParcelable { 983 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 984 @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2) 985 public static final @NonNull Parcelable.Creator<TextMatchInfo> CREATOR = 986 new TextMatchInfoCreator(); 987 988 @Field(id = 1, getter = "getExactMatchRange") 989 private final MatchRange mExactMatchRange; 990 @Field(id = 2, getter = "getSubmatchRange") 991 private final MatchRange mSubmatchRange; 992 @Field(id = 3, getter = "getSnippetRange") 993 private final MatchRange mSnippetRange; 994 995 /** 996 * The path of the matching snippet property. 997 * 998 * <p>If this is {@code null}, methods which require access to the property, like 999 * {@link #getExactMatch}, will throw {@link NullPointerException}. 1000 */ 1001 private @Nullable String mPropertyPath = null; 1002 1003 /** 1004 * Document which the match comes from. 1005 * 1006 * <p>If this is {@code null}, methods which require access to the document, like 1007 * {@link #getExactMatch}, will throw {@link NullPointerException}. 1008 */ 1009 private @Nullable GenericDocument mDocument = null; 1010 1011 /** Full text of the matched property. Populated on first use. */ 1012 private @Nullable String mFullText; 1013 1014 /** 1015 * Creates a new immutable TextMatchInfo. 1016 * 1017 * @param exactMatchRange the exact {@link MatchRange} for the entry. 1018 * @param submatchRange the sub-match {@link MatchRange} for the entry. 1019 * @param snippetRange the snippet {@link MatchRange} for the entry. 1020 */ 1021 @ExperimentalAppSearchApi 1022 @Constructor TextMatchInfo( @aramid = 1) @onNull MatchRange exactMatchRange, @Param(id = 2) @NonNull MatchRange submatchRange, @Param(id = 3) @NonNull MatchRange snippetRange)1023 public TextMatchInfo( 1024 @Param(id = 1) @NonNull MatchRange exactMatchRange, 1025 @Param(id = 2) @NonNull MatchRange submatchRange, 1026 @Param(id = 3) @NonNull MatchRange snippetRange) { 1027 mExactMatchRange = exactMatchRange; 1028 mSubmatchRange = submatchRange; 1029 mSnippetRange = snippetRange; 1030 } 1031 1032 /** 1033 * Gets the full text corresponding to the given entry. 1034 * <p>Class example 1: this returns "A commonly used fake word is foo. Another nonsense 1035 * word that's used a lot is bar." 1036 * <p>Class example 2: for the first {@link TextMatchInfo}, this returns "Test Name Jr." 1037 * and, for the second {@link TextMatchInfo}, this returns "Testing 1 2 3". 1038 */ 1039 @FlaggedApi(Flags.FLAG_ENABLE_EMBEDDING_MATCH_INFO) 1040 @ExperimentalAppSearchApi getFullText()1041 public @NonNull String getFullText() { 1042 if (mFullText == null) { 1043 if (mDocument == null || mPropertyPath == null) { 1044 throw new IllegalStateException( 1045 "Document or property path has not been populated; this TextMatchInfo" 1046 + " cannot be used yet"); 1047 } 1048 mFullText = getPropertyValues(mDocument, mPropertyPath); 1049 } 1050 return mFullText; 1051 } 1052 1053 /** 1054 * Gets the {@link MatchRange} of the exact term of the given entry that matched the query. 1055 * <p>Class example 1: this returns [29, 32]. 1056 * <p>Class example 2: for the first {@link TextMatchInfo}, this returns [0, 4] and, for the 1057 * second {@link TextMatchInfo}, this returns [0, 7]. 1058 */ 1059 @FlaggedApi(Flags.FLAG_ENABLE_EMBEDDING_MATCH_INFO) 1060 @ExperimentalAppSearchApi getExactMatchRange()1061 public @NonNull MatchRange getExactMatchRange() { 1062 return mExactMatchRange; 1063 } 1064 1065 /** 1066 * Gets the exact term of the given entry that matched the query. 1067 * <p>Class example 1: this returns "foo". 1068 * <p>Class example 2: for the first {@link TextMatchInfo}, this returns "Test" and, for the 1069 * second {@link TextMatchInfo}, this returns "Testing". 1070 */ 1071 @FlaggedApi(Flags.FLAG_ENABLE_EMBEDDING_MATCH_INFO) 1072 @ExperimentalAppSearchApi getExactMatch()1073 public @NonNull CharSequence getExactMatch() { 1074 return getSubstring(getExactMatchRange()); 1075 } 1076 1077 /** 1078 * Gets the {@link MatchRange} of the exact term subsequence of the given entry that matched 1079 * the query. 1080 * <p>Class example 1: this returns [29, 32]. 1081 * <p>Class example 2: for the first {@link TextMatchInfo}, this returns [0, 4] and, for the 1082 * second {@link TextMatchInfo}, this returns [0, 4]. 1083 * 1084 * <!--@exportToFramework:ifJetpack()--> 1085 * <p>This information may not be available depending on the backend and Android API 1086 * level. To ensure it is available, call {@link Features#isFeatureSupported}. 1087 * 1088 * @throws UnsupportedOperationException if {@link Features#isFeatureSupported} is 1089 * false. 1090 * <!--@exportToFramework:else()--> 1091 */ 1092 @RequiresFeature( 1093 enforcement = "androidx.appsearch.app.Features#isFeatureSupported", 1094 name = Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH) 1095 @FlaggedApi(Flags.FLAG_ENABLE_EMBEDDING_MATCH_INFO) 1096 @ExperimentalAppSearchApi getSubmatchRange()1097 public @NonNull MatchRange getSubmatchRange() { 1098 checkSubmatchSupported(); 1099 return mSubmatchRange; 1100 } 1101 1102 /** 1103 * Gets the exact term subsequence of the given entry that matched the query. 1104 * <p>Class example 1: this returns "foo". 1105 * <p>Class example 2: for the first {@link TextMatchInfo}, this returns "Test" and, for the 1106 * second {@link TextMatchInfo}, this returns "Test". 1107 * 1108 * <!--@exportToFramework:ifJetpack()--> 1109 * <p>This information may not be available depending on the backend and Android API 1110 * level. To ensure it is available, call {@link Features#isFeatureSupported}. 1111 * 1112 * @throws UnsupportedOperationException if {@link Features#isFeatureSupported} is 1113 * false. 1114 * <!--@exportToFramework:else()--> 1115 */ 1116 @RequiresFeature( 1117 enforcement = "androidx.appsearch.app.Features#isFeatureSupported", 1118 name = Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH) 1119 @FlaggedApi(Flags.FLAG_ENABLE_EMBEDDING_MATCH_INFO) 1120 @ExperimentalAppSearchApi getSubmatch()1121 public @NonNull CharSequence getSubmatch() { 1122 checkSubmatchSupported(); 1123 return getSubstring(getSubmatchRange()); 1124 } 1125 1126 /** 1127 * Gets the snippet {@link TextMatchInfo} corresponding to the given entry. 1128 * <p>Only populated when set maxSnippetSize > 0 in 1129 * {@link SearchSpec.Builder#setMaxSnippetSize}. 1130 * <p>Class example 1: this returns [29, 41]. 1131 * <p>Class example 2: for the first {@link TextMatchInfo}, this returns [0, 9] and, for the 1132 * second {@link TextMatchInfo}, this returns [0, 13]. 1133 */ 1134 @FlaggedApi(Flags.FLAG_ENABLE_EMBEDDING_MATCH_INFO) 1135 @ExperimentalAppSearchApi getSnippetRange()1136 public @NonNull MatchRange getSnippetRange() { 1137 return mSnippetRange; 1138 } 1139 1140 /** 1141 * Gets the snippet corresponding to the given entry. 1142 * <p>Snippet - Provides a subset of the content to display. Only populated when requested 1143 * maxSnippetSize > 0. The size of this content can be changed by 1144 * {@link SearchSpec.Builder#setMaxSnippetSize}. Windowing is centered around the middle of 1145 * the matched token with content on either side clipped to token boundaries. 1146 * <p>Class example 1: this returns "foo. Another". 1147 * <p>Class example 2: for the first {@link TextMatchInfo}, this returns "Test Name" and, 1148 * for 1149 * the second {@link TextMatchInfo}, this returns "Testing 1 2 3". 1150 */ 1151 @FlaggedApi(Flags.FLAG_ENABLE_EMBEDDING_MATCH_INFO) 1152 @ExperimentalAppSearchApi getSnippet()1153 public @NonNull CharSequence getSnippet() { 1154 return getSubstring(getSnippetRange()); 1155 } 1156 getSubstring(MatchRange range)1157 private CharSequence getSubstring(MatchRange range) { 1158 return getFullText().substring(range.getStart(), range.getEnd()); 1159 } 1160 checkSubmatchSupported()1161 private void checkSubmatchSupported() { 1162 if (mSubmatchRange.getStart() == -1) { 1163 throw new UnsupportedOperationException( 1164 "Submatch is not supported with this backend/Android API level " 1165 + "combination"); 1166 } 1167 } 1168 1169 /** Extracts the matching string from the document. */ getPropertyValues(GenericDocument document, String propertyName)1170 private static String getPropertyValues(GenericDocument document, String propertyName) { 1171 String result = document.getPropertyString(propertyName); 1172 if (result == null) { 1173 throw new IllegalStateException( 1174 "No content found for requested property path: " + propertyName); 1175 } 1176 return result; 1177 } 1178 1179 /** 1180 * Sets the {@link GenericDocument} for this {@link TextMatchInfo}. 1181 * 1182 * {@link TextMatchInfo} lacks a constructor that populates {@link TextMatchInfo#mDocument} 1183 * This provides the ability to set {@link TextMatchInfo#mDocument} 1184 */ setDocument(@onNull GenericDocument document)1185 void setDocument(@NonNull GenericDocument document) { 1186 mDocument = Preconditions.checkNotNull(document); 1187 } 1188 1189 /** 1190 * Sets the property path for this {@link TextMatchInfo}. 1191 * 1192 * {@link TextMatchInfo} lacks a constructor that populates 1193 * {@link TextMatchInfo#mPropertyPath} 1194 * This provides the ability to set it. 1195 */ setPropertyPath(@onNull String propertyPath)1196 void setPropertyPath(@NonNull String propertyPath) { 1197 mPropertyPath = Preconditions.checkNotNull(propertyPath); 1198 } 1199 1200 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 1201 @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2) 1202 @Override writeToParcel(@onNull Parcel dest, int flags)1203 public void writeToParcel(@NonNull Parcel dest, int flags) { 1204 TextMatchInfoCreator.writeToParcel(this, dest, flags); 1205 } 1206 } 1207 1208 /** 1209 * This class represents match objects for any snippets that might be present in 1210 * {@link SearchResults} from an embedding query. Using this class, you can get: 1211 * <ul> 1212 * <li>the semantic score of the matching vector with the embedding query</li> 1213 * <li>the query embedding vector index - the index of the query {@link EmbeddingVector} 1214 * in the list returned by {@link SearchSpec#getEmbeddingParameters()}</li> 1215 * <li>the embedding search metric type for the corresponding query</li> 1216 * </ul> 1217 * for each vector match in the document. 1218 */ 1219 @SafeParcelable.Class(creator = "EmbeddingMatchInfoCreator") 1220 @SuppressWarnings("HiddenSuperclass") 1221 @FlaggedApi(Flags.FLAG_ENABLE_EMBEDDING_MATCH_INFO) 1222 @ExperimentalAppSearchApi 1223 public static final class EmbeddingMatchInfo extends AbstractSafeParcelable { 1224 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 1225 @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2) 1226 public static final @NonNull Parcelable.Creator<EmbeddingMatchInfo> CREATOR = 1227 new EmbeddingMatchInfoCreator(); 1228 1229 @Field(id = 1, getter = "getSemanticScore") 1230 private final double mSemanticScore; 1231 1232 @Field(id = 2, getter = "getQueryEmbeddingVectorIndex") 1233 private final int mQueryEmbeddingVectorIndex; 1234 1235 @Field(id = 3, getter = "getEmbeddingSearchMetricType") 1236 private final int mEmbeddingSearchMetricType; 1237 1238 /** 1239 * Creates a new immutable EmbeddingMatchInfo. 1240 * 1241 * @param semanticScore the semantic score of the embedding match against the query vector. 1242 * @param queryEmbeddingVectorIndex the index of the matched query embedding vector in 1243 * {@link SearchSpec#getEmbeddingParameters()} 1244 * @param embeddingSearchMetricType the search metric type used to calculate the score 1245 * for the match and the query vector 1246 */ 1247 @ExperimentalAppSearchApi 1248 @Constructor EmbeddingMatchInfo( @aramid = 1) double semanticScore, @Param(id = 2) int queryEmbeddingVectorIndex, @Param(id = 3) @SearchSpec.EmbeddingSearchMetricType int embeddingSearchMetricType)1249 public EmbeddingMatchInfo( 1250 @Param(id = 1) double semanticScore, 1251 @Param(id = 2) int queryEmbeddingVectorIndex, 1252 @Param(id = 3) 1253 @SearchSpec.EmbeddingSearchMetricType int embeddingSearchMetricType) { 1254 Preconditions.checkArgumentInRange(embeddingSearchMetricType, 1255 EMBEDDING_SEARCH_METRIC_TYPE_COSINE, 1256 EMBEDDING_SEARCH_METRIC_TYPE_EUCLIDEAN, "Embedding search metric type"); 1257 mSemanticScore = semanticScore; 1258 mQueryEmbeddingVectorIndex = queryEmbeddingVectorIndex; 1259 mEmbeddingSearchMetricType = embeddingSearchMetricType; 1260 } 1261 1262 /** 1263 * Gets the semantic score corresponding to the embedding match. 1264 */ 1265 @FlaggedApi(Flags.FLAG_ENABLE_EMBEDDING_MATCH_INFO) 1266 @ExperimentalAppSearchApi getSemanticScore()1267 public double getSemanticScore() { 1268 return mSemanticScore; 1269 } 1270 1271 /** 1272 * Gets the index of the query vector that this embedding match corresponds to. This is 1273 * the index of the query {@link EmbeddingVector} in the list returned by 1274 * {@link SearchSpec#getEmbeddingParameters()} 1275 */ 1276 @FlaggedApi(Flags.FLAG_ENABLE_EMBEDDING_MATCH_INFO) 1277 @ExperimentalAppSearchApi getQueryEmbeddingVectorIndex()1278 public int getQueryEmbeddingVectorIndex() { 1279 return mQueryEmbeddingVectorIndex; 1280 } 1281 1282 /** 1283 * Gets the embedding search metric type that this embedding match corresponds to. 1284 */ 1285 @FlaggedApi(Flags.FLAG_ENABLE_EMBEDDING_MATCH_INFO) 1286 @ExperimentalAppSearchApi 1287 @SearchSpec.EmbeddingSearchMetricType getEmbeddingSearchMetricType()1288 public int getEmbeddingSearchMetricType() { 1289 return mEmbeddingSearchMetricType; 1290 } 1291 1292 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 1293 @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2) 1294 @Override writeToParcel(@onNull Parcel dest, int flags)1295 public void writeToParcel(@NonNull Parcel dest, int flags) { 1296 EmbeddingMatchInfoCreator.writeToParcel(this, dest, flags); 1297 } 1298 } 1299 1300 /** 1301 * Class providing the position range of a text match information. 1302 * 1303 * <p> All ranges are finite, and the left side of the range is always {@code <=} the right 1304 * side of the range. 1305 * 1306 * <p> Example: MatchRange(0, 100) represents hundred ints from 0 to 99." 1307 */ 1308 @SafeParcelable.Class(creator = "MatchRangeCreator") 1309 @SuppressWarnings("HiddenSuperclass") 1310 public static final class MatchRange extends AbstractSafeParcelable { 1311 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 1312 @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2) 1313 public static final @NonNull Parcelable.Creator<MatchRange> CREATOR = 1314 new MatchRangeCreator(); 1315 1316 @Field(id = 1, getter = "getStart") 1317 private final int mStart; 1318 @Field(id = 2, getter = "getEnd") 1319 private final int mEnd; 1320 1321 /** 1322 * Creates a new immutable range. 1323 * <p> The endpoints are {@code [start, end)}; that is the range is bounded. {@code start} 1324 * must be lesser or equal to {@code end}. 1325 * 1326 * @param start The start point (inclusive) 1327 * @param end The end point (exclusive) 1328 */ 1329 @Constructor MatchRange( @aramid = 1) int start, @Param(id = 2) int end)1330 public MatchRange( 1331 @Param(id = 1) int start, 1332 @Param(id = 2) int end) { 1333 if (start > end) { 1334 throw new IllegalArgumentException("Start point must be less than or equal to " 1335 + "end point"); 1336 } 1337 mStart = start; 1338 mEnd = end; 1339 } 1340 1341 /** Gets the start point (inclusive). */ getStart()1342 public int getStart() { 1343 return mStart; 1344 } 1345 1346 /** Gets the end point (exclusive). */ getEnd()1347 public int getEnd() { 1348 return mEnd; 1349 } 1350 1351 @Override equals(@ullable Object other)1352 public boolean equals(@Nullable Object other) { 1353 if (this == other) { 1354 return true; 1355 } 1356 if (!(other instanceof MatchRange)) { 1357 return false; 1358 } 1359 MatchRange otherMatchRange = (MatchRange) other; 1360 return this.getStart() == otherMatchRange.getStart() 1361 && this.getEnd() == otherMatchRange.getEnd(); 1362 } 1363 1364 @Override toString()1365 public @NonNull String toString() { 1366 return "MatchRange { start: " + mStart + " , end: " + mEnd + "}"; 1367 } 1368 1369 @Override hashCode()1370 public int hashCode() { 1371 return ObjectsCompat.hash(mStart, mEnd); 1372 } 1373 1374 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 1375 @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2) 1376 @Override writeToParcel(@onNull Parcel dest, int flags)1377 public void writeToParcel(@NonNull Parcel dest, int flags) { 1378 MatchRangeCreator.writeToParcel(this, dest, flags); 1379 } 1380 } 1381 } 1382