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

Commit 6cd07dea authored by Siddhartha Chhabra's avatar Siddhartha Chhabra Committed by Android (Google) Code Review
Browse files

Merge "Added ResultSpec, SnippetSpec and ScoringSpec."

parents 6a79f29c af0b52fd
Loading
Loading
Loading
Loading
+48 −0
Original line number Diff line number Diff line
@@ -101,6 +101,54 @@ public final class AppSearch {
            this(document.mProto, document.mPropertyBundle);
        }

        /** @hide */
        Document(@NonNull DocumentProto documentProto) {
            this(documentProto, new Bundle());
            for (int i = 0; i < documentProto.getPropertiesCount(); i++) {
                PropertyProto property = documentProto.getProperties(i);
                String name = property.getName();
                if (property.getStringValuesCount() > 0) {
                    String[] values = new String[property.getStringValuesCount()];
                    for (int j = 0; j < values.length; j++) {
                        values[j] = property.getStringValues(j);
                    }
                    mPropertyBundle.putStringArray(name, values);
                } else if (property.getInt64ValuesCount() > 0) {
                    long[] values = new long[property.getInt64ValuesCount()];
                    for (int j = 0; j < values.length; j++) {
                        values[j] = property.getInt64Values(j);
                    }
                    mPropertyBundle.putLongArray(property.getName(), values);
                } else if (property.getDoubleValuesCount() > 0) {
                    double[] values = new double[property.getDoubleValuesCount()];
                    for (int j = 0; j < values.length; j++) {
                        values[j] = property.getDoubleValues(j);
                    }
                    mPropertyBundle.putDoubleArray(property.getName(), values);
                } else if (property.getBooleanValuesCount() > 0) {
                    boolean[] values = new boolean[property.getBooleanValuesCount()];
                    for (int j = 0; j < values.length; j++) {
                        values[j] = property.getBooleanValues(j);
                    }
                    mPropertyBundle.putBooleanArray(property.getName(), values);
                } else if (property.getBytesValuesCount() > 0) {
                    byte[][] values = new byte[property.getBytesValuesCount()][];
                    for (int j = 0; j < values.length; j++) {
                        values[j] = property.getBytesValues(j).toByteArray();
                    }
                    mPropertyBundle.putObject(name, values);
                } else if (property.getDocumentValuesCount() > 0) {
                    Document[] values = new Document[property.getDocumentValuesCount()];
                    for (int j = 0; j < values.length; j++) {
                        values[j] = new Document(property.getDocumentValues(j));
                    }
                    mPropertyBundle.putObject(name, values);
                } else {
                    throw new IllegalStateException("Unknown type of value: " + name);
                }
            }
        }

        /**
         * Creates a new {@link Document.Builder}.
         *
+12 −7
Original line number Diff line number Diff line
@@ -26,6 +26,7 @@ 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.proto.SearchSpecProto;
import com.google.android.icing.proto.StatusProto;
import com.google.android.icing.protobuf.InvalidProtocolBufferException;

@@ -186,28 +187,28 @@ public class AppSearchManager {
     *<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
     *     <p>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
     *     <p>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
     *     <p>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.
     *     <p>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.
     *     <p> Specifies 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
     *     <p>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
@@ -263,7 +264,11 @@ public class AppSearchManager {
        }, executor);

        try {
            mService.query(queryExpression, searchSpec.getProto().toByteArray(), future);
            SearchSpecProto searchSpecProto = searchSpec.getSearchSpecProto();
            searchSpecProto = searchSpecProto.toBuilder().setQuery(queryExpression).build();
            mService.query(searchSpecProto.toByteArray(),
                    searchSpec.getResultSpecProto().toByteArray(),
                    searchSpec.getScoringSpecProto().toByteArray(), future);
        } catch (RemoteException e) {
            future.completeExceptionally(e);
        }
+6 −4
Original line number Diff line number Diff line
@@ -47,12 +47,14 @@ interface IAppSearchManager {
    void putDocuments(in List documentsBytes, in AndroidFuture<AppSearchBatchResult> callback);

    /**
     * Searches a document based on a given query string.
     * Searches a document based on a given specifications.
     *
     * @param queryExpression Query String to search.
     * @param searchSpec Serialized SearchSpecProto.
     * @param searchSpecBytes Serialized SearchSpecProto.
     * @param resultSpecBytes Serialized SearchResultsProto.
     * @param scoringSpecBytes Serialized ScoringSpecProto.
     * @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);
    void query(in byte[] searchSpecBytes, in byte[] resultSpecBytes,
            in byte[] scoringSpecBytes, in AndroidFuture callback);
}
+182 −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 android.util.Range;

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

/**
 * Snippet: It refers to a substring of text from the content of document that is returned as a
 * part of search result.
 * This class represents a match objects for any Snippets that might be present in
 * {@link SearchResults} from query. Using this class user can get the full text, exact matches and
 * Snippets of document content for a given match.
 *
 * <p>Class Example 1:
 * A document contains following text in property subject:
 * <p>A commonly used fake word is foo. Another nonsense word that’s used a lot is bar.
 *
 * <p>If the queryExpression is "foo".
 *
 * <p>{@link MatchInfo#getPropertyPath()} returns "subject"
 * <p>{@link MatchInfo#getFullText()} returns "A commonly used fake word is foo. Another nonsense
 * word that’s used a lot is bar."
 * <p>{@link MatchInfo#getExactMatchPosition()} returns [29, 32]
 * <p>{@link MatchInfo#getExactMatch()} returns "foo"
 * <p>{@link MatchInfo#getSnippetPosition()} returns [29, 41]
 * <p>{@link MatchInfo#getSnippet()} returns "is foo. Another"
 * <p>
 * <p>Class Example 2:
 * A document contains a property name sender which contains 2 property names name and email, so
 * we will have 2 property paths: {@code sender.name} and {@code sender.email}.
 * <p> Let {@code sender.name = "Test Name Jr."} and {@code sender.email = "TestNameJr@gmail.com"}
 *
 * <p>If the queryExpression is "Test". We will have 2 matches.
 *
 * <p> Match-1
 * <p>{@link MatchInfo#getPropertyPath()} returns "sender.name"
 * <p>{@link MatchInfo#getFullText()} returns "Test Name Jr."
 * <p>{@link MatchInfo#getExactMatchPosition()} returns [0, 4]
 * <p>{@link MatchInfo#getExactMatch()} returns "Test"
 * <p>{@link MatchInfo#getSnippetPosition()} returns [0, 9]
 * <p>{@link MatchInfo#getSnippet()} returns "Test Name Jr."
 * <p> Match-2
 * <p>{@link MatchInfo#getPropertyPath()} returns "sender.email"
 * <p>{@link MatchInfo#getFullText()} returns "TestNameJr@gmail.com"
 * <p>{@link MatchInfo#getExactMatchPosition()} returns [0, 20]
 * <p>{@link MatchInfo#getExactMatch()} returns "TestNameJr@gmail.com"
 * <p>{@link MatchInfo#getSnippetPosition()} returns [0, 20]
 * <p>{@link MatchInfo#getSnippet()} returns "TestNameJr@gmail.com"
 * @hide
 */
