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

Commit af0b52fd authored by sidchhabra's avatar sidchhabra
Browse files

Added ResultSpec, SnippetSpec and ScoringSpec.

Change-Id: Ib851feec672a0223ef8f05f2ba5849220cbd34e3
Test: atest core/tests/coretests/src/android/app/appsearch/SnippetTest.java
Bug: 145631811
parent 435a9f5b
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