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