// TODO(sidchhabra): Capture real snippet after integration with icingLib.
public final class MatchInfo {

    private final String mPropertyPath;
    private final SnippetMatchProto mSnippetMatch;
    private final AppSearch.Document mDocument;
    /**
     * List of content with same property path in a document when there are multiple matches in
     * repeated sections.
     */
    private final String[] mValues;

    /** @hide */
    public MatchInfo(@NonNull String propertyPath, @NonNull SnippetMatchProto snippetMatch,
            @NonNull AppSearch.Document document) {
        mPropertyPath = propertyPath;
        mSnippetMatch = snippetMatch;
        mDocument = document;
        // In IcingLib snippeting is available for only 3 data types i.e String, double and long,
        // so we need to check which of these three are requested.
        // TODO (sidchhabra): getPropertyStringArray takes property name, handle for property path.
        String[] values = mDocument.getPropertyStringArray(propertyPath);
        if (values == null) {
            values = doubleToString(mDocument.getPropertyDoubleArray(propertyPath));
        }
        if (values == null) {
            values = longToString(mDocument.getPropertyLongArray(propertyPath));
        }
        if (values == null) {
            throw new IllegalStateException("No content found for requested property path!");
        }
        mValues = values;
    }

    /**
     * Gets the property path corresponding to the given entry.
     * <p>Property Path: '.' - delimited sequence of property names indicating which property in
     * the Document these snippets correspond to.
     * <p>Example properties: 'body', 'sender.name', 'sender.emailaddress', etc.
     * For class example 1 this returns "subject"
     */
    @NonNull
    public String getPropertyPath() {
        return mPropertyPath;
    }

    /**
     * Gets the full text corresponding to the given entry.
     * <p>For class example this returns "A commonly used fake word is foo. Another nonsense word
     * that’s used a lot is bar."
     */
    @NonNull
    public String getFullText() {
        return mValues[mSnippetMatch.getValuesIndex()];
    }

    /**
     * Gets the exact match range corresponding to the given entry.
     * <p>For class example 1 this returns [29, 32]
     */
    @NonNull
    public Range getExactMatchPosition() {
        return new Range(mSnippetMatch.getExactMatchPosition(),
                mSnippetMatch.getExactMatchPosition() + mSnippetMatch.getExactMatchBytes());
    }

    /**
     * Gets the exact match corresponding to the given entry.
     * <p>For class example 1 this returns "foo"
     */
    @NonNull
    public CharSequence getExactMatch() {
        return getSubstring(getExactMatchPosition());
    }

    /**
     * Gets the snippet range corresponding to the given entry.
     * <p>For class example 1 this returns [29, 41]
     */
    @NonNull
    public Range getSnippetPosition() {
        return new Range(mSnippetMatch.getWindowPosition(),
                mSnippetMatch.getWindowPosition() + mSnippetMatch.getWindowBytes());
    }

    /**
     * Gets the snippet corresponding to the given entry.
     * <p>Snippet - Provides a subset of the content to display. The
     * length of this content can be changed {@link SearchSpec.Builder#setMaxSnippetSize(int)}.
     * Windowing is centered around the middle of the matched token with content on either side
     * clipped to token boundaries.
     * <p>For class example 1 this returns "foo. Another"
     */
    @NonNull
    public CharSequence getSnippet() {
        return getSubstring(getSnippetPosition());
    }

    private CharSequence getSubstring(Range range) {
        return getFullText()
                .substring((int) range.getLower(), (int) range.getUpper());
    }

    /** Utility method to convert double[] to String[] */
    private String[] doubleToString(double[] values) {
        //TODO(sidchhabra): Implement the method.
        return null;
    }

    /** Utility method to convert long[] to String[] */
    private String[] longToString(long[] values) {
        //TODO(sidchhabra): Implement the method.
        return null;
    }
}
+61 −22
Original line number Diff line number Diff line
@@ -17,27 +17,51 @@
package android.app.appsearch;

import android.annotation.NonNull;
import android.annotation.Nullable;

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

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

/**
 * 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.
 * This iterator class is not thread safe.
 * @hide
 */
public final class SearchResults {
public final class SearchResults implements Iterator<SearchResults.Result> {

    private final SearchResultProto mSearchResultProto;
    private int mNextIdx;

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

    @Override
    public boolean hasNext() {
        return mNextIdx < mSearchResultProto.getResultsCount();
    }

    @NonNull
    @Override
    public Result next() {
        if (!hasNext()) {
            throw new NoSuchElementException();
        }
        Result result = new Result(mSearchResultProto.getResults(mNextIdx));
        mNextIdx++;
        return result;
    }



    /**
     * This class represents the result obtained from the query. It will contain the document which
     * which matched the specified query string and specifications.
@@ -46,6 +70,9 @@ public final class SearchResults {
    public static final class Result {
        private final SearchResultProto.ResultProto mResultProto;

        @Nullable
        private AppSearch.Document mDocument;

        private Result(SearchResultProto.ResultProto resultProto) {
            mResultProto = resultProto;
        }
@@ -55,35 +82,47 @@ public final class SearchResults {
         * @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())
                    .setCreationTimestampMillis(mResultProto.getDocument().getCreationTimestampMs())
                    .setScore(mResultProto.getDocument().getScore())
                    .build();
        }

        // TODO(sidchhabra): Add Getter for ResultReader for Snippet.
            if (mDocument == null) {
                mDocument = new AppSearch.Document(mResultProto.getDocument());
            }

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

        /**
     * Returns a {@link Result} iterator. Returns Empty Iterator if there are no matching results.
         * Contains a list of Snippets that matched the request. Only populated when requested in
         * {@link SearchSpec.Builder#setMaxSnippetSize(int)}.
         * @return  List of matches based on {@link SearchSpec}, if snippeting is disabled and this
         * method is called it will return {@code null}. Users can also restrict snippet population
         * using {@link SearchSpec.Builder#setNumToSnippet} and
         * {@link SearchSpec.Builder#setNumMatchesPerProperty}, for all results after that value
         * this method will return {@code null}.
         * @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));
        // TODO(sidchhabra): Replace Document with proper constructor.
        @Nullable
        public List<MatchInfo> getMatchInfo() {
            if (!mResultProto.hasSnippet()) {
                return null;
            }
            AppSearch.Document document = getDocument();
            List<MatchInfo> matchList = new ArrayList<>();
            for (Iterator entryProtoIterator = mResultProto.getSnippet()
                    .getEntriesList().iterator(); entryProtoIterator.hasNext(); ) {
                SnippetProto.EntryProto entry = (SnippetProto.EntryProto) entryProtoIterator.next();
                for (Iterator snippetMatchProtoIterator = entry.getSnippetMatchesList().iterator();
                        snippetMatchProtoIterator.hasNext(); ) {
                    matchList.add(new MatchInfo(entry.getPropertyName(),
                            (SnippetMatchProto) snippetMatchProtoIterator.next(), document));
                }
            }
        return results.iterator();
            return matchList;
        }
    }

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