blob: 15acf103f2e6b92a57d47aa2ae2e0412419d5f41 [file] [log] [blame]
/*
* 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 android.annotation.SuppressLint;
import android.os.Bundle;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.app.appsearch.exceptions.AppSearchException;
import android.app.appsearch.exceptions.IllegalSearchSpecException;
import com.android.internal.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;
/**
* 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.
* @hide
*/
// TODO(sidchhabra) : AddResultSpec fields for Snippets etc.
public final class SearchSpec {
static final String TERM_MATCH_TYPE_FIELD = "termMatchType";
static final String SCHEMA_TYPE_FIELD = "schemaType";
static final String NAMESPACE_FIELD = "namespace";
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";
/** @hide */
public static final int DEFAULT_NUM_PER_PAGE = 10;
// TODO(b/170371356): In framework, we may want these limits might be flag controlled.
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
})
@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;
/**
* 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;
private final Bundle mBundle;
/** @hide */
public SearchSpec(@NonNull Bundle bundle) {
Preconditions.checkNotNull(bundle);
mBundle = bundle;
}
/**
* Returns the {@link Bundle} populated by this builder.
* @hide
*/
@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> getSchemas() {
List<String> schemas = mBundle.getStringArrayList(SCHEMA_TYPE_FIELD);
if (schemas == null) {
return Collections.emptyList();
}
return Collections.unmodifiableList(schemas);
}
/**
* Returns the list of namespaces to search for.
*
* <p>If empty, the query will search over all namespaces.
*/
@NonNull
public List<String> getNamespaces() {
List<String> namespaces = mBundle.getStringArrayList(NAMESPACE_FIELD);
if (namespaces == null) {
return Collections.emptyList();
}
return Collections.unmodifiableList(namespaces);
}
/** Returns the number of results per page in the returned object. */
public int getNumPerPage() {
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);
}
/** Builder for {@link SearchSpec objects}. */
public static final class Builder {
private final Bundle mBundle;
private final ArrayList<String> mSchemaTypes = new ArrayList<>();
private final ArrayList<String> mNamespaces = new ArrayList<>();
private boolean mBuilt = false;
/** Creates a new {@link SearchSpec.Builder}. */
public Builder() {
mBundle = new Bundle();
mBundle.putInt(NUM_PER_PAGE_FIELD, DEFAULT_NUM_PER_PAGE);
}
/**
* Indicates how the query terms should match {@code TermMatchCode} in the index.
*/
@NonNull
public Builder setTermMatch(@TermMatch int termMatchTypeCode) {
Preconditions.checkState(!mBuilt, "Builder has already been used");
Preconditions.checkArgumentInRange(termMatchTypeCode, TERM_MATCH_EXACT_ONLY,
TERM_MATCH_PREFIX, "Term match type");
mBundle.putInt(TERM_MATCH_TYPE_FIELD, termMatchTypeCode);
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 addSchema(@NonNull String... schemaTypes) {
Preconditions.checkNotNull(schemaTypes);
Preconditions.checkState(!mBuilt, "Builder has already been used");
return addSchema(Arrays.asList(schemaTypes));
}
/**
* 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 addSchema(@NonNull Collection<String> schemaTypes) {
Preconditions.checkNotNull(schemaTypes);
Preconditions.checkState(!mBuilt, "Builder has already been used");
mSchemaTypes.addAll(schemaTypes);
return this;
}
/**
* 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 addNamespace(@NonNull String... namespaces) {
Preconditions.checkNotNull(namespaces);
Preconditions.checkState(!mBuilt, "Builder has already been used");
return addNamespace(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 addNamespace(@NonNull Collection<String> namespaces) {
Preconditions.checkNotNull(namespaces);
Preconditions.checkState(!mBuilt, "Builder has already been used");
mNamespaces.addAll(namespaces);
return this;
}
/**
* Sets the number of results per page in the returned object.
* <p> The default number of results per page is 10. And should be set in range [0, 10k].
*/
@NonNull
public SearchSpec.Builder setNumPerPage(int numPerPage) {
Preconditions.checkState(!mBuilt, "Builder has already been used");
Preconditions.checkArgumentInRange(numPerPage, 0, MAX_NUM_PER_PAGE, "NumPerPage");
mBundle.putInt(NUM_PER_PAGE_FIELD, numPerPage);
return this;
}
/** Sets ranking strategy for AppSearch results.*/
@NonNull
public Builder setRankingStrategy(@RankingStrategy int rankingStrategy) {
Preconditions.checkState(!mBuilt, "Builder has already been used");
Preconditions.checkArgumentInRange(rankingStrategy, RANKING_STRATEGY_NONE,
RANKING_STRATEGY_CREATION_TIMESTAMP, "Result ranking strategy");
mBundle.putInt(RANKING_STRATEGY_FIELD, rankingStrategy);
return this;
}
/**
* Indicates the order of returned search results, the default is DESC, 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.checkState(!mBuilt, "Builder has already been used");
Preconditions.checkArgumentInRange(order, ORDER_DESCENDING, ORDER_ASCENDING,
"Result ranking order");
mBundle.putInt(ORDER_FIELD, order);
return this;
}
/**
* Only the first {@code snippetCount} documents based on the ranking strategy
* will have snippet information provided.
*
* <p>If set to 0 (default), snippeting is disabled and {@link SearchResult#getMatches} will
* return {@code null} for that result.
*
* <p>The value should be set in range[0, 10k].
*/
@NonNull
public SearchSpec.Builder setSnippetCount(int snippetCount) {
Preconditions.checkState(!mBuilt, "Builder has already been used");
Preconditions.checkArgumentInRange(snippetCount, 0, MAX_SNIPPET_COUNT, "snippetCount");
mBundle.putInt(SNIPPET_COUNT_FIELD, snippetCount);
return this;
}
/**
* Sets {@code snippetCountPerProperty}. Only the first {@code snippetCountPerProperty}
* snippets for a every property of {@link GenericDocument} will contain snippet
* information.
*
* <p>If set to 0, snippeting is disabled and {@link SearchResult#getMatches}
* will return {@code null} for that result.
*
* <p>The value should be set in range[0, 10k].
*/
@NonNull
public SearchSpec.Builder setSnippetCountPerProperty(int snippetCountPerProperty) {
Preconditions.checkState(!mBuilt, "Builder has already been used");
Preconditions.checkArgumentInRange(snippetCountPerProperty,
0, MAX_SNIPPET_PER_PROPERTY_COUNT, "snippetCountPerProperty");
mBundle.putInt(SNIPPET_COUNT_PER_PROPERTY_FIELD, 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.
*
* <p>The value should be in range[0, 10k].
*/
@NonNull
public SearchSpec.Builder setMaxSnippetSize(int maxSnippetSize) {
Preconditions.checkState(!mBuilt, "Builder has already been used");
Preconditions.checkArgumentInRange(
maxSnippetSize, 0, MAX_SNIPPET_SIZE_LIMIT, "maxSnippetSize");
mBundle.putInt(MAX_SNIPPET_FIELD, maxSnippetSize);
return this;
}
/**
* Constructs a new {@link SearchSpec} from the contents of this builder.
*
* <p>After calling this method, the builder must no longer be used.
*/
@NonNull
public SearchSpec build() {
Preconditions.checkState(!mBuilt, "Builder has already been used");
if (!mBundle.containsKey(TERM_MATCH_TYPE_FIELD)) {
throw new IllegalSearchSpecException("Missing termMatchType field.");
}
mBundle.putStringArrayList(NAMESPACE_FIELD, mNamespaces);
mBundle.putStringArrayList(SCHEMA_TYPE_FIELD, mSchemaTypes);
mBuilt = true;
return new SearchSpec(mBundle);
}
}
}