• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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