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