public final class

SearchSpec

extends java.lang.Object

 java.lang.Object

↳androidx.appsearch.app.SearchSpec

Gradle dependencies

compile group: 'androidx.appsearch', name: 'appsearch', version: '1.0.0-alpha04'

  • groupId: androidx.appsearch
  • artifactId: appsearch
  • version: 1.0.0-alpha04

Artifact androidx.appsearch:appsearch:1.0.0-alpha04 it located at Google repository (https://maven.google.com/)

Overview

This class represents the specification logic for AppSearch. It can be used to set the type of search, like prefix or exact only or apply filters to search for a specific schema type only etc.

Summary

Fields
public static final intDEFAULT_NUM_PER_PAGE

public static final intGROUPING_TYPE_PER_NAMESPACE

Results should be grouped together by namespace for the purpose of enforcing a limit on the number of results returned per namespace.

public static final intGROUPING_TYPE_PER_PACKAGE

Results should be grouped together by package for the purpose of enforcing a limit on the number of results returned per package.

public static final intORDER_ASCENDING

Search results will be returned in an ascending order.

public static final intORDER_DESCENDING

Search results will be returned in a descending order.

public static final java.lang.StringPROJECTION_SCHEMA_TYPE_WILDCARD

Schema type to be used in SearchSpec.Builder.addProjection(String, Collection) to apply property paths to all results, excepting any types that have had their own, specific property paths set.

public static final intRANKING_STRATEGY_CREATION_TIMESTAMP

Ranked by document creation timestamps.

public static final intRANKING_STRATEGY_DOCUMENT_SCORE

Ranked by app-provided document scores.

public static final intRANKING_STRATEGY_NONE

No Ranking, results are returned in arbitrary order.

public static final intRANKING_STRATEGY_RELEVANCE_SCORE

Ranked by document relevance score.

public static final intRANKING_STRATEGY_SYSTEM_USAGE_COUNT

Ranked by number of usages from a system UI surface.

public static final intRANKING_STRATEGY_SYSTEM_USAGE_LAST_USED_TIMESTAMP

Ranked by timestamp of last usage from a system UI surface.

public static final intRANKING_STRATEGY_USAGE_COUNT

Ranked by number of usages, as reported by the app.

public static final intRANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP

Ranked by timestamp of last usage, as reported by the app.

public static final intTERM_MATCH_EXACT_ONLY

Query terms will only match exact tokens in the index.

public static final intTERM_MATCH_PREFIX

Query terms will match indexed tokens when the query term is a prefix of the token.

Constructors
publicSearchSpec(Bundle bundle)

Methods
public BundlegetBundle()

Returns the populated by this builder.

public java.util.List<java.lang.String>getFilterNamespaces()

Returns the list of namespaces to search over.

public java.util.List<java.lang.String>getFilterPackageNames()

Returns the list of package name filters to search over.

public java.util.List<java.lang.String>getFilterSchemas()

Returns the list of schema types to search for.

public intgetMaxSnippetSize()

Returns the maximum size of a snippet in characters.

public intgetOrder()

Returns the order of returned search results (descending or ascending).

public java.util.Map<java.lang.String, java.util.List>getProjections()

Returns a map from schema type to property paths to be used for projection.

public intgetRankingStrategy()

Returns the ranking strategy.

public intgetResultCountPerPage()

Returns the number of results per page in the result set.

public intgetResultGroupingLimit()

Get the maximum number of results to return for each group.

public intgetResultGroupingTypeFlags()

Get the type of grouping limit to apply, or 0 if SearchSpec.Builder.setResultGrouping(int, int) was not called.

public intgetSnippetCount()

Returns how many documents to generate snippets for.

public intgetSnippetCountPerProperty()

Returns how many matches for each property of a matching document to generate snippets for.

public intgetTermMatch()

Returns how the query terms should match terms in the index.

from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Fields

public static final java.lang.String PROJECTION_SCHEMA_TYPE_WILDCARD

Schema type to be used in SearchSpec.Builder.addProjection(String, Collection) to apply property paths to all results, excepting any types that have had their own, specific property paths set.

public static final int DEFAULT_NUM_PER_PAGE

public static final int TERM_MATCH_EXACT_ONLY

Query terms will only match exact tokens in the index.

Ex. A query term "foo" will only match indexed token "foo", and not "foot" or "football".

public static final int TERM_MATCH_PREFIX

Query terms will match indexed tokens when the query term is a prefix of the token.

Ex. A query term "foo" will match indexed tokens like "foo", "foot", and "football".

public static final int RANKING_STRATEGY_NONE

No Ranking, results are returned in arbitrary order.

public static final int RANKING_STRATEGY_DOCUMENT_SCORE

Ranked by app-provided document scores.

public static final int RANKING_STRATEGY_CREATION_TIMESTAMP

Ranked by document creation timestamps.

public static final int RANKING_STRATEGY_RELEVANCE_SCORE

Ranked by document relevance score.

public static final int RANKING_STRATEGY_USAGE_COUNT

Ranked by number of usages, as reported by the app.

public static final int RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP

Ranked by timestamp of last usage, as reported by the app.

public static final int RANKING_STRATEGY_SYSTEM_USAGE_COUNT

Ranked by number of usages from a system UI surface.

public static final int RANKING_STRATEGY_SYSTEM_USAGE_LAST_USED_TIMESTAMP

Ranked by timestamp of last usage from a system UI surface.

public static final int ORDER_DESCENDING

Search results will be returned in a descending order.

public static final int ORDER_ASCENDING

Search results will be returned in an ascending order.

public static final int GROUPING_TYPE_PER_PACKAGE

Results should be grouped together by package for the purpose of enforcing a limit on the number of results returned per package.

public static final int GROUPING_TYPE_PER_NAMESPACE

Results should be grouped together by namespace for the purpose of enforcing a limit on the number of results returned per namespace.

Constructors

public SearchSpec(Bundle bundle)

Methods

public Bundle getBundle()

Returns the populated by this builder.

public int getTermMatch()

Returns how the query terms should match terms in the index.

public java.util.List<java.lang.String> getFilterSchemas()

Returns the list of schema types to search for.

If empty, the query will search over all schema types.

public java.util.List<java.lang.String> getFilterNamespaces()

Returns the list of namespaces to search over.

If empty, the query will search over all namespaces.

public java.util.List<java.lang.String> getFilterPackageNames()

Returns the list of package name filters to search over.

If empty, the query will search over all packages that the caller has access to. If package names are specified which caller doesn't have access to, then those package names will be ignored.

public int getResultCountPerPage()

Returns the number of results per page in the result set.

public int getRankingStrategy()

Returns the ranking strategy.

public int getOrder()

Returns the order of returned search results (descending or ascending).

public int getSnippetCount()

Returns how many documents to generate snippets for.

public int getSnippetCountPerProperty()

Returns how many matches for each property of a matching document to generate snippets for.

public int getMaxSnippetSize()

Returns the maximum size of a snippet in characters.

public java.util.Map<java.lang.String, java.util.List> getProjections()

Returns a map from schema type to property paths to be used for projection.

If the map is empty, then all properties will be retrieved for all results.

Calling this function repeatedly is inefficient. Prefer to retain the Map returned by this function, rather than calling it multiple times.

public int getResultGroupingTypeFlags()

Get the type of grouping limit to apply, or 0 if SearchSpec.Builder.setResultGrouping(int, int) was not called.

public int getResultGroupingLimit()

Get the maximum number of results to return for each group.

Returns:

the maximum number of results to return for each group or Integer.MAX_VALUE if SearchSpec.Builder.setResultGrouping(int, int) was not called.

Source

/*
 * 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 androidx.appsearch.app;

import android.annotation.SuppressLint;
import android.os.Bundle;

import androidx.annotation.IntDef;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.appsearch.annotation.Document;
import androidx.appsearch.exceptions.AppSearchException;
import androidx.appsearch.util.BundleUtil;
import androidx.collection.ArrayMap;
import androidx.core.util.Preconditions;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * This class represents the specification logic for AppSearch. It can be used to set the type of
 * search, like prefix or exact only or apply filters to search for a specific schema type only etc.
 */
// TODO(sidchhabra) : AddResultSpec fields for Snippets etc.
public final class SearchSpec {
    /**
     * Schema type to be used in {@link SearchSpec.Builder#addProjection} to apply
     * property paths to all results, excepting any types that have had their own, specific
     * property paths set.
     */
    public static final String PROJECTION_SCHEMA_TYPE_WILDCARD = "*";

    static final String TERM_MATCH_TYPE_FIELD = "termMatchType";
    static final String SCHEMA_FIELD = "schema";
    static final String NAMESPACE_FIELD = "namespace";
    static final String PACKAGE_NAME_FIELD = "packageName";
    static final String NUM_PER_PAGE_FIELD = "numPerPage";
    static final String RANKING_STRATEGY_FIELD = "rankingStrategy";
    static final String ORDER_FIELD = "order";
    static final String SNIPPET_COUNT_FIELD = "snippetCount";
    static final String SNIPPET_COUNT_PER_PROPERTY_FIELD = "snippetCountPerProperty";
    static final String MAX_SNIPPET_FIELD = "maxSnippet";
    static final String PROJECTION_TYPE_PROPERTY_PATHS_FIELD = "projectionTypeFieldMasks";
    static final String RESULT_GROUPING_TYPE_FLAGS = "resultGroupingTypeFlags";
    static final String RESULT_GROUPING_LIMIT = "resultGroupingLimit";

    /** @hide */
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    public static final int DEFAULT_NUM_PER_PAGE = 10;

    // TODO(b/170371356): In framework, we may want these limits to be flag controlled.
    //  If that happens, the @IntRange() directives in this class may have to change.
    private static final int MAX_NUM_PER_PAGE = 10_000;
    private static final int MAX_SNIPPET_COUNT = 10_000;
    private static final int MAX_SNIPPET_PER_PROPERTY_COUNT = 10_000;
    private static final int MAX_SNIPPET_SIZE_LIMIT = 10_000;

    /**
     * Term Match Type for the query.
     *
     * @hide
     */
    // NOTE: The integer values of these constants must match the proto enum constants in
    // {@link com.google.android.icing.proto.SearchSpecProto.termMatchType}
    @IntDef(value = {
            TERM_MATCH_EXACT_ONLY,
            TERM_MATCH_PREFIX
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface TermMatch {
    }

    /**
     * Query terms will only match exact tokens in the index.
     * <p>Ex. A query term "foo" will only match indexed token "foo", and not "foot" or "football".
     */
    public static final int TERM_MATCH_EXACT_ONLY = 1;
    /**
     * Query terms will match indexed tokens when the query term is a prefix of the token.
     * <p>Ex. A query term "foo" will match indexed tokens like "foo", "foot", and "football".
     */
    public static final int TERM_MATCH_PREFIX = 2;

    /**
     * Ranking Strategy for query result.
     *
     * @hide
     */
    // NOTE: The integer values of these constants must match the proto enum constants in
    // {@link ScoringSpecProto.RankingStrategy.Code}
    @IntDef(value = {
            RANKING_STRATEGY_NONE,
            RANKING_STRATEGY_DOCUMENT_SCORE,
            RANKING_STRATEGY_CREATION_TIMESTAMP,
            RANKING_STRATEGY_RELEVANCE_SCORE,
            RANKING_STRATEGY_USAGE_COUNT,
            RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP,
            RANKING_STRATEGY_SYSTEM_USAGE_COUNT,
            RANKING_STRATEGY_SYSTEM_USAGE_LAST_USED_TIMESTAMP,
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface RankingStrategy {
    }

    /** No Ranking, results are returned in arbitrary order. */
    public static final int RANKING_STRATEGY_NONE = 0;
    /** Ranked by app-provided document scores. */
    public static final int RANKING_STRATEGY_DOCUMENT_SCORE = 1;
    /** Ranked by document creation timestamps. */
    public static final int RANKING_STRATEGY_CREATION_TIMESTAMP = 2;
    /** Ranked by document relevance score. */
    public static final int RANKING_STRATEGY_RELEVANCE_SCORE = 3;
    /** Ranked by number of usages, as reported by the app. */
    public static final int RANKING_STRATEGY_USAGE_COUNT = 4;
    /** Ranked by timestamp of last usage, as reported by the app. */
    public static final int RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP = 5;
    /** Ranked by number of usages from a system UI surface. */
    public static final int RANKING_STRATEGY_SYSTEM_USAGE_COUNT = 6;
    /** Ranked by timestamp of last usage from a system UI surface. */
    public static final int RANKING_STRATEGY_SYSTEM_USAGE_LAST_USED_TIMESTAMP = 7;

    /**
     * Order for query result.
     *
     * @hide
     */
    // NOTE: The integer values of these constants must match the proto enum constants in
    // {@link ScoringSpecProto.Order.Code}
    @IntDef(value = {
            ORDER_DESCENDING,
            ORDER_ASCENDING
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface Order {
    }

    /** Search results will be returned in a descending order. */
    public static final int ORDER_DESCENDING = 0;
    /** Search results will be returned in an ascending order. */
    public static final int ORDER_ASCENDING = 1;

    /**
     * Grouping type for result limits.
     *
     * @hide
     */
    @IntDef(flag = true, value = {
            GROUPING_TYPE_PER_PACKAGE,
            GROUPING_TYPE_PER_NAMESPACE
    })
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    @Retention(RetentionPolicy.SOURCE)
    public @interface GroupingType {
    }

    /**
     * Results should be grouped together by package for the purpose of enforcing a limit on the
     * number of results returned per package.
     */
    public static final int GROUPING_TYPE_PER_PACKAGE = 0b01;
    /**
     * Results should be grouped together by namespace for the purpose of enforcing a limit on the
     * number of results returned per namespace.
     */
    public static final int GROUPING_TYPE_PER_NAMESPACE = 0b10;

    private final Bundle mBundle;

    /** @hide */
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    public SearchSpec(@NonNull Bundle bundle) {
        Preconditions.checkNotNull(bundle);
        mBundle = bundle;
    }

    /**
     * Returns the {@link Bundle} populated by this builder.
     *
     * @hide
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    @NonNull
    public Bundle getBundle() {
        return mBundle;
    }

    /** Returns how the query terms should match terms in the index. */
    public @TermMatch int getTermMatch() {
        return mBundle.getInt(TERM_MATCH_TYPE_FIELD, -1);
    }

    /**
     * Returns the list of schema types to search for.
     *
     * <p>If empty, the query will search over all schema types.
     */
    @NonNull
    public List<String> getFilterSchemas() {
        List<String> schemas = mBundle.getStringArrayList(SCHEMA_FIELD);
        if (schemas == null) {
            return Collections.emptyList();
        }
        return Collections.unmodifiableList(schemas);
    }

    /**
     * Returns the list of namespaces to search over.
     *
     * <p>If empty, the query will search over all namespaces.
     */
    @NonNull
    public List<String> getFilterNamespaces() {
        List<String> namespaces = mBundle.getStringArrayList(NAMESPACE_FIELD);
        if (namespaces == null) {
            return Collections.emptyList();
        }
        return Collections.unmodifiableList(namespaces);
    }

    /**
     * Returns the list of package name filters to search over.
     *
     * <p>If empty, the query will search over all packages that the caller has access to. If
     * package names are specified which caller doesn't have access to, then those package names
     * will be ignored.
     */
    @NonNull
    public List<String> getFilterPackageNames() {
        List<String> packageNames = mBundle.getStringArrayList(PACKAGE_NAME_FIELD);
        if (packageNames == null) {
            return Collections.emptyList();
        }
        return Collections.unmodifiableList(packageNames);
    }

    /** Returns the number of results per page in the result set. */
    public int getResultCountPerPage() {
        return mBundle.getInt(NUM_PER_PAGE_FIELD, DEFAULT_NUM_PER_PAGE);
    }

    /** Returns the ranking strategy. */
    public @RankingStrategy int getRankingStrategy() {
        return mBundle.getInt(RANKING_STRATEGY_FIELD);
    }

    /** Returns the order of returned search results (descending or ascending). */
    public @Order int getOrder() {
        return mBundle.getInt(ORDER_FIELD);
    }

    /** Returns how many documents to generate snippets for. */
    public int getSnippetCount() {
        return mBundle.getInt(SNIPPET_COUNT_FIELD);
    }

    /**
     * Returns how many matches for each property of a matching document to generate snippets for.
     */
    public int getSnippetCountPerProperty() {
        return mBundle.getInt(SNIPPET_COUNT_PER_PROPERTY_FIELD);
    }

    /** Returns the maximum size of a snippet in characters. */
    public int getMaxSnippetSize() {
        return mBundle.getInt(MAX_SNIPPET_FIELD);
    }

    /**
     * Returns a map from schema type to property paths to be used for projection.
     *
     * <p>If the map is empty, then all properties will be retrieved for all results.
     *
     * <p>Calling this function repeatedly is inefficient. Prefer to retain the Map returned
     * by this function, rather than calling it multiple times.
     */
    @NonNull
    public Map<String, List<String>> getProjections() {
        Bundle typePropertyPathsBundle = mBundle.getBundle(PROJECTION_TYPE_PROPERTY_PATHS_FIELD);
        Set<String> schemas = typePropertyPathsBundle.keySet();
        Map<String, List<String>> typePropertyPathsMap = new ArrayMap<>(schemas.size());
        for (String schema : schemas) {
            typePropertyPathsMap.put(schema, typePropertyPathsBundle.getStringArrayList(schema));
        }
        return typePropertyPathsMap;
    }

    /**
     * Get the type of grouping limit to apply, or 0 if {@link Builder#setResultGrouping} was not
     * called.
     */
    public @GroupingType int getResultGroupingTypeFlags() {
        return mBundle.getInt(RESULT_GROUPING_TYPE_FLAGS);
    }

    /**
     * Get the maximum number of results to return for each group.
     *
     * @return the maximum number of results to return for each group or Integer.MAX_VALUE if
     * {@link Builder#setResultGrouping(int, int)} was not called.
     */
    public int getResultGroupingLimit() {
        return mBundle.getInt(RESULT_GROUPING_LIMIT, Integer.MAX_VALUE);
    }

    /** Builder for {@link SearchSpec objects}. */
    public static final class Builder {
        private ArrayList<String> mSchemas = new ArrayList<>();
        private ArrayList<String> mNamespaces = new ArrayList<>();
        private ArrayList<String> mPackageNames = new ArrayList<>();
        private Bundle mProjectionTypePropertyMasks = new Bundle();

        private int mResultCountPerPage = DEFAULT_NUM_PER_PAGE;
        private @TermMatch int mTermMatchType = TERM_MATCH_PREFIX;
        private int mSnippetCount = 0;
        private int mSnippetCountPerProperty = MAX_SNIPPET_PER_PROPERTY_COUNT;
        private int mMaxSnippetSize = 0;
        private @RankingStrategy int mRankingStrategy = RANKING_STRATEGY_NONE;
        private @Order int mOrder = ORDER_DESCENDING;
        private @GroupingType int mGroupingTypeFlags = 0;
        private int mGroupingLimit = 0;
        private boolean mBuilt = false;

        /**
         * Indicates how the query terms should match {@code TermMatchCode} in the index.
         *
         * <p>If this method is not called, the default term match type is
         * {@link SearchSpec#TERM_MATCH_PREFIX}.
         */
        @NonNull
        public Builder setTermMatch(@TermMatch int termMatchType) {
            Preconditions.checkArgumentInRange(termMatchType, TERM_MATCH_EXACT_ONLY,
                    TERM_MATCH_PREFIX, "Term match type");
            resetIfBuilt();
            mTermMatchType = termMatchType;
            return this;
        }

        /**
         * Adds a Schema type filter to {@link SearchSpec} Entry. Only search for documents that
         * have the specified schema types.
         *
         * <p>If unset, the query will search over all schema types.
         */
        @NonNull
        public Builder addFilterSchemas(@NonNull String... schemas) {
            Preconditions.checkNotNull(schemas);
            resetIfBuilt();
            return addFilterSchemas(Arrays.asList(schemas));
        }

        /**
         * Adds a Schema type filter to {@link SearchSpec} Entry. Only search for documents that
         * have the specified schema types.
         *
         * <p>If unset, the query will search over all schema types.
         */
        @NonNull
        public Builder addFilterSchemas(@NonNull Collection<String> schemas) {
            Preconditions.checkNotNull(schemas);
            resetIfBuilt();
            mSchemas.addAll(schemas);
            return this;
        }

// @exportToFramework:startStrip()

        /**
         * Adds the Schema names of given document classes to the Schema type filter of
         * {@link SearchSpec} Entry. Only search for documents that have the specified schema types.
         *
         * <p>If unset, the query will search over all schema types.
         *
         * @param documentClasses classes annotated with {@link Document}.
         */
        // Merged list available from getFilterSchemas
        @SuppressLint("MissingGetterMatchingBuilder")
        @NonNull
        public Builder addFilterDocumentClasses(
                @NonNull Collection<? extends Class<?>> documentClasses) throws AppSearchException {
            Preconditions.checkNotNull(documentClasses);
            resetIfBuilt();
            List<String> schemas = new ArrayList<>(documentClasses.size());
            DocumentClassFactoryRegistry registry = DocumentClassFactoryRegistry.getInstance();
            for (Class<?> documentClass : documentClasses) {
                DocumentClassFactory<?> factory = registry.getOrCreateFactory(documentClass);
                schemas.add(factory.getSchemaName());
            }
            addFilterSchemas(schemas);
            return this;
        }
// @exportToFramework:endStrip()

// @exportToFramework:startStrip()

        /**
         * Adds the Schema names of given document classes to the Schema type filter of
         * {@link SearchSpec} Entry. Only search for documents that have the specified schema types.
         *
         * <p>If unset, the query will search over all schema types.
         *
         * @param documentClasses classes annotated with {@link Document}.
         */
        // Merged list available from getFilterSchemas()
        @SuppressLint("MissingGetterMatchingBuilder")
        @NonNull
        public Builder addFilterDocumentClasses(@NonNull Class<?>... documentClasses)
                throws AppSearchException {
            Preconditions.checkNotNull(documentClasses);
            resetIfBuilt();
            return addFilterDocumentClasses(Arrays.asList(documentClasses));
        }
// @exportToFramework:endStrip()

        /**
         * Adds a namespace filter to {@link SearchSpec} Entry. Only search for documents that
         * have the specified namespaces.
         * <p>If unset, the query will search over all namespaces.
         */
        @NonNull
        public Builder addFilterNamespaces(@NonNull String... namespaces) {
            Preconditions.checkNotNull(namespaces);
            resetIfBuilt();
            return addFilterNamespaces(Arrays.asList(namespaces));
        }

        /**
         * Adds a namespace filter to {@link SearchSpec} Entry. Only search for documents that
         * have the specified namespaces.
         * <p>If unset, the query will search over all namespaces.
         */
        @NonNull
        public Builder addFilterNamespaces(@NonNull Collection<String> namespaces) {
            Preconditions.checkNotNull(namespaces);
            resetIfBuilt();
            mNamespaces.addAll(namespaces);
            return this;
        }

        /**
         * Adds a package name filter to {@link SearchSpec} Entry. Only search for documents that
         * were indexed from the specified packages.
         *
         * <p>If unset, the query will search over all packages that the caller has access to.
         * If package names are specified which caller doesn't have access to, then those package
         * names will be ignored.
         */
        @NonNull
        public Builder addFilterPackageNames(@NonNull String... packageNames) {
            Preconditions.checkNotNull(packageNames);
            resetIfBuilt();
            return addFilterPackageNames(Arrays.asList(packageNames));
        }

        /**
         * Adds a package name filter to {@link SearchSpec} Entry. Only search for documents that
         * were indexed from the specified packages.
         *
         * <p>If unset, the query will search over all packages that the caller has access to.
         * If package names are specified which caller doesn't have access to, then those package
         * names will be ignored.
         */
        @NonNull
        public Builder addFilterPackageNames(@NonNull Collection<String> packageNames) {
            Preconditions.checkNotNull(packageNames);
            resetIfBuilt();
            mPackageNames.addAll(packageNames);
            return this;
        }

        /**
         * Sets the number of results per page in the returned object.
         *
         * <p>The default number of results per page is 10.
         */
        @NonNull
        public SearchSpec.Builder setResultCountPerPage(
                @IntRange(from = 0, to = MAX_NUM_PER_PAGE) int resultCountPerPage) {
            Preconditions.checkArgumentInRange(
                    resultCountPerPage, 0, MAX_NUM_PER_PAGE, "resultCountPerPage");
            resetIfBuilt();
            mResultCountPerPage = resultCountPerPage;
            return this;
        }

        /** Sets ranking strategy for AppSearch results. */
        @NonNull
        public Builder setRankingStrategy(@RankingStrategy int rankingStrategy) {
            Preconditions.checkArgumentInRange(rankingStrategy, RANKING_STRATEGY_NONE,
                    RANKING_STRATEGY_SYSTEM_USAGE_LAST_USED_TIMESTAMP, "Result ranking strategy");
            resetIfBuilt();
            mRankingStrategy = rankingStrategy;
            return this;
        }

        /**
         * Indicates the order of returned search results, the default is
         * {@link #ORDER_DESCENDING}, meaning that results with higher scores come first.
         *
         * <p>This order field will be ignored if RankingStrategy = {@code RANKING_STRATEGY_NONE}.
         */
        @NonNull
        public Builder setOrder(@Order int order) {
            Preconditions.checkArgumentInRange(order, ORDER_DESCENDING, ORDER_ASCENDING,
                    "Result ranking order");
            resetIfBuilt();
            mOrder = order;
            return this;
        }

        /**
         * Only the first {@code snippetCount} documents based on the ranking strategy
         * will have snippet information provided.
         *
         * <p>The list returned from {@link SearchResult#getMatchInfos} will contain at most this
         * many entries.
         *
         * <p>If set to 0 (default), snippeting is disabled and the list returned from
         * {@link SearchResult#getMatchInfos} will be empty.
         */
        @NonNull
        public SearchSpec.Builder setSnippetCount(
                @IntRange(from = 0, to = MAX_SNIPPET_COUNT) int snippetCount) {
            Preconditions.checkArgumentInRange(snippetCount, 0, MAX_SNIPPET_COUNT, "snippetCount");
            resetIfBuilt();
            mSnippetCount = snippetCount;
            return this;
        }

        /**
         * Sets {@code snippetCountPerProperty}. Only the first {@code snippetCountPerProperty}
         * snippets for each property of each {@link GenericDocument} will contain snippet
         * information.
         *
         * <p>If set to 0, snippeting is disabled and the list
         * returned from {@link SearchResult#getMatchInfos} will be empty.
         *
         * <p>The default behavior is to snippet all matches a property contains, up to the maximum
         * value of 10,000.
         */
        @NonNull
        public SearchSpec.Builder setSnippetCountPerProperty(
                @IntRange(from = 0, to = MAX_SNIPPET_PER_PROPERTY_COUNT)
                        int snippetCountPerProperty) {
            Preconditions.checkArgumentInRange(snippetCountPerProperty,
                    0, MAX_SNIPPET_PER_PROPERTY_COUNT, "snippetCountPerProperty");
            resetIfBuilt();
            mSnippetCountPerProperty = snippetCountPerProperty;
            return this;
        }

        /**
         * Sets {@code maxSnippetSize}, the maximum snippet size. Snippet windows start at
         * {@code maxSnippetSize/2} bytes before the middle of the matching token and end at
         * {@code maxSnippetSize/2} bytes after the middle of the matching token. It respects
         * token boundaries, therefore the returned window may be smaller than requested.
         *
         * <p> Setting {@code maxSnippetSize} to 0 will disable windowing and an empty string will
         * be returned. If matches enabled is also set to false, then snippeting is disabled.
         *
         * <p>Ex. {@code maxSnippetSize} = 16. "foo bar baz bat rat" with a query of "baz" will
         * return a window of "bar baz bat" which is only 11 bytes long.
         */
        @NonNull
        public SearchSpec.Builder setMaxSnippetSize(
                @IntRange(from = 0, to = MAX_SNIPPET_SIZE_LIMIT) int maxSnippetSize) {
            Preconditions.checkArgumentInRange(
                    maxSnippetSize, 0, MAX_SNIPPET_SIZE_LIMIT, "maxSnippetSize");
            resetIfBuilt();
            mMaxSnippetSize = maxSnippetSize;
            return this;
        }

        /**
         * Adds property paths for the specified type to be used for projection. If property
         * paths are added for a type, then only the properties referred to will be retrieved for
         * results of that type. If a property path that is specified isn't present in a result,
         * it will be ignored for that result. Property paths cannot be null.
         *
         * <p>If no property paths are added for a particular type, then all properties of
         * results of that type will be retrieved.
         *
         * <p>If property path is added for the
         * {@link SearchSpec#PROJECTION_SCHEMA_TYPE_WILDCARD}, then those property paths will
         * apply to all results, excepting any types that have their own, specific property paths
         * set.
         *
         * <p>Suppose the following document is in the index.
         * <pre>{@code
         * Email: Document {
         *   sender: Document {
         *     name: "Mr. Person"
         *     email: "mrperson123@google.com"
         *   }
         *   recipients: [
         *     Document {
         *       name: "John Doe"
         *       email: "johndoe123@google.com"
         *     }
         *     Document {
         *       name: "Jane Doe"
         *       email: "janedoe123@google.com"
         *     }
         *   ]
         *   subject: "IMPORTANT"
         *   body: "Limited time offer!"
         * }
         * }</pre>
         *
         * <p>Then, suppose that a query for "important" is issued with the following projection
         * type property paths:
         * <pre>{@code
         * {schema: "Email", ["subject", "sender.name", "recipients.name"]}
         * }</pre>
         *
         * <p>The above document will be returned as:
         * <pre>{@code
         * Email: Document {
         *   sender: Document {
         *     name: "Mr. Body"
         *   }
         *   recipients: [
         *     Document {
         *       name: "John Doe"
         *     }
         *     Document {
         *       name: "Jane Doe"
         *     }
         *   ]
         *   subject: "IMPORTANT"
         * }
         * }</pre>
         */
        @NonNull
        public SearchSpec.Builder addProjection(
                @NonNull String schema, @NonNull Collection<String> propertyPaths) {
            Preconditions.checkNotNull(schema);
            Preconditions.checkNotNull(propertyPaths);
            resetIfBuilt();
            ArrayList<String> propertyPathsArrayList = new ArrayList<>(propertyPaths.size());
            for (String propertyPath : propertyPaths) {
                Preconditions.checkNotNull(propertyPath);
                propertyPathsArrayList.add(propertyPath);
            }
            mProjectionTypePropertyMasks.putStringArrayList(schema, propertyPathsArrayList);
            return this;
        }

        /**
         * Set the maximum number of results to return for each group, where groups are defined
         * by grouping type.
         *
         * <p>Calling this method will override any previous calls. So calling
         * setResultGrouping(GROUPING_TYPE_PER_PACKAGE, 7) and then calling
         * setResultGrouping(GROUPING_TYPE_PER_PACKAGE, 2) will result in only the latter, a
         * limit of two results per package, being applied. Or calling setResultGrouping
         * (GROUPING_TYPE_PER_PACKAGE, 1) and then calling setResultGrouping
         * (GROUPING_TYPE_PER_PACKAGE | GROUPING_PER_NAMESPACE, 5) will result in five results
         * per package per namespace.
         *
         * @param groupingTypeFlags One or more combination of grouping types.
         * @param limit             Number of results to return per {@code groupingTypeFlags}.
         * @throws IllegalArgumentException if groupingTypeFlags is zero.
         */
        // Individual parameters available from getResultGroupingTypeFlags and
        // getResultGroupingLimit
        @SuppressLint("MissingGetterMatchingBuilder")
        @NonNull
        public Builder setResultGrouping(@GroupingType int groupingTypeFlags, int limit) {
            Preconditions.checkState(
                    groupingTypeFlags != 0, "Result grouping type cannot be zero.");
            resetIfBuilt();
            mGroupingTypeFlags = groupingTypeFlags;
            mGroupingLimit = limit;
            return this;
        }

        /** Constructs a new {@link SearchSpec} from the contents of this builder. */
        @NonNull
        public SearchSpec build() {
            Bundle bundle = new Bundle();
            bundle.putStringArrayList(SCHEMA_FIELD, mSchemas);
            bundle.putStringArrayList(NAMESPACE_FIELD, mNamespaces);
            bundle.putStringArrayList(PACKAGE_NAME_FIELD, mPackageNames);
            bundle.putBundle(PROJECTION_TYPE_PROPERTY_PATHS_FIELD, mProjectionTypePropertyMasks);
            bundle.putInt(NUM_PER_PAGE_FIELD, mResultCountPerPage);
            bundle.putInt(TERM_MATCH_TYPE_FIELD, mTermMatchType);
            bundle.putInt(SNIPPET_COUNT_FIELD, mSnippetCount);
            bundle.putInt(SNIPPET_COUNT_PER_PROPERTY_FIELD, mSnippetCountPerProperty);
            bundle.putInt(MAX_SNIPPET_FIELD, mMaxSnippetSize);
            bundle.putInt(RANKING_STRATEGY_FIELD, mRankingStrategy);
            bundle.putInt(ORDER_FIELD, mOrder);
            bundle.putInt(RESULT_GROUPING_TYPE_FLAGS, mGroupingTypeFlags);
            bundle.putInt(RESULT_GROUPING_LIMIT, mGroupingLimit);
            mBuilt = true;
            return new SearchSpec(bundle);
        }

        private void resetIfBuilt() {
            if (mBuilt) {
                mSchemas = new ArrayList<>(mSchemas);
                mNamespaces = new ArrayList<>(mNamespaces);
                mPackageNames = new ArrayList<>(mPackageNames);
                mProjectionTypePropertyMasks = BundleUtil.deepCopy(mProjectionTypePropertyMasks);
                mBuilt = false;
            }
        }
    }
}