1 /*
2  * Copyright 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.app.appsearch;
18 
19 import android.annotation.NonNull;
20 import android.annotation.SuppressLint;
21 
22 import com.google.common.util.concurrent.ListenableFuture;
23 
24 import java.io.Closeable;
25 import java.util.List;
26 import java.util.Set;
27 
28 /**
29  * Provides a connection to a single AppSearch database.
30  *
31  * <p>An {@link AppSearchSessionShim} instance provides access to database operations such as
32  * setting a schema, adding documents, and searching.
33  *
34  * <p>Instances of this interface are usually obtained from a storage implementation, e.g. {@code
35  * AppSearchManager.createSearchSessionAsync()} or {@code
36  * PlatformStorage.createSearchSessionAsync()}.
37  *
38  * <p>All implementations of this interface must be thread safe.
39  *
40  * @see GlobalSearchSessionShim
41  */
42 public interface AppSearchSessionShim extends Closeable {
43 
44     /**
45      * Sets the schema that represents the organizational structure of data within the AppSearch
46      * database.
47      *
48      * <p>Upon creating an {@link AppSearchSessionShim}, {@link #setSchema} should be called. If the
49      * schema needs to be updated, or it has not been previously set, then the provided schema will
50      * be saved and persisted to disk. Otherwise, {@link #setSchema} is handled efficiently as a
51      * no-op call.
52      *
53      * @param request the schema to set or update the AppSearch database to.
54      * @return a {@link ListenableFuture} which resolves to a {@link SetSchemaResponse} object.
55      */
56     @NonNull
setSchemaAsync(@onNull SetSchemaRequest request)57     ListenableFuture<SetSchemaResponse> setSchemaAsync(@NonNull SetSchemaRequest request);
58 
59     /**
60      * Retrieves the schema most recently successfully provided to {@link #setSchema}.
61      *
62      * @return The pending {@link GetSchemaResponse} of performing this operation.
63      */
64     // This call hits disk; async API prevents us from treating these calls as properties.
65     @SuppressLint("KotlinPropertyAccess")
66     @NonNull
getSchemaAsync()67     ListenableFuture<GetSchemaResponse> getSchemaAsync();
68 
69     /**
70      * Retrieves the set of all namespaces in the current database with at least one document.
71      *
72      * @return The pending result of performing this operation.
73      */
74     @NonNull
getNamespacesAsync()75     ListenableFuture<Set<String>> getNamespacesAsync();
76 
77     /**
78      * Indexes documents into the {@link AppSearchSessionShim} database.
79      *
80      * <p>Each {@link GenericDocument} object must have a {@code schemaType} field set to an {@link
81      * AppSearchSchema} type that has been previously registered by calling the {@link #setSchema}
82      * method.
83      *
84      * @param request containing documents to be indexed.
85      * @return a {@link ListenableFuture} which resolves to an {@link AppSearchBatchResult}. The
86      *     keys of the returned {@link AppSearchBatchResult} are the IDs of the input documents. The
87      *     values are either {@code null} if the corresponding document was successfully indexed, or
88      *     a failed {@link AppSearchResult} otherwise.
89      */
90     @NonNull
putAsync( @onNull PutDocumentsRequest request)91     ListenableFuture<AppSearchBatchResult<String, Void>> putAsync(
92             @NonNull PutDocumentsRequest request);
93 
94     /**
95      * Gets {@link GenericDocument} objects by document IDs in a namespace from the {@link
96      * AppSearchSessionShim} database.
97      *
98      * @param request a request containing a namespace and IDs to get documents for.
99      * @return A {@link ListenableFuture} which resolves to an {@link AppSearchBatchResult}. The
100      *     keys of the {@link AppSearchBatchResult} represent the input document IDs from the {@link
101      *     GetByDocumentIdRequest} object. The values are either the corresponding {@link
102      *     GenericDocument} object for the ID on success, or an {@link AppSearchResult} object on
103      *     failure. For example, if an ID is not found, the value for that ID will be set to an
104      *     {@link AppSearchResult} object with result code: {@link
105      *     AppSearchResult#RESULT_NOT_FOUND}.
106      */
107     @NonNull
getByDocumentIdAsync( @onNull GetByDocumentIdRequest request)108     ListenableFuture<AppSearchBatchResult<String, GenericDocument>> getByDocumentIdAsync(
109             @NonNull GetByDocumentIdRequest request);
110 
111     /**
112      * Retrieves documents from the open {@link AppSearchSessionShim} that match a given query
113      * string and type of search provided.
114      *
115      * <p>Query strings can be empty, contain one term with no operators, or contain multiple terms
116      * and operators.
117      *
118      * <p>For query strings that are empty, all documents that match the {@link SearchSpec} will be
119      * returned.
120      *
121      * <p>For query strings with a single term and no operators, documents that match the provided
122      * query string and {@link SearchSpec} will be returned.
123      *
124      * <p>The following operators are supported:
125      *
126      * <ul>
127      *   <li>AND (implicit)
128      *       <p>AND is an operator that matches documents that contain <i>all</i> provided terms.
129      *       <p><b>NOTE:</b> A space between terms is treated as an "AND" operator. Explicitly
130      *       including "AND" in a query string will treat "AND" as a term, returning documents that
131      *       also contain "AND".
132      *       <p>Example: "apple AND banana" matches documents that contain the terms "apple", "and",
133      *       "banana".
134      *       <p>Example: "apple banana" matches documents that contain both "apple" and "banana".
135      *       <p>Example: "apple banana cherry" matches documents that contain "apple", "banana", and
136      *       "cherry".
137      *   <li>OR
138      *       <p>OR is an operator that matches documents that contain <i>any</i> provided term.
139      *       <p>Example: "apple OR banana" matches documents that contain either "apple" or
140      *       "banana".
141      *       <p>Example: "apple OR banana OR cherry" matches documents that contain any of "apple",
142      *       "banana", or "cherry".
143      *   <li>Exclusion (-)
144      *       <p>Exclusion (-) is an operator that matches documents that <i>do not</i> contain the
145      *       provided term.
146      *       <p>Example: "-apple" matches documents that do not contain "apple".
147      *   <li>Grouped Terms
148      *       <p>For queries that require multiple operators and terms, terms can be grouped into
149      *       subqueries. Subqueries are contained within an open "(" and close ")" parenthesis.
150      *       <p>Example: "(donut OR bagel) (coffee OR tea)" matches documents that contain either
151      *       "donut" or "bagel" and either "coffee" or "tea".
152      *   <li>Property Restricts
153      *       <p>For queries that require a term to match a specific {@link AppSearchSchema} property
154      *       of a document, a ":" must be included between the property name and the term.
155      *       <p>Example: "subject:important" matches documents that contain the term "important" in
156      *       the "subject" property.
157      * </ul>
158      *
159      * <p>The above description covers the query operators that are supported on all versions of
160      * AppSearch. Additional operators and their required features are described below.
161      *
162      * <p>{@link Features#LIST_FILTER_QUERY_LANGUAGE}: This feature covers the expansion of the
163      * query language to conform to the definition of the list filters language (https://aip
164      * .dev/160). This includes:
165      *
166      * <ul>
167      *   <li>addition of explicit 'AND' and 'NOT' operators
168      *   <li>property restricts are allowed with groupings (ex. "prop:(a OR b)")
169      *   <li>addition of custom functions to control matching
170      * </ul>
171      *
172      * <p>The newly added custom functions covered by this feature are:
173      *
174      * <ul>
175      *   <li>createList(String...)
176      *   <li>search(String, {@code List<String>})
177      *   <li>propertyDefined(String)
178      * </ul>
179      *
180      * <p>createList takes a variable number of strings and returns a list of strings. It is for use
181      * with search.
182      *
183      * <p>search takes a query string that will be parsed according to the supported query language
184      * and an optional list of strings that specify the properties to be restricted to. This exists
185      * as a convenience for multiple property restricts. So, for example, the query `(subject:foo OR
186      * body:foo) (subject:bar OR body:bar)` could be rewritten as `search("foo bar",
187      * createList("subject", "bar"))`.
188      *
189      * <p>propertyDefined takes a string specifying the property of interest and matches all
190      * documents of any type that defines the specified property (ex.
191      * `propertyDefined("sender.name")`). Note that propertyDefined will match so long as the
192      * document's type defines the specified property. Unlike the "hasProperty" function below, this
193      * function does NOT require that the document actually hold any values for this property.
194      *
195      * <p>{@link Features#NUMERIC_SEARCH}: This feature covers numeric search expressions. In the
196      * query language, the values of properties that have {@link
197      * AppSearchSchema.LongPropertyConfig#INDEXING_TYPE_RANGE} set can be matched with a numeric
198      * search expression (the property, a supported comparator and an integer value). Supported
199      * comparators are <, <=, ==, >= and >.
200      *
201      * <p>Ex. `price < 10` will match all documents that has a numeric value in its price property
202      * that is less than 10.
203      *
204      * <p>{@link Features#VERBATIM_SEARCH}: This feature covers the verbatim string operator
205      * (quotation marks).
206      *
207      * <p>Ex. `"foo/bar" OR baz` will ensure that 'foo/bar' is treated as a single 'verbatim' token.
208      *
209      * <p>{@link Features#LIST_FILTER_HAS_PROPERTY_FUNCTION}: This feature covers the "hasProperty"
210      * function in query expressions, which takes a string specifying the property of interest and
211      * matches all documents that hold values for this property. Not to be confused with the
212      * "propertyDefined" function, which checks whether a document's schema has defined the
213      * property, instead of whether a document itself has this property.
214      *
215      * <p>Ex. `foo hasProperty("sender.name")` will return all documents that have the term "foo"
216      * AND have values in the property "sender.name". Consider two documents, documentA and
217      * documentB, of the same schema with an optional property "sender.name". If documentA sets
218      * "foo" in this property but documentB does not, then `hasProperty("sender.name")` will only
219      * match documentA. However, `propertyDefined("sender.name")` will match both documentA and
220      * documentB, regardless of whether a value is actually set.
221      *
222      * <p>{@link Features#SCHEMA_EMBEDDING_PROPERTY_CONFIG}: This feature covers the
223      * "semanticSearch" and "getSearchSpecEmbedding" functions in query expressions, which are used
224      * for semantic search.
225      *
226      * <p>Usage: semanticSearch(getSearchSpecEmbedding({embedding_index}), {low}, {high}, {metric})
227      *
228      * <ul>
229      *   <li>semanticSearch matches all documents that have at least one embedding vector with a
230      *       matching model signature (see {@link EmbeddingVector#getModelSignature()}) and a
231      *       similarity score within the range specified based on the provided metric.
232      *   <li>getSearchSpecEmbedding({embedding_index}) retrieves the embedding search passed in
233      *       {@link SearchSpec.Builder#addSearchEmbeddings} based on the index specified, which
234      *       starts from 0.
235      *   <li>"low" and "high" are floating point numbers that specify the similarity score range. If
236      *       omitted, they default to negative and positive infinity, respectively.
237      *   <li>"metric" is a string value that specifies how embedding similarities should be
238      *       calculated. If omitted, it defaults to the metric specified in {@link
239      *       SearchSpec.Builder#setDefaultEmbeddingSearchMetricType(int)}. Possible values:
240      *       <ul>
241      *         <li>"COSINE"
242      *         <li>"DOT_PRODUCT"
243      *         <li>"EUCLIDEAN"
244      *       </ul>
245      * </ul>
246      *
247      * <p>Examples:
248      *
249      * <ul>
250      *   <li>Basic: semanticSearch(getSearchSpecEmbedding(0), 0.5, 1, "COSINE")
251      *   <li>With a property restriction: property1:semanticSearch(getSearchSpecEmbedding(0), 0.5,
252      *       1)
253      *   <li>Hybrid: foo OR semanticSearch(getSearchSpecEmbedding(0), 0.5, 1)
254      *   <li>Complex: (foo OR semanticSearch(getSearchSpecEmbedding(0), 0.5, 1)) AND bar
255      * </ul>
256      *
257      * <p>{@link Features#LIST_FILTER_TOKENIZE_FUNCTION}: This feature covers the "tokenize"
258      * function in query expressions, which takes a string and treats the entire string as plain
259      * text. This string is then segmented, normalized and stripped of punctuation-only segments.
260      * The remaining tokens are then AND'd together. This function is useful for callers who wish to
261      * provide user input, but want to ensure that that user input does not invoke any query
262      * operators.
263      *
264      * <p>Ex. `foo OR tokenize("bar OR baz.")`. The string "bar OR baz." will be segmented into
265      * "bar", "OR", "baz", ".". Punctuation is removed and the segments are normalized to "bar",
266      * "or", "baz". This query will be equivalent to `foo OR (bar AND or AND baz)`.
267      *
268      * <p>Ex. `tokenize("\"bar\" OR \\baz")`. Quotation marks and escape characters must be escaped.
269      * This query will be segmented into "\"", "bar", "\"", "OR", "\", "baz". Once stripped of
270      * punctuation and normalized, this will be equivalent to the query `bar AND or AND baz`.
271      *
272      * <p>The availability of each of these features can be checked by calling {@link
273      * Features#isFeatureSupported} with the desired feature.
274      *
275      * <p>Additional search specifications, such as filtering by {@link AppSearchSchema} type or
276      * adding projection, can be set by calling the corresponding {@link SearchSpec.Builder} setter.
277      *
278      * <p>This method is lightweight. The heavy work will be done in {@link
279      * SearchResultsShim#getNextPage}.
280      *
281      * @param queryExpression query string to search.
282      * @param searchSpec spec for setting document filters, adding projection, setting term match
283      *     type, etc.
284      * @return a {@link SearchResultsShim} object for retrieved matched documents.
285      */
286     // TODO(b/326656531): Refine the javadoc to provide guidance on the best practice of
287     //  embedding searches and how to select an appropriate metric.
288     @NonNull
search(@onNull String queryExpression, @NonNull SearchSpec searchSpec)289     SearchResultsShim search(@NonNull String queryExpression, @NonNull SearchSpec searchSpec);
290 
291     /**
292      * Retrieves suggested Strings that could be used as {@code queryExpression} in {@link
293      * #search(String, SearchSpec)} API.
294      *
295      * <p>The {@code suggestionQueryExpression} can contain one term with no operators, or contain
296      * multiple terms and operators. Operators will be considered as a normal term. Please see the
297      * operator examples below. The {@code suggestionQueryExpression} must end with a valid term,
298      * the suggestions are generated based on the last term. If the input {@code
299      * suggestionQueryExpression} doesn't have a valid token, AppSearch will return an empty result
300      * list. Please see the invalid examples below.
301      *
302      * <p>Example: if there are following documents with content stored in AppSearch.
303      *
304      * <ul>
305      *   <li>document1: "term1"
306      *   <li>document2: "term1 term2"
307      *   <li>document3: "term1 term2 term3"
308      *   <li>document4: "org"
309      * </ul>
310      *
311      * <p>Search suggestions with the single term {@code suggestionQueryExpression} "t", the
312      * suggested results are:
313      *
314      * <ul>
315      *   <li>"term1" - Use it to be queryExpression in {@link #search} could get 3 {@link
316      *       SearchResult}s, which contains document 1, 2 and 3.
317      *   <li>"term2" - Use it to be queryExpression in {@link #search} could get 2 {@link
318      *       SearchResult}s, which contains document 2 and 3.
319      *   <li>"term3" - Use it to be queryExpression in {@link #search} could get 1 {@link
320      *       SearchResult}, which contains document 3.
321      * </ul>
322      *
323      * <p>Search suggestions with the multiple term {@code suggestionQueryExpression} "org t", the
324      * suggested result will be "org term1" - The last token is completed by the suggested String.
325      *
326      * <p>Operators in {@link #search} are supported.
327      *
328      * <p><b>NOTE:</b> Exclusion and Grouped Terms in the last term is not supported.
329      *
330      * <p>example: "apple -f": This Api will throw an {@link
331      * android.app.appsearch.exceptions.AppSearchException} with {@link
332      * AppSearchResult#RESULT_INVALID_ARGUMENT}.
333      *
334      * <p>example: "apple (f)": This Api will return an empty results.
335      *
336      * <p>Invalid example: All these input {@code suggestionQueryExpression} don't have a valid last
337      * token, AppSearch will return an empty result list.
338      *
339      * <ul>
340      *   <li>"" - Empty {@code suggestionQueryExpression}.
341      *   <li>"(f)" - Ending in a closed brackets.
342      *   <li>"f:" - Ending in an operator.
343      *   <li>"f " - Ending in trailing space.
344      * </ul>
345      *
346      * @param suggestionQueryExpression the non empty query string to search suggestions
347      * @param searchSuggestionSpec spec for setting document filters
348      * @return The pending result of performing this operation which resolves to a List of {@link
349      *     SearchSuggestionResult} on success. The returned suggestion Strings are ordered by the
350      *     number of {@link SearchResult} you could get by using that suggestion in {@link #search}.
351      * @see #search(String, SearchSpec)
352      */
353     @NonNull
searchSuggestionAsync( @onNull String suggestionQueryExpression, @NonNull SearchSuggestionSpec searchSuggestionSpec)354     ListenableFuture<List<SearchSuggestionResult>> searchSuggestionAsync(
355             @NonNull String suggestionQueryExpression,
356             @NonNull SearchSuggestionSpec searchSuggestionSpec);
357 
358     /**
359      * Reports usage of a particular document by namespace and ID.
360      *
361      * <p>A usage report represents an event in which a user interacted with or viewed a document.
362      *
363      * <p>For each call to {@link #reportUsage}, AppSearch updates usage count and usage recency *
364      * metrics for that particular document. These metrics are used for ordering {@link #search}
365      * results by the {@link SearchSpec#RANKING_STRATEGY_USAGE_COUNT} and {@link
366      * SearchSpec#RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP} ranking strategies.
367      *
368      * <p>Reporting usage of a document is optional.
369      *
370      * @param request The usage reporting request.
371      * @return The pending result of performing this operation which resolves to {@code null} on
372      *     success.
373      */
374     @NonNull
reportUsageAsync(@onNull ReportUsageRequest request)375     ListenableFuture<Void> reportUsageAsync(@NonNull ReportUsageRequest request);
376 
377     /**
378      * Removes {@link GenericDocument} objects by document IDs in a namespace from the {@link
379      * AppSearchSessionShim} database.
380      *
381      * <p>Removed documents will no longer be surfaced by {@link #search} or {@link
382      * #getByDocumentId} calls.
383      *
384      * <p>Once the database crosses the document count or byte usage threshold, removed documents
385      * will be deleted from disk.
386      *
387      * @param request {@link RemoveByDocumentIdRequest} with IDs in a namespace to remove from the
388      *     index.
389      * @return a {@link ListenableFuture} which resolves to an {@link AppSearchBatchResult}. The
390      *     keys of the {@link AppSearchBatchResult} represent the input IDs from the {@link
391      *     RemoveByDocumentIdRequest} object. The values are either {@code null} on success, or a
392      *     failed {@link AppSearchResult} otherwise. IDs that are not found will return a failed
393      *     {@link AppSearchResult} with a result code of {@link AppSearchResult#RESULT_NOT_FOUND}.
394      */
395     @NonNull
removeAsync( @onNull RemoveByDocumentIdRequest request)396     ListenableFuture<AppSearchBatchResult<String, Void>> removeAsync(
397             @NonNull RemoveByDocumentIdRequest request);
398 
399     /**
400      * Removes {@link GenericDocument}s from the index by Query. Documents will be removed if they
401      * match the {@code queryExpression} in given namespaces and schemaTypes which is set via {@link
402      * SearchSpec.Builder#addFilterNamespaces} and {@link SearchSpec.Builder#addFilterSchemas}.
403      *
404      * <p>An empty {@code queryExpression} matches all documents.
405      *
406      * <p>An empty set of namespaces or schemaTypes matches all namespaces or schemaTypes in the
407      * current database.
408      *
409      * @param queryExpression Query String to search.
410      * @param searchSpec Spec containing schemaTypes, namespaces and query expression indicates how
411      *     document will be removed. All specific about how to scoring, ordering, snippeting and
412      *     resulting will be ignored.
413      * @return The pending result of performing this operation.
414      * @throws IllegalArgumentException if the {@link SearchSpec} contains a {@link JoinSpec}.
415      *     {@link JoinSpec} lets you join docs that are not owned by the caller, so the semantics of
416      *     failures from this method would be complex.
417      */
418     @NonNull
removeAsync( @onNull String queryExpression, @NonNull SearchSpec searchSpec)419     ListenableFuture<Void> removeAsync(
420             @NonNull String queryExpression, @NonNull SearchSpec searchSpec);
421 
422     /**
423      * Gets the storage info for this {@link AppSearchSessionShim} database.
424      *
425      * <p>This may take time proportional to the number of documents and may be inefficient to call
426      * repeatedly.
427      *
428      * @return a {@link ListenableFuture} which resolves to a {@link StorageInfo} object.
429      */
430     @NonNull
getStorageInfoAsync()431     ListenableFuture<StorageInfo> getStorageInfoAsync();
432 
433     /**
434      * Flush all schema and document updates, additions, and deletes to disk if possible.
435      *
436      * <p>The request is not guaranteed to be handled and may be ignored by some implementations of
437      * AppSearchSessionShim.
438      *
439      * @return The pending result of performing this operation. {@link
440      *     android.app.appsearch.exceptions.AppSearchException} with {@link
441      *     AppSearchResult#RESULT_INTERNAL_ERROR} will be set to the future if we hit error when
442      *     save to disk.
443      */
444     @NonNull
requestFlushAsync()445     ListenableFuture<Void> requestFlushAsync();
446 
447     /**
448      * Returns the {@link Features} to check for the availability of certain features for this
449      * session.
450      */
451     @NonNull
getFeatures()452     Features getFeatures();
453 
454     /**
455      * Closes the {@link AppSearchSessionShim} to persist all schema and document updates,
456      * additions, and deletes to disk.
457      */
458     @Override
close()459     void close();
460 }
461