Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 16e5fefb authored by sidchhabra's avatar sidchhabra
Browse files

Added query and SearchResults.

Bug: 145631811
Test: atest core/tests/coretests/src/android/app/appsearch/SearchResultsTest.java
Change-Id: Ie1bb0028b6ff999401cb96f9b9c4b121c9d0e448
parent 5a4ed4cc
Loading
Loading
Loading
Loading
+94 −1
Original line number Diff line number Diff line
/*
 * Copyright (C) 2019 The Android Open Source Project
 * Copyright (C) 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.
@@ -25,9 +25,12 @@ import android.os.RemoteException;
import com.android.internal.infra.AndroidFuture;

import com.google.android.icing.proto.SchemaProto;
import com.google.android.icing.proto.SearchResultProto;
import com.google.android.icing.protobuf.InvalidProtocolBufferException;

import java.util.List;
import java.util.concurrent.Executor;
import java.util.function.BiConsumer;
import java.util.function.Consumer;

/**
@@ -127,4 +130,94 @@ public class AppSearchManager {
        // TODO(b/147614371) Fix error report for multiple documents.
        future.whenCompleteAsync((noop, err) -> callback.accept(err), executor);
    }

    /**
     * This method searches for documents based on a given query string. It also accepts
     * specifications regarding how to search and format the results.
     *
     *<p>Currently we support following features in the raw query format:
     * <ul>
     *     <li>AND
     *     AND joins (e.g. “match documents that have both the terms ‘dog’ and
     *     ‘cat’”).
     *     Example: hello world matches documents that have both ‘hello’ and ‘world’
     *     <li>OR
     *     OR joins (e.g. “match documents that have either the term ‘dog’ or
     *     ‘cat’”).
     *     Example: dog OR puppy
     *     <li>Exclusion
     *     Exclude a term (e.g. “match documents that do
     *     not have the term ‘dog’”).
     *     Example: -dog excludes the term ‘dog’
     *     <li>Grouping terms
     *     Allow for conceptual grouping of subqueries to enable hierarchical structures (e.g.
     *     “match documents that have either ‘dog’ or ‘puppy’, and either ‘cat’ or ‘kitten’”).
     *     Example: (dog puppy) (cat kitten) two one group containing two terms.
     *     <li>Property restricts
     *      which properties of a document to specifically match terms in (e.g.
     *     “match documents where the ‘subject’ property contains ‘important’”).
     *     Example: subject:important matches documents with the term ‘important’ in the
     *     ‘subject’ property
     *     <li>Schema type restricts
     *     This is similar to property restricts, but allows for restricts on top-level document
     *     fields, such as schema_type. Clients should be able to limit their query to documents of
     *     a certain schema_type (e.g. “match documents that are of the ‘Email’ schema_type”).
     *     Example: { schema_type_filters: “Email”, “Video”,query: “dog” } will match documents
     *     that contain the query term ‘dog’ and are of either the ‘Email’ schema type or the
     *     ‘Video’ schema type.
     * </ul>
     *
     * <p> It is strongly recommended to use Jetpack APIs.
     *
     * @param queryExpression Query String to search.
     * @param searchSpec Spec for setting filters, raw query etc.
     * @param executor Executor on which to invoke the callback.
     * @param callback  Callback to receive errors resulting from the query operation. If the
     *                 operation succeeds, the callback will be invoked with {@code null}.
     * @hide
     */
    @NonNull
    public void query(
            @NonNull String queryExpression,
            @NonNull SearchSpec searchSpec,
            @NonNull @CallbackExecutor Executor executor,
            @NonNull BiConsumer<? super SearchResults, ? super Throwable> callback) {
        AndroidFuture<byte[]> future = new AndroidFuture<>();
        future.whenCompleteAsync((searchResultBytes, err) -> {
            if (err != null) {
                callback.accept(null, err);
                return;
            }

            if (searchResultBytes != null) {
                SearchResultProto searchResultProto;
                try {
                    searchResultProto = SearchResultProto.parseFrom(searchResultBytes);
                } catch (InvalidProtocolBufferException e) {
                    callback.accept(null, e);
                    return;
                }
                if (searchResultProto.hasError()) {
                    // TODO(sidchhabra): Add better exception handling.
                    callback.accept(
                            null,
                            new RuntimeException(searchResultProto.getError().getErrorMessage()));
                    return;
                }
                SearchResults searchResults = new SearchResults(searchResultProto);
                callback.accept(searchResults, null);
                return;
            }

            // Nothing was supplied in the future at all
            callback.accept(
                    null, new IllegalStateException("Unknown failure occurred while querying"));
        }, executor);

        try {
            mService.query(queryExpression, searchSpec.getProto().toByteArray(), future);
        } catch (RemoteException e) {
            future.completeExceptionally(e);
        }
    }
}
+10 −1
Original line number Diff line number Diff line
/**
 * Copyright 2019, The Android Open Source Project
 * 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.
@@ -29,4 +29,13 @@ interface IAppSearchManager {
     */
    void setSchema(in byte[] schemaProto, in AndroidFuture callback);
    void put(in byte[] documentBytes, in AndroidFuture callback);
    /**
     * Searches a document based on a given query string.
     *
     * @param queryExpression Query String to search.
     * @param searchSpec Serialized SearchSpecProto.
     * @param callback {@link AndroidFuture}. Will be completed with a serialized
     *     {@link SearchResultsProto}, or completed exceptionally if query fails.
     */
     void query(in String queryExpression, in byte[] searchSpecBytes, in AndroidFuture callback);
}
+36 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 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.NonNull;

/**
 * Indicates that a {@link android.app.appsearch.SearchResults} has logical inconsistencies such
 * as unpopulated mandatory fields or illegal combinations of parameters.
 *
 * @hide
 */
public class IllegalSearchSpecException extends IllegalArgumentException {
    /**
     * Constructs a new {@link IllegalSearchSpecException}.
     *
     * @param message A developer-readable description of the issue with the bundle.
     */
    public IllegalSearchSpecException(@NonNull String message) {
        super(message);
    }
}
+89 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 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.NonNull;

import com.google.android.icing.proto.SearchResultProto;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
 * SearchResults are a list of results that are returned from a query. Each result from this
 * list contains a document and may contain other fields like snippets based on request.
 * @hide
 */
public final class SearchResults {

    private final SearchResultProto mSearchResultProto;

    /** @hide */
    public SearchResults(SearchResultProto searchResultProto) {
        mSearchResultProto = searchResultProto;
    }

    /**
     * This class represents the result obtained from the query. It will contain the document which
     * which matched the specified query string and specifications.
     * @hide
     */
    public static final class Result {
        private final SearchResultProto.ResultProto mResultProto;

        private Result(SearchResultProto.ResultProto resultProto) {
            mResultProto = resultProto;
        }

        /**
         * Contains the matching {@link AppSearch.Document}.
         * @return Document object which matched the query.
         * @hide
         */
        // TODO(sidchhabra): Switch to Document constructor that takes proto.
        @NonNull
        public AppSearch.Document getDocument() {
            return AppSearch.Document.newBuilder(mResultProto.getDocument().getUri(),
                    mResultProto.getDocument().getSchema())
                    .setCreationTimestampSecs(mResultProto.getDocument().getCreationTimestampSecs())
                    .setScore(mResultProto.getDocument().getScore())
                    .build();
        }

        // TODO(sidchhabra): Add Getter for ResultReader for Snippet.
    }

    @Override
    public String toString() {
        return mSearchResultProto.toString();
    }

    /**
     * Returns a {@link Result} iterator. Returns Empty Iterator if there are no matching results.
     * @hide
     */
    @NonNull
    public Iterator<Result> getResults() {
        List<Result> results = new ArrayList<>();
        // TODO(sidchhabra): Pass results using a RemoteStream.
        for (SearchResultProto.ResultProto resultProto : mSearchResultProto.getResultsList()) {
            results.add(new Result(resultProto));
        }
        return results.iterator();
    }
}
+127 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 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.IntDef;
import android.annotation.NonNull;

import com.google.android.icing.proto.SearchSpecProto;
import com.google.android.icing.proto.TermMatchType;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
 * 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 {

    private final SearchSpecProto mSearchSpecProto;

    private SearchSpec(SearchSpecProto searchSpecProto) {
        mSearchSpecProto = searchSpecProto;
    }

    /** Creates a new {@link SearchSpec.Builder}. */
    @NonNull
    public static SearchSpec.Builder newBuilder() {
        return new SearchSpec.Builder();
    }

    /** @hide */
    @NonNull
    SearchSpecProto getProto() {
        return mSearchSpecProto;
    }

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

    public static final int TERM_MATCH_TYPE_EXACT_ONLY = 1;
    public static final int TERM_MATCH_TYPE_PREFIX = 2;

    /** Builder for {@link SearchSpec objects}. */
    public static final class Builder {

        private final SearchSpecProto.Builder mBuilder = SearchSpecProto.newBuilder();

        private Builder(){}

        /**
         * Indicates how the query terms should match {@link TermMatchTypeCode} in the index.
         *
         *   TermMatchType.Code=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"
         *
         *   TermMatchType.Code=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".
         */
        @NonNull
        public Builder setTermMatchType(@TermMatchTypeCode int termMatchTypeCode) {
            TermMatchType.Code termMatchTypeCodeProto =
                    TermMatchType.Code.forNumber(termMatchTypeCode);
            if (termMatchTypeCodeProto == null) {
                throw new IllegalArgumentException("Invalid term match type: " + termMatchTypeCode);
            }
            mBuilder.setTermMatchType(termMatchTypeCodeProto);
            return this;
        }

        /**
         * Adds a Schema type filter to {@link SearchSpec} Entry.
         * Only search for documents that have the specified schema types.
         * If unset, the query will search over all schema types.
         */
        @NonNull
        public Builder setSchemaTypes(@NonNull String... schemaTypes) {
            for (String schemaType : schemaTypes) {
                mBuilder.addSchemaTypeFilters(schemaType);
            }
            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() {
            if (mBuilder.getTermMatchType() == TermMatchType.Code.UNKNOWN) {
                throw new IllegalSearchSpecException("Missing termMatchType field.");
            }
            return new SearchSpec(mBuilder.build());
        }
    }

}
Loading