/* * Copyright 2020 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.app.appsearch; import static android.app.appsearch.SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_COSINE; import static android.app.appsearch.SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_EUCLIDEAN; import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.appsearch.annotation.CanIgnoreReturnValue; import android.app.appsearch.safeparcel.AbstractSafeParcelable; import android.app.appsearch.safeparcel.GenericDocumentParcel; import android.app.appsearch.safeparcel.SafeParcelable; import android.app.appsearch.util.BundleUtil; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; import android.util.ArrayMap; import com.android.appsearch.flags.Flags; import com.android.internal.util.Preconditions; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; /** * This class represents one of the results obtained from an AppSearch query. * *

This allows clients to obtain: * *

* *

"Snippet" refers to a substring of text from the content of document that is returned as a * part of search result. * * @see SearchResults */ @SafeParcelable.Class(creator = "SearchResultCreator") // TODO(b/384721898): Switch to JSpecify annotations @SuppressWarnings({"HiddenSuperclass", "JSpecifyNullness"}) public final class SearchResult extends AbstractSafeParcelable { @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2) public static final @NonNull Parcelable.Creator CREATOR = new SearchResultCreator(); @Field(id = 1) final GenericDocumentParcel mDocument; @Field(id = 2) final List mMatchInfos; @Field(id = 3, getter = "getPackageName") private final String mPackageName; @Field(id = 4, getter = "getDatabaseName") private final String mDatabaseName; @Field(id = 5, getter = "getRankingSignal") private final double mRankingSignal; @Field(id = 6, getter = "getJoinedResults") private final List mJoinedResults; @Field(id = 7, getter = "getInformationalRankingSignals") private final @NonNull List mInformationalRankingSignals; /** * Holds the map from schema type names to the list of their parent types. * *

The map includes entries for the {@link GenericDocument}'s own type and all of the nested * documents' types. Child types are guaranteed to appear before parent types in each list. * *

Parent types include transitive parents. * *

All schema names in this map are un-prefixed, for both keys and values. */ @Field(id = 8) final @NonNull Bundle mParentTypeMap; /** Cache of the {@link GenericDocument}. Comes from mDocument at first use. */ private @Nullable GenericDocument mDocumentCached; /** Cache of the inflated {@link MatchInfo}. Comes from inflating mMatchInfos at first use. */ private @Nullable List mMatchInfosCached; /** @hide */ @Constructor SearchResult( @Param(id = 1) @NonNull GenericDocumentParcel document, @Param(id = 2) @NonNull List matchInfos, @Param(id = 3) @NonNull String packageName, @Param(id = 4) @NonNull String databaseName, @Param(id = 5) double rankingSignal, @Param(id = 6) @NonNull List joinedResults, @Param(id = 7) @Nullable List informationalRankingSignals, @Param(id = 8) @Nullable Bundle parentTypeMap) { mDocument = Objects.requireNonNull(document); mMatchInfos = Objects.requireNonNull(matchInfos); mPackageName = Objects.requireNonNull(packageName); mDatabaseName = Objects.requireNonNull(databaseName); mRankingSignal = rankingSignal; mJoinedResults = Collections.unmodifiableList(Objects.requireNonNull(joinedResults)); if (informationalRankingSignals != null) { mInformationalRankingSignals = Collections.unmodifiableList(informationalRankingSignals); } else { mInformationalRankingSignals = Collections.emptyList(); } if (parentTypeMap != null) { mParentTypeMap = parentTypeMap; } else { mParentTypeMap = Bundle.EMPTY; } } /** * Contains the matching {@link GenericDocument}. * * @return Document object which matched the query. */ public @NonNull GenericDocument getGenericDocument() { if (mDocumentCached == null) { mDocumentCached = new GenericDocument(mDocument); } return mDocumentCached; } /** * Returns a list of {@link MatchInfo}s providing information about how the document in {@link * #getGenericDocument} matched the query. * * @return List of matches based on {@link SearchSpec}. If snippeting is disabled using {@link * SearchSpec.Builder#setSnippetCount} or {@link * SearchSpec.Builder#setSnippetCountPerProperty}, for all results after that value, this * method returns an empty list. */ public @NonNull List getMatchInfos() { if (mMatchInfosCached == null) { mMatchInfosCached = new ArrayList<>(mMatchInfos.size()); for (int i = 0; i < mMatchInfos.size(); i++) { MatchInfo matchInfo = mMatchInfos.get(i); matchInfo.setDocument(getGenericDocument()); if (matchInfo.getTextMatch() != null) { // This is necessary in order to use the TextMatchInfo after IPC, since // TextMatch.mPropertyPath is private and is not retained by SafeParcelable // across IPC. matchInfo.mTextMatch.setPropertyPath(matchInfo.getPropertyPath()); } if (mMatchInfosCached != null) { // This additional check is added for NullnessChecker. mMatchInfosCached.add(matchInfo); } } mMatchInfosCached = Collections.unmodifiableList(mMatchInfosCached); } // This check is added for NullnessChecker, mMatchInfos will always be NonNull. return Objects.requireNonNull(mMatchInfosCached); } /** * Contains the package name of the app that stored the {@link GenericDocument}. * * @return Package name that stored the document */ public @NonNull String getPackageName() { return mPackageName; } /** * Contains the database name that stored the {@link GenericDocument}. * * @return Name of the database within which the document is stored */ public @NonNull String getDatabaseName() { return mDatabaseName; } /** * Returns the ranking signal of the {@link GenericDocument}, according to the ranking strategy * set in {@link SearchSpec.Builder#setRankingStrategy(int)}. * *

The meaning of the ranking signal and its value is determined by the selected ranking * strategy: * *

* * @return Ranking signal of the document */ public double getRankingSignal() { return mRankingSignal; } /** * Returns the informational ranking signals of the {@link GenericDocument}, according to the * expressions added in {@link SearchSpec.Builder#addInformationalRankingExpressions}. */ @FlaggedApi(Flags.FLAG_ENABLE_INFORMATIONAL_RANKING_EXPRESSIONS) public @NonNull List getInformationalRankingSignals() { return mInformationalRankingSignals; } /** * Returns the map from schema type names to the list of their parent types. * *

The map includes entries for the {@link GenericDocument}'s own type and all of the nested * documents' types. Child types are guaranteed to appear before parent types in each list. * *

Parent types include transitive parents. * *

Calling this function repeatedly is inefficient. Prefer to retain the Map returned by this * function, rather than calling it multiple times. */ @FlaggedApi(Flags.FLAG_ENABLE_SEARCH_RESULT_PARENT_TYPES) public @NonNull Map> getParentTypeMap() { Set schemaTypes = mParentTypeMap.keySet(); Map> parentTypeMap = new ArrayMap<>(schemaTypes.size()); for (String schemaType : schemaTypes) { ArrayList parentTypes = mParentTypeMap.getStringArrayList(schemaType); if (parentTypes != null) { parentTypeMap.put(schemaType, parentTypes); } } return parentTypeMap; } /** * Gets a list of {@link SearchResult} joined from the join operation. * *

These joined documents match the outer document as specified in the {@link JoinSpec} with * parentPropertyExpression and childPropertyExpression. They are ordered according to the * {@link JoinSpec#getNestedSearchSpec}, and as many SearchResults as specified by {@link * JoinSpec#getMaxJoinedResultCount} will be returned. If no {@link JoinSpec} was specified, * this returns an empty list. * *

This method is inefficient to call repeatedly, as new {@link SearchResult} objects are * created each time. * * @return a List of SearchResults containing joined documents. */ public @NonNull List getJoinedResults() { return mJoinedResults; } @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2) @Override public void writeToParcel(@NonNull Parcel dest, int flags) { SearchResultCreator.writeToParcel(this, dest, flags); } /** Builder for {@link SearchResult} objects. */ public static final class Builder { private final String mPackageName; private final String mDatabaseName; private List mMatchInfos = new ArrayList<>(); private GenericDocument mGenericDocument; private double mRankingSignal; private List mInformationalRankingSignals = new ArrayList<>(); private Bundle mParentTypeMap = new Bundle(); private List mJoinedResults = new ArrayList<>(); private boolean mBuilt = false; /** * Constructs a new builder for {@link SearchResult} objects. * * @param packageName the package name the matched document belongs to * @param databaseName the database name the matched document belongs to. */ public Builder(@NonNull String packageName, @NonNull String databaseName) { mPackageName = Objects.requireNonNull(packageName); mDatabaseName = Objects.requireNonNull(databaseName); } /** @hide */ public Builder(@NonNull SearchResult searchResult) { Objects.requireNonNull(searchResult); mPackageName = searchResult.getPackageName(); mDatabaseName = searchResult.getDatabaseName(); mGenericDocument = searchResult.getGenericDocument(); mRankingSignal = searchResult.getRankingSignal(); mInformationalRankingSignals = new ArrayList<>(searchResult.getInformationalRankingSignals()); setParentTypeMap(searchResult.getParentTypeMap()); List matchInfos = searchResult.getMatchInfos(); for (int i = 0; i < matchInfos.size(); i++) { addMatchInfo(new MatchInfo.Builder(matchInfos.get(i)).build()); } List joinedResults = searchResult.getJoinedResults(); for (int i = 0; i < joinedResults.size(); i++) { addJoinedResult(joinedResults.get(i)); } } /** Sets the document which matched. */ @CanIgnoreReturnValue public @NonNull Builder setGenericDocument(@NonNull GenericDocument document) { Objects.requireNonNull(document); resetIfBuilt(); mGenericDocument = document; return this; } /** Adds another match to this SearchResult. */ @CanIgnoreReturnValue public @NonNull Builder addMatchInfo(@NonNull MatchInfo matchInfo) { Preconditions.checkState( matchInfo.mDocument == null, "This MatchInfo is already associated with a SearchResult and can't be " + "reassigned"); resetIfBuilt(); mMatchInfos.add(matchInfo); return this; } /** Sets the ranking signal of the matched document in this SearchResult. */ @CanIgnoreReturnValue public @NonNull Builder setRankingSignal(double rankingSignal) { resetIfBuilt(); mRankingSignal = rankingSignal; return this; } /** Adds the informational ranking signal of the matched document in this SearchResult. */ @CanIgnoreReturnValue @FlaggedApi(Flags.FLAG_ENABLE_INFORMATIONAL_RANKING_EXPRESSIONS) public @NonNull Builder addInformationalRankingSignal(double rankingSignal) { resetIfBuilt(); mInformationalRankingSignals.add(rankingSignal); return this; } /** * Sets the map from schema type names to the list of their parent types. * *

The map should include entries for the {@link GenericDocument}'s own type and all of * the nested documents' types. * *

Child types must appear before parent types in each list. Otherwise, the * GenericDocument's toDocumentClass method (an AndroidX-only API) may not correctly * identify the most concrete type. This could lead to unintended deserialization into a * more general type instead of a more specific type. * *

Parent types should include transitive parents. */ @CanIgnoreReturnValue @FlaggedApi(Flags.FLAG_ENABLE_SEARCH_RESULT_PARENT_TYPES) public @NonNull Builder setParentTypeMap(@NonNull Map> parentTypeMap) { Objects.requireNonNull(parentTypeMap); resetIfBuilt(); mParentTypeMap.clear(); for (Map.Entry> entry : parentTypeMap.entrySet()) { Objects.requireNonNull(entry.getKey()); Objects.requireNonNull(entry.getValue()); ArrayList parentTypes = new ArrayList<>(entry.getValue().size()); for (int i = 0; i < entry.getValue().size(); i++) { String parentType = entry.getValue().get(i); parentTypes.add(Objects.requireNonNull(parentType)); } mParentTypeMap.putStringArrayList(entry.getKey(), parentTypes); } return this; } /** * Adds a {@link SearchResult} that was joined by the {@link JoinSpec}. * * @param joinedResult The joined SearchResult to add. */ @CanIgnoreReturnValue public @NonNull Builder addJoinedResult(@NonNull SearchResult joinedResult) { resetIfBuilt(); mJoinedResults.add(joinedResult); return this; } /** * Clears the {@link MatchInfo}s. * * @hide */ @CanIgnoreReturnValue public @NonNull Builder clearMatchInfos() { resetIfBuilt(); mMatchInfos.clear(); return this; } /** * Clears the {@link SearchResult}s that were joined. * * @hide */ @CanIgnoreReturnValue public @NonNull Builder clearJoinedResults() { resetIfBuilt(); mJoinedResults.clear(); return this; } /** Constructs a new {@link SearchResult}. */ public @NonNull SearchResult build() { mBuilt = true; return new SearchResult( mGenericDocument.getDocumentParcel(), mMatchInfos, mPackageName, mDatabaseName, mRankingSignal, mJoinedResults, mInformationalRankingSignals, mParentTypeMap); } private void resetIfBuilt() { if (mBuilt) { mMatchInfos = new ArrayList<>(mMatchInfos); mJoinedResults = new ArrayList<>(mJoinedResults); mInformationalRankingSignals = new ArrayList<>(mInformationalRankingSignals); mParentTypeMap = BundleUtil.deepCopy(mParentTypeMap); mBuilt = false; } } } /** * This class represents match objects for any snippets that might be present in {@link * SearchResults} from a query. * *

A {@link MatchInfo} contains either a {@link TextMatchInfo} representing a text match * snippet, or an {@link EmbeddingMatchInfo} representing an embedding match snippet. */ @SafeParcelable.Class(creator = "MatchInfoCreator") @SuppressWarnings("HiddenSuperclass") public static final class MatchInfo extends AbstractSafeParcelable { @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2) public static final @NonNull Parcelable.Creator CREATOR = new MatchInfoCreator(); /** The path of the matching snippet property. */ @Field(id = 1, getter = "getPropertyPath") private final String mPropertyPath; @Field(id = 2) final int mExactMatchRangeStart; @Field(id = 3) final int mExactMatchRangeEnd; @Field(id = 4) final int mSubmatchRangeStart; @Field(id = 5) final int mSubmatchRangeEnd; @Field(id = 6) final int mSnippetRangeStart; @Field(id = 7) final int mSnippetRangeEnd; /** Represents text-based match information. */ @Field(id = 8, getter = "getTextMatch") private @Nullable final TextMatchInfo mTextMatch; /** Represents embedding-based match information. */ @Field(id = 9, getter = "getEmbeddingMatch") private @Nullable final EmbeddingMatchInfo mEmbeddingMatch; private @Nullable PropertyPath mPropertyPathObject = null; /** * Document which the match comes from. * *

If this is {@code null}, methods which require access to the document, like {@link * #getExactMatch}, will throw {@link NullPointerException}. */ private @Nullable GenericDocument mDocument = null; @Constructor MatchInfo( @Param(id = 1) @NonNull 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) { mPropertyPath = Objects.requireNonNull(propertyPath); mExactMatchRangeStart = exactMatchRangeStart; mExactMatchRangeEnd = exactMatchRangeEnd; mSubmatchRangeStart = submatchRangeStart; mSubmatchRangeEnd = submatchRangeEnd; mSnippetRangeStart = snippetRangeStart; mSnippetRangeEnd = snippetRangeEnd; mEmbeddingMatch = embeddingMatchInfo; TextMatchInfo tempTextMatch = textMatchInfo; if (tempTextMatch == null && mEmbeddingMatch == null) { tempTextMatch = new TextMatchInfo( exactMatchRangeStart, exactMatchRangeEnd, submatchRangeStart, submatchRangeEnd, snippetRangeStart, snippetRangeEnd); tempTextMatch.setPropertyPath(mPropertyPath); } mTextMatch = tempTextMatch; } /** * Gets the property path corresponding to the given entry. * *

A property path is a '.' - delimited sequence of property names indicating which * property in the document these snippets correspond to. * *

Example properties: 'body', 'sender.name', 'sender.emailaddress', etc. For class * example 1 this returns "subject" */ public @NonNull String getPropertyPath() { return mPropertyPath; } /** * Gets a {@link PropertyPath} object representing the property path corresponding to the * given entry. * *

Methods such as {@link GenericDocument#getPropertyDocument} accept a path as a string * rather than a {@link PropertyPath} object. However, you may want to manipulate the path * before getting a property document. This method returns a {@link PropertyPath} rather * than a String for easier path manipulation, which can then be converted to a String. * * @see #getPropertyPath * @see PropertyPath */ public @NonNull PropertyPath getPropertyPathObject() { if (mPropertyPathObject == null) { mPropertyPathObject = new PropertyPath(mPropertyPath); } return mPropertyPathObject; } /** * Retrieves the text-based match information. * * @return A {@link TextMatchInfo} instance, or null if the match is not text-based. */ @FlaggedApi(Flags.FLAG_ENABLE_EMBEDDING_MATCH_INFO) public @Nullable TextMatchInfo getTextMatch() { return mTextMatch; } /** * Retrieves the embedding-based match information. Only populated when {@link * SearchSpec#shouldRetrieveEmbeddingMatchInfos()} is true. * * @return A {@link EmbeddingMatchInfo} instance, or null if the match is not an embedding * match. */ @FlaggedApi(Flags.FLAG_ENABLE_EMBEDDING_MATCH_INFO) public @Nullable EmbeddingMatchInfo getEmbeddingMatch() { return mEmbeddingMatch; } /** * Gets the full text corresponding to the given entry. Returns an empty string if the match * is not text-based. */ public @NonNull String getFullText() { if (mTextMatch == null) { return ""; } return mTextMatch.getFullText(); } /** * Gets the {@link MatchRange} of the exact term of the given entry that matched the query. * Returns [0, 0] if the match is not text-based. */ public @NonNull MatchRange getExactMatchRange() { if (mTextMatch == null) { return new MatchRange(0, 0); } return mTextMatch.getExactMatchRange(); } /** * Gets the exact term of the given entry that matched the query. Returns an empty * CharSequence if the match is not text-based. */ public @NonNull CharSequence getExactMatch() { if (mTextMatch == null) { return ""; } return mTextMatch.getExactMatch(); } /** * Gets the {@link MatchRange} of the submatch term subsequence of the given entry that * matched the query. Returns [0, 0] if the match is not text-based. */ public @NonNull MatchRange getSubmatchRange() { if (mTextMatch == null) { return new MatchRange(0, 0); } return mTextMatch.getSubmatchRange(); } /** * Gets the exact term subsequence of the given entry that matched the query. Returns an * empty CharSequence if the match is not text-based. */ public @NonNull CharSequence getSubmatch() { if (mTextMatch == null) { return ""; } return mTextMatch.getSubmatch(); } /** * Gets the snippet {@link MatchRange} corresponding to the given entry. Returns [0,0] if * the match is not text-based. * *

Only populated when set maxSnippetSize > 0 in {@link * SearchSpec.Builder#setMaxSnippetSize}. */ public @NonNull MatchRange getSnippetRange() { if (mTextMatch == null) { return new MatchRange(0, 0); } return mTextMatch.getSnippetRange(); } /** * Gets the snippet corresponding to the given entry. Returns an empty CharSequence if the * match is not text-based. * *

Snippet - Provides a subset of the content to display. Only populated when requested * maxSnippetSize > 0. The size of this content can be changed by {@link * SearchSpec.Builder#setMaxSnippetSize}. Windowing is centered around the middle of the * matched token with content on either side clipped to token boundaries. */ public @NonNull CharSequence getSnippet() { if (mTextMatch == null) { return ""; } return mTextMatch.getSnippet(); } /** * Sets the {@link GenericDocument} for {@link MatchInfo}. * *

{@link MatchInfo} lacks a constructor that populates {@link MatchInfo#mDocument} This * provides the ability to set {@link MatchInfo#mDocument} */ void setDocument(@NonNull GenericDocument document) { mDocument = Objects.requireNonNull(document); if (mTextMatch != null) { mTextMatch.setDocument(document); } } @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2) @Override public void writeToParcel(@NonNull Parcel dest, int flags) { MatchInfoCreator.writeToParcel(this, dest, flags); } /** Builder for {@link MatchInfo} objects. */ public static final class Builder { private final String mPropertyPath; private EmbeddingMatchInfo mEmbeddingMatch = null; private MatchRange mExactMatchRange = new MatchRange(0, 0); private MatchRange mSubmatchRange = new MatchRange(-1, -1); private MatchRange mSnippetRange = new MatchRange(0, 0); /** * Creates a new {@link MatchInfo.Builder} reporting a match with the given property * path. * *

A property path is a dot-delimited sequence of property names indicating which * property in the document these snippets correspond to. * *

Example properties: 'body', 'sender.name', 'sender.emailaddress', etc. For class * example 1, this returns "subject". * * @param propertyPath A dot-delimited sequence of property names indicating which * property in the document these snippets correspond to. */ public Builder(@NonNull String propertyPath) { mPropertyPath = Objects.requireNonNull(propertyPath); } /** @hide */ public Builder(@NonNull MatchInfo matchInfo) { Objects.requireNonNull(matchInfo); mPropertyPath = matchInfo.mPropertyPath; mEmbeddingMatch = matchInfo.getEmbeddingMatch(); mExactMatchRange = matchInfo.getExactMatchRange(); // Using the fields directly instead of getSubmatchRange() to bypass the // checkSubmatchSupported check. mSubmatchRange = new MatchRange(matchInfo.mSubmatchRangeStart, matchInfo.mSubmatchRangeEnd); mSnippetRange = matchInfo.getSnippetRange(); } /** Sets the {@link EmbeddingMatchInfo} corresponding to the given entry. */ @FlaggedApi(Flags.FLAG_ENABLE_EMBEDDING_MATCH_INFO) @CanIgnoreReturnValue public @NonNull Builder setEmbeddingMatch(@Nullable EmbeddingMatchInfo embeddingMatch) { mEmbeddingMatch = embeddingMatch; return this; } /** Sets the exact {@link MatchRange} corresponding to the given entry. */ @CanIgnoreReturnValue public @NonNull Builder setExactMatchRange(@NonNull MatchRange matchRange) { mExactMatchRange = Objects.requireNonNull(matchRange); return this; } /** Sets the submatch {@link MatchRange} corresponding to the given entry. */ @CanIgnoreReturnValue public @NonNull Builder setSubmatchRange(@NonNull MatchRange matchRange) { mSubmatchRange = Objects.requireNonNull(matchRange); return this; } /** Sets the snippet {@link MatchRange} corresponding to the given entry. */ @CanIgnoreReturnValue public @NonNull Builder setSnippetRange(@NonNull MatchRange matchRange) { mSnippetRange = Objects.requireNonNull(matchRange); return this; } /** Constructs a new {@link MatchInfo}. */ public @NonNull MatchInfo build() { TextMatchInfo textMatch = null; if (mEmbeddingMatch == null) { textMatch = new TextMatchInfo( mExactMatchRange.getStart(), mExactMatchRange.getEnd(), mSubmatchRange.getStart(), mSubmatchRange.getEnd(), mSnippetRange.getStart(), mSnippetRange.getEnd()); textMatch.setPropertyPath(mPropertyPath); } return new MatchInfo( mPropertyPath, mExactMatchRange.getStart(), mExactMatchRange.getEnd(), mSubmatchRange.getStart(), mSubmatchRange.getEnd(), mSnippetRange.getStart(), mSnippetRange.getEnd(), textMatch, mEmbeddingMatch); } } } /** * This class represents match objects for any text match snippets that might be present in * {@link SearchResults} from a string query. Using this class, you can get: * *

* * for each text match in the document. * *

Class Example 1: * *

A document contains the following text in property "subject": * *

"A commonly used fake word is foo. Another nonsense word that’s used a lot is bar." * *

If the queryExpression is "foo" and {@link SearchSpec#getMaxSnippetSize} is 10, * *

* *

* *

Class Example 2: * *

A document contains one property named "subject" and one property named "sender" which * contains a "name" property. * *

In this case, we will have 2 property paths: {@code sender.name} and {@code subject}. * *

Let {@code sender.name = "Test Name Jr."} and {@code subject = "Testing 1 2 3"} * *

If the queryExpression is "Test" with {@link SearchSpec#TERM_MATCH_PREFIX} and {@link * SearchSpec#getMaxSnippetSize} is 10. We will have 2 matches: * *

Match-1 * *

* *

Match-2 * *

*/ @SafeParcelable.Class(creator = "TextMatchInfoCreator") @SuppressWarnings("HiddenSuperclass") @FlaggedApi(Flags.FLAG_ENABLE_EMBEDDING_MATCH_INFO) public static final class TextMatchInfo extends AbstractSafeParcelable { @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2) public static final @NonNull Parcelable.Creator CREATOR = new TextMatchInfoCreator(); @Field(id = 1) final int mExactMatchRangeStart; @Field(id = 2) final int mExactMatchRangeEnd; @Field(id = 3) final int mSubmatchRangeStart; @Field(id = 4) final int mSubmatchRangeEnd; @Field(id = 5) final int mSnippetRangeStart; @Field(id = 6) final int mSnippetRangeEnd; /** * The path of the matching snippet property. * *

If this is {@code null}, methods which require access to the property, like {@link * #getExactMatch}, will throw {@link NullPointerException}. */ private @Nullable String mPropertyPath = null; /** * Document which the match comes from. * *

If this is {@code null}, methods which require access to the document, like {@link * #getExactMatch}, will throw {@link NullPointerException}. */ private @Nullable GenericDocument mDocument = null; /** Full text of the matched property. Populated on first use. */ private @Nullable String mFullText; /** Range of property that exactly matched the query. Populated on first use. */ private @Nullable MatchRange mExactMatchRangeCached; /** * Range of property that corresponds to the subsequence of the exact match that directly * matches a query term. Populated on first use. */ private @Nullable MatchRange mSubmatchRangeCached; /** Range of some reasonable amount of context around the query. Populated on first use. */ private @Nullable MatchRange mWindowRangeCached; /** * Creates a new immutable TextMatchInfo. * * @param exactMatchRangeStart the start of the exact {@link MatchRange} for the entry. * @param exactMatchRangeEnd the end of the exact {@link MatchRange} for the entry. * @param submatchRangeStart the start of the sub-match {@link MatchRange} for the entry. * @param submatchRangeEnd the end of the sub-match {@link MatchRange} for the entry. * @param snippetRangeStart the start of the snippet {@link MatchRange} for the entry. * @param snippetRangeEnd the end of the snippet {@link MatchRange} for the entry. */ @Constructor public TextMatchInfo( @Param(id = 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) { mExactMatchRangeStart = exactMatchRangeStart; mExactMatchRangeEnd = exactMatchRangeEnd; mSubmatchRangeStart = submatchRangeStart; mSubmatchRangeEnd = submatchRangeEnd; mSnippetRangeStart = snippetRangeStart; mSnippetRangeEnd = snippetRangeEnd; } /** * Gets the full text corresponding to the given entry. * *

Class example 1: this returns "A commonly used fake word is foo. Another nonsense word * that's used a lot is bar." * *

Class example 2: for the first {@link TextMatchInfo}, this returns "Test Name Jr." * and, for the second {@link TextMatchInfo}, this returns "Testing 1 2 3". */ @FlaggedApi(Flags.FLAG_ENABLE_EMBEDDING_MATCH_INFO) public @NonNull String getFullText() { if (mFullText == null) { if (mDocument == null || mPropertyPath == null) { throw new IllegalStateException( "Document or property path has not been populated; this TextMatchInfo" + " cannot be used yet"); } mFullText = getPropertyValues(mDocument, mPropertyPath); } return mFullText; } /** * Gets the {@link MatchRange} of the exact term of the given entry that matched the query. * *

Class example 1: this returns [29, 32]. * *

Class example 2: for the first {@link TextMatchInfo}, this returns [0, 4] and, for the * second {@link TextMatchInfo}, this returns [0, 7]. */ @FlaggedApi(Flags.FLAG_ENABLE_EMBEDDING_MATCH_INFO) public @NonNull MatchRange getExactMatchRange() { if (mExactMatchRangeCached == null) { mExactMatchRangeCached = new MatchRange(mExactMatchRangeStart, mExactMatchRangeEnd); } return mExactMatchRangeCached; } /** * Gets the exact term of the given entry that matched the query. * *

Class example 1: this returns "foo". * *

Class example 2: for the first {@link TextMatchInfo}, this returns "Test" and, for the * second {@link TextMatchInfo}, this returns "Testing". */ @FlaggedApi(Flags.FLAG_ENABLE_EMBEDDING_MATCH_INFO) public @NonNull CharSequence getExactMatch() { return getSubstring(getExactMatchRange()); } /** * Gets the {@link MatchRange} of the exact term subsequence of the given entry that matched * the query. * *

Class example 1: this returns [29, 32]. * *

Class example 2: for the first {@link TextMatchInfo}, this returns [0, 4] and, for the * second {@link TextMatchInfo}, this returns [0, 4]. */ @FlaggedApi(Flags.FLAG_ENABLE_EMBEDDING_MATCH_INFO) public @NonNull MatchRange getSubmatchRange() { checkSubmatchSupported(); if (mSubmatchRangeCached == null) { mSubmatchRangeCached = new MatchRange(mSubmatchRangeStart, mSubmatchRangeEnd); } return mSubmatchRangeCached; } /** * Gets the exact term subsequence of the given entry that matched the query. * *

Class example 1: this returns "foo". * *

Class example 2: for the first {@link TextMatchInfo}, this returns "Test" and, for the * second {@link TextMatchInfo}, this returns "Test". */ @FlaggedApi(Flags.FLAG_ENABLE_EMBEDDING_MATCH_INFO) public @NonNull CharSequence getSubmatch() { checkSubmatchSupported(); return getSubstring(getSubmatchRange()); } /** * Gets the snippet {@link TextMatchInfo} corresponding to the given entry. * *

Only populated when set maxSnippetSize > 0 in {@link * SearchSpec.Builder#setMaxSnippetSize}. * *

Class example 1: this returns [29, 41]. * *

Class example 2: for the first {@link TextMatchInfo}, this returns [0, 9] and, for the * second {@link TextMatchInfo}, this returns [0, 13]. */ @FlaggedApi(Flags.FLAG_ENABLE_EMBEDDING_MATCH_INFO) public @NonNull MatchRange getSnippetRange() { if (mWindowRangeCached == null) { mWindowRangeCached = new MatchRange(mSnippetRangeStart, mSnippetRangeEnd); } return mWindowRangeCached; } /** * Gets the snippet corresponding to the given entry. * *

Snippet - Provides a subset of the content to display. Only populated when requested * maxSnippetSize > 0. The size of this content can be changed by {@link * SearchSpec.Builder#setMaxSnippetSize}. Windowing is centered around the middle of the * matched token with content on either side clipped to token boundaries. * *

Class example 1: this returns "foo. Another". * *

Class example 2: for the first {@link TextMatchInfo}, this returns "Test Name" and, * for the second {@link TextMatchInfo}, this returns "Testing 1 2 3". */ @FlaggedApi(Flags.FLAG_ENABLE_EMBEDDING_MATCH_INFO) public @NonNull CharSequence getSnippet() { return getSubstring(getSnippetRange()); } private CharSequence getSubstring(MatchRange range) { return getFullText().substring(range.getStart(), range.getEnd()); } private void checkSubmatchSupported() { if (mSubmatchRangeStart == -1) { throw new UnsupportedOperationException( "Submatch is not supported with this backend/Android API level " + "combination"); } } /** Extracts the matching string from the document. */ private static String getPropertyValues(GenericDocument document, String propertyName) { String result = document.getPropertyString(propertyName); if (result == null) { throw new IllegalStateException( "No content found for requested property path: " + propertyName); } return result; } /** * Sets the {@link GenericDocument} for this {@link TextMatchInfo}. * *

{@link TextMatchInfo} lacks a constructor that populates {@link * TextMatchInfo#mDocument} This provides the ability to set {@link TextMatchInfo#mDocument} */ void setDocument(@NonNull GenericDocument document) { mDocument = Objects.requireNonNull(document); } /** * Sets the property path for this {@link TextMatchInfo}. * *

{@link TextMatchInfo} lacks a constructor that populates {@link * TextMatchInfo#mPropertyPath} This provides the ability to set it. */ void setPropertyPath(@NonNull String propertyPath) { mPropertyPath = Objects.requireNonNull(propertyPath); } @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2) @Override public void writeToParcel(@NonNull Parcel dest, int flags) { TextMatchInfoCreator.writeToParcel(this, dest, flags); } } /** * This class represents match objects for any snippets that might be present in {@link * SearchResults} from an embedding query. Using this class, you can get: * *

* * for each vector match in the document. */ @SafeParcelable.Class(creator = "EmbeddingMatchInfoCreator") @SuppressWarnings("HiddenSuperclass") @FlaggedApi(Flags.FLAG_ENABLE_EMBEDDING_MATCH_INFO) public static final class EmbeddingMatchInfo extends AbstractSafeParcelable { @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2) public static final @NonNull Parcelable.Creator CREATOR = new EmbeddingMatchInfoCreator(); @Field(id = 1, getter = "getSemanticScore") private final double mSemanticScore; @Field(id = 2, getter = "getQueryEmbeddingVectorIndex") private final int mQueryEmbeddingVectorIndex; @Field(id = 3, getter = "getEmbeddingSearchMetricType") private final int mEmbeddingSearchMetricType; /** * Creates a new immutable EmbeddingMatchInfo. * * @param semanticScore the semantic score of the embedding match against the query vector. * @param queryEmbeddingVectorIndex the index of the matched query embedding vector in * {@link SearchSpec#getEmbeddingParameters()} * @param embeddingSearchMetricType the search metric type used to calculate the score for * the match and the query vector */ @Constructor public EmbeddingMatchInfo( @Param(id = 1) double semanticScore, @Param(id = 2) int queryEmbeddingVectorIndex, @Param(id = 3) @SearchSpec.EmbeddingSearchMetricType int embeddingSearchMetricType) { Preconditions.checkArgumentInRange( embeddingSearchMetricType, EMBEDDING_SEARCH_METRIC_TYPE_COSINE, EMBEDDING_SEARCH_METRIC_TYPE_EUCLIDEAN, "Embedding search metric type"); mSemanticScore = semanticScore; mQueryEmbeddingVectorIndex = queryEmbeddingVectorIndex; mEmbeddingSearchMetricType = embeddingSearchMetricType; } /** Gets the semantic score corresponding to the embedding match. */ @FlaggedApi(Flags.FLAG_ENABLE_EMBEDDING_MATCH_INFO) public double getSemanticScore() { return mSemanticScore; } /** * Gets the index of the query vector that this embedding match corresponds to. This is the * index of the query {@link EmbeddingVector} in the list returned by {@link * SearchSpec#getEmbeddingParameters()} */ @FlaggedApi(Flags.FLAG_ENABLE_EMBEDDING_MATCH_INFO) public int getQueryEmbeddingVectorIndex() { return mQueryEmbeddingVectorIndex; } /** Gets the embedding search metric type that this embedding match corresponds to. */ @FlaggedApi(Flags.FLAG_ENABLE_EMBEDDING_MATCH_INFO) @SearchSpec.EmbeddingSearchMetricType public int getEmbeddingSearchMetricType() { return mEmbeddingSearchMetricType; } @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2) @Override public void writeToParcel(@NonNull Parcel dest, int flags) { EmbeddingMatchInfoCreator.writeToParcel(this, dest, flags); } } /** * Class providing the position range of a text match information. * *

All ranges are finite, and the left side of the range is always {@code <=} the right side * of the range. * *

Example: MatchRange(0, 100) represents hundred ints from 0 to 99." */ public static final class MatchRange { private final int mEnd; private final int mStart; /** * Creates a new immutable range. * *

The endpoints are {@code [start, end)}; that is the range is bounded. {@code start} * must be lesser or equal to {@code end}. * * @param start The start point (inclusive) * @param end The end point (exclusive) */ public MatchRange(int start, int end) { if (start > end) { throw new IllegalArgumentException( "Start point must be less than or equal to " + "end point"); } mStart = start; mEnd = end; } /** Gets the start point (inclusive). */ public int getStart() { return mStart; } /** Gets the end point (exclusive). */ public int getEnd() { return mEnd; } @Override public boolean equals(@Nullable Object other) { if (this == other) { return true; } if (!(other instanceof MatchRange)) { return false; } MatchRange otherMatchRange = (MatchRange) other; return this.getStart() == otherMatchRange.getStart() && this.getEnd() == otherMatchRange.getEnd(); } @Override public @NonNull String toString() { return "MatchRange { start: " + mStart + " , end: " + mEnd + "}"; } @Override public int hashCode() { return Objects.hash(mStart, mEnd); } } }