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 androidx.appsearch.cts.app;
18 
19 import static androidx.appsearch.app.AppSearchResult.RESULT_INVALID_ARGUMENT;
20 import static androidx.appsearch.app.AppSearchResult.RESULT_INVALID_SCHEMA;
21 import static androidx.appsearch.app.AppSearchResult.RESULT_NOT_FOUND;
22 import static androidx.appsearch.testutil.AppSearchTestUtils.checkIsBatchResultSuccess;
23 import static androidx.appsearch.testutil.AppSearchTestUtils.convertSearchResultsToDocuments;
24 import static androidx.appsearch.testutil.AppSearchTestUtils.doGet;
25 import static androidx.appsearch.testutil.AppSearchTestUtils.retrieveAllSearchResults;
26 
27 import static com.google.common.truth.Truth.assertThat;
28 
29 import static org.junit.Assert.assertThrows;
30 import static org.junit.Assume.assumeFalse;
31 import static org.junit.Assume.assumeTrue;
32 
33 import android.content.Context;
34 
35 import androidx.appsearch.app.AppSearchBatchResult;
36 import androidx.appsearch.app.AppSearchResult;
37 import androidx.appsearch.app.AppSearchSchema;
38 import androidx.appsearch.app.AppSearchSchema.BooleanPropertyConfig;
39 import androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig;
40 import androidx.appsearch.app.AppSearchSchema.DoublePropertyConfig;
41 import androidx.appsearch.app.AppSearchSchema.LongPropertyConfig;
42 import androidx.appsearch.app.AppSearchSchema.PropertyConfig;
43 import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig;
44 import androidx.appsearch.app.AppSearchSession;
45 import androidx.appsearch.app.EmbeddingVector;
46 import androidx.appsearch.app.Features;
47 import androidx.appsearch.app.GenericDocument;
48 import androidx.appsearch.app.GetByDocumentIdRequest;
49 import androidx.appsearch.app.GetSchemaResponse;
50 import androidx.appsearch.app.JoinSpec;
51 import androidx.appsearch.app.PackageIdentifier;
52 import androidx.appsearch.app.PropertyPath;
53 import androidx.appsearch.app.PutDocumentsRequest;
54 import androidx.appsearch.app.RemoveByDocumentIdRequest;
55 import androidx.appsearch.app.ReportUsageRequest;
56 import androidx.appsearch.app.SchemaVisibilityConfig;
57 import androidx.appsearch.app.SearchResult;
58 import androidx.appsearch.app.SearchResults;
59 import androidx.appsearch.app.SearchSpec;
60 import androidx.appsearch.app.SearchSuggestionResult;
61 import androidx.appsearch.app.SearchSuggestionSpec;
62 import androidx.appsearch.app.SetSchemaRequest;
63 import androidx.appsearch.app.StorageInfo;
64 import androidx.appsearch.cts.app.customer.EmailDocument;
65 import androidx.appsearch.exceptions.AppSearchException;
66 import androidx.appsearch.flags.Flags;
67 import androidx.appsearch.testutil.AppSearchEmail;
68 import androidx.appsearch.testutil.AppSearchTestUtils;
69 import androidx.appsearch.testutil.flags.RequiresFlagsEnabled;
70 import androidx.appsearch.usagereporting.ClickAction;
71 import androidx.appsearch.usagereporting.DismissAction;
72 import androidx.appsearch.usagereporting.ImpressionAction;
73 import androidx.appsearch.usagereporting.SearchAction;
74 import androidx.appsearch.util.DocumentIdUtil;
75 import androidx.collection.ArrayMap;
76 import androidx.test.core.app.ApplicationProvider;
77 
78 import com.google.common.collect.ImmutableList;
79 import com.google.common.collect.ImmutableMap;
80 import com.google.common.collect.ImmutableSet;
81 import com.google.common.util.concurrent.ListenableFuture;
82 import com.google.common.util.concurrent.MoreExecutors;
83 
84 import org.jspecify.annotations.NonNull;
85 import org.junit.After;
86 import org.junit.Before;
87 import org.junit.Rule;
88 import org.junit.Test;
89 import org.junit.rules.RuleChain;
90 
91 import java.util.ArrayList;
92 import java.util.Arrays;
93 import java.util.Collections;
94 import java.util.HashSet;
95 import java.util.List;
96 import java.util.Map;
97 import java.util.Set;
98 import java.util.concurrent.ExecutionException;
99 import java.util.concurrent.ExecutorService;
100 
101 public abstract class AppSearchSessionCtsTestBase {
102     static final String DB_NAME_1 = "";
103     static final String DB_NAME_2 = "testDb2";
104 
105     // Since we cannot call non-public API in the cts test, make a copy of these 3 action types, so
106     // we can create taken actions in GenericDocument form.
107     private static final int ACTION_TYPE_SEARCH = 1;
108     private static final int ACTION_TYPE_CLICK = 2;
109     private static final int ACTION_TYPE_IMPRESSION = 3;
110     private static final int ACTION_TYPE_DISMISS = 4;
111 
112     @Rule
113     public final RuleChain mRuleChain = AppSearchTestUtils.createCommonTestRules();
114 
115     private final Context mContext = ApplicationProvider.getApplicationContext();
116 
117     private AppSearchSession mDb1;
118     private AppSearchSession mDb2;
119 
createSearchSessionAsync( @onNull String dbName)120     protected abstract ListenableFuture<AppSearchSession> createSearchSessionAsync(
121             @NonNull String dbName) throws Exception;
122 
createSearchSessionAsync( @onNull String dbName, @NonNull ExecutorService executor)123     protected abstract ListenableFuture<AppSearchSession> createSearchSessionAsync(
124             @NonNull String dbName, @NonNull ExecutorService executor) throws Exception;
125 
126     @Before
setUp()127     public void setUp() throws Exception {
128         mDb1 = createSearchSessionAsync(DB_NAME_1).get();
129         mDb2 = createSearchSessionAsync(DB_NAME_2).get();
130 
131         // Cleanup whatever documents may still exist in these databases. This is needed in
132         // addition to tearDown in case a test exited without completing properly.
133         cleanup();
134     }
135 
136     @After
tearDown()137     public void tearDown() throws Exception {
138         // Cleanup whatever documents may still exist in these databases.
139         cleanup();
140     }
141 
cleanup()142     private void cleanup() throws Exception {
143         mDb1.setSchemaAsync(
144                 new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
145         mDb2.setSchemaAsync(
146                 new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
147     }
148 
149     @Test
testSetSchema()150     public void testSetSchema() throws Exception {
151         AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email")
152                 .addProperty(new StringPropertyConfig.Builder("subject")
153                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
154                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
155                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
156                         .build()
157                 ).addProperty(new StringPropertyConfig.Builder("body")
158                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
159                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
160                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
161                         .build()
162                 ).build();
163         mDb1.setSchemaAsync(
164                 new SetSchemaRequest.Builder().addSchemas(emailSchema).build()).get();
165     }
166 
167     @Test
testSetSchema_Failure()168     public void testSetSchema_Failure() throws Exception {
169         mDb1.setSchemaAsync(
170                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
171         AppSearchSchema emailSchema1 = new AppSearchSchema.Builder(AppSearchEmail.SCHEMA_TYPE)
172                 .build();
173 
174         SetSchemaRequest setSchemaRequest1 =
175                 new SetSchemaRequest.Builder().addSchemas(emailSchema1).build();
176         ExecutionException executionException =
177                 assertThrows(ExecutionException.class,
178                         () -> mDb1.setSchemaAsync(setSchemaRequest1).get());
179         assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
180         AppSearchException exception = (AppSearchException) executionException.getCause();
181         assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_SCHEMA);
182         assertThat(exception).hasMessageThat().contains("Schema is incompatible.");
183         assertThat(exception).hasMessageThat().contains("Incompatible types: {builtin:Email}");
184 
185         SetSchemaRequest setSchemaRequest2 = new SetSchemaRequest.Builder().build();
186         executionException = assertThrows(ExecutionException.class,
187                 () -> mDb1.setSchemaAsync(setSchemaRequest2).get());
188         assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
189         exception = (AppSearchException) executionException.getCause();
190         assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_SCHEMA);
191         assertThat(exception).hasMessageThat().contains("Schema is incompatible.");
192         assertThat(exception).hasMessageThat().contains("Deleted types: {builtin:Email}");
193     }
194 
195     @Test
196     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_DESCRIPTION)  // setDescription
testSetSchema_schemaDescription_notSupported()197     public void testSetSchema_schemaDescription_notSupported() throws Exception {
198         assumeFalse(mDb1.getFeatures().isFeatureSupported(
199                 Features.SCHEMA_SET_DESCRIPTION));
200         AppSearchSchema schema = new AppSearchSchema.Builder("Email1")
201                 .setDescription("Unsupported description")
202                 .addProperty(new StringPropertyConfig.Builder("body")
203                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
204                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
205                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
206                         .build()
207                 ).build();
208 
209         SetSchemaRequest request = new SetSchemaRequest.Builder()
210                 .addSchemas(schema)
211                 .build();
212 
213         UnsupportedOperationException exception = assertThrows(
214                 UnsupportedOperationException.class,
215                 () -> mDb1.setSchemaAsync(request).get());
216         assertThat(exception).hasMessageThat().contains(Features.SCHEMA_SET_DESCRIPTION
217                 + " is not available on this AppSearch implementation.");
218     }
219 
220     @Test
221     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_DESCRIPTION)  // setDescription
testSetSchema_propertyDescription_notSupported()222     public void testSetSchema_propertyDescription_notSupported() throws Exception {
223         assumeFalse(mDb1.getFeatures().isFeatureSupported(
224                 Features.SCHEMA_SET_DESCRIPTION));
225         AppSearchSchema schema = new AppSearchSchema.Builder("Email1")
226                 .addProperty(new StringPropertyConfig.Builder("body")
227                         .setDescription("Unsupported description")
228                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
229                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
230                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
231                         .build()
232                 ).build();
233 
234         SetSchemaRequest request = new SetSchemaRequest.Builder()
235                 .addSchemas(schema)
236                 .build();
237 
238         UnsupportedOperationException exception = assertThrows(
239                 UnsupportedOperationException.class,
240                 () -> mDb1.setSchemaAsync(request).get());
241         assertThat(exception).hasMessageThat().contains(Features.SCHEMA_SET_DESCRIPTION
242                 + " is not available on this AppSearch implementation.");
243     }
244 
245     @Test
246     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_DESCRIPTION)  // setDescription
testSetSchema_updateSchemaDescription()247     public void testSetSchema_updateSchemaDescription() throws Exception {
248         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_SET_DESCRIPTION));
249 
250         AppSearchSchema schema1 =
251                 new AppSearchSchema.Builder("Email")
252                         .setDescription("A type of electronic message.")
253                         .addProperty(
254                                 new StringPropertyConfig.Builder("subject")
255                                         .setDescription("A summary of the email.")
256                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
257                                         .setIndexingType(
258                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
259                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
260                                         .build())
261                         .addProperty(
262                                 new StringPropertyConfig.Builder("body")
263                                         .setDescription("All of the content of the email.")
264                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
265                                         .setIndexingType(
266                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
267                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
268                                         .build())
269                         .build();
270 
271         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema1).build())
272                 .get();
273 
274         Set<AppSearchSchema> actualSchemaTypes = mDb1.getSchemaAsync().get().getSchemas();
275         assertThat(actualSchemaTypes).containsExactly(schema1);
276 
277         // Change the type description.
278         AppSearchSchema schema2 =
279                 new AppSearchSchema.Builder("Email")
280                         .setDescription("Like mail but with an 'a'.")
281                         .addProperty(
282                                 new StringPropertyConfig.Builder("subject")
283                                         .setDescription("A summary of the email.")
284                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
285                                         .setIndexingType(
286                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
287                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
288                                         .build())
289                         .addProperty(
290                                 new StringPropertyConfig.Builder("body")
291                                         .setDescription("All of the content of the email.")
292                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
293                                         .setIndexingType(
294                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
295                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
296                                         .build())
297                         .build();
298 
299         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema2).build())
300                 .get();
301 
302         GetSchemaResponse getSchemaResponse = mDb1.getSchemaAsync().get();
303         assertThat(getSchemaResponse.getSchemas()).containsExactly(schema2);
304     }
305 
306     @Test
307     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_DESCRIPTION)  // setDescription
testSetSchema_updatePropertyDescription()308     public void testSetSchema_updatePropertyDescription() throws Exception {
309         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_SET_DESCRIPTION));
310 
311         AppSearchSchema schema1 =
312                 new AppSearchSchema.Builder("Email")
313                         .setDescription("A type of electronic message.")
314                         .addProperty(
315                                 new StringPropertyConfig.Builder("subject")
316                                         .setDescription("A summary of the email.")
317                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
318                                         .setIndexingType(
319                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
320                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
321                                         .build())
322                         .addProperty(
323                                 new StringPropertyConfig.Builder("body")
324                                         .setDescription("All of the content of the email.")
325                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
326                                         .setIndexingType(
327                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
328                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
329                                         .build())
330                         .build();
331 
332         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema1).build())
333                 .get();
334 
335         Set<AppSearchSchema> actualSchemaTypes = mDb1.getSchemaAsync().get().getSchemas();
336         assertThat(actualSchemaTypes).containsExactly(schema1);
337 
338         // Change the type description.
339         AppSearchSchema schema2 =
340                 new AppSearchSchema.Builder("Email")
341                         .setDescription("A type of electronic message.")
342                         .addProperty(
343                                 new StringPropertyConfig.Builder("subject")
344                                         .setDescription("The most important part of the email.")
345                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
346                                         .setIndexingType(
347                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
348                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
349                                         .build())
350                         .addProperty(
351                                 new StringPropertyConfig.Builder("body")
352                                         .setDescription("All the other stuff.")
353                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
354                                         .setIndexingType(
355                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
356                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
357                                         .build())
358                         .build();
359 
360         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema2).build())
361                 .get();
362 
363         GetSchemaResponse getSchemaResponse = mDb1.getSchemaAsync().get();
364         assertThat(getSchemaResponse.getSchemas()).containsExactly(schema2);
365     }
366 
367     @Test
testSetSchema_updateVersion()368     public void testSetSchema_updateVersion() throws Exception {
369         AppSearchSchema schema = new AppSearchSchema.Builder("Email")
370                 .addProperty(new StringPropertyConfig.Builder("subject")
371                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
372                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
373                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
374                         .build()
375                 ).addProperty(new StringPropertyConfig.Builder("body")
376                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
377                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
378                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
379                         .build()
380                 ).build();
381 
382         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema)
383                 .setVersion(1).build()).get();
384 
385         Set<AppSearchSchema> actualSchemaTypes = mDb1.getSchemaAsync().get().getSchemas();
386         assertThat(actualSchemaTypes).containsExactly(schema);
387 
388         // increase version number
389         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema)
390                 .setVersion(2).build()).get();
391 
392         GetSchemaResponse getSchemaResponse = mDb1.getSchemaAsync().get();
393         assertThat(getSchemaResponse.getSchemas()).containsExactly(schema);
394         assertThat(getSchemaResponse.getVersion()).isEqualTo(2);
395     }
396 
397     @Test
testSetSchema_checkVersion()398     public void testSetSchema_checkVersion() throws Exception {
399         AppSearchSchema schema = new AppSearchSchema.Builder("Email")
400                 .addProperty(new StringPropertyConfig.Builder("subject")
401                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
402                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
403                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
404                         .build()
405                 ).addProperty(new StringPropertyConfig.Builder("body")
406                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
407                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
408                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
409                         .build()
410                 ).build();
411 
412         // set different version number to different database.
413         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema)
414                 .setVersion(135).build()).get();
415         mDb2.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema)
416                 .setVersion(246).build()).get();
417 
418 
419         // check the version has been set correctly.
420         GetSchemaResponse getSchemaResponse = mDb1.getSchemaAsync().get();
421         assertThat(getSchemaResponse.getSchemas()).containsExactly(schema);
422         assertThat(getSchemaResponse.getVersion()).isEqualTo(135);
423 
424         getSchemaResponse = mDb2.getSchemaAsync().get();
425         assertThat(getSchemaResponse.getSchemas()).containsExactly(schema);
426         assertThat(getSchemaResponse.getVersion()).isEqualTo(246);
427     }
428 
429     @Test
testSetSchema_addIndexedNestedDocumentProperty()430     public void testSetSchema_addIndexedNestedDocumentProperty() throws Exception {
431         // Create schema with a nested document type
432         // SectionId assignment for 'Person':
433         // - "name": string type, indexed. Section id = 0.
434         // - "worksFor.name": string type, (nested) indexed. Section id = 1.
435         AppSearchSchema personSchema = new AppSearchSchema.Builder("Person")
436                 .addProperty(new StringPropertyConfig.Builder("name")
437                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
438                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
439                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
440                         .build())
441                 .addProperty(new DocumentPropertyConfig.Builder("worksFor", "Organization")
442                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
443                         .setShouldIndexNestedProperties(true)
444                         .build())
445                 .build();
446         AppSearchSchema organizationSchema = new AppSearchSchema.Builder("Organization")
447                 .addProperty(new StringPropertyConfig.Builder("name")
448                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
449                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
450                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
451                         .build())
452                 .build();
453         mDb1.setSchemaAsync(
454                 new SetSchemaRequest.Builder().addSchemas(personSchema, organizationSchema)
455                         .build()).get();
456 
457         // Index documents and verify using getDocuments
458         GenericDocument person = new GenericDocument.Builder<>("namespace", "person1", "Person")
459                 .setPropertyString("name", "John")
460                 .setPropertyDocument("worksFor",
461                         new GenericDocument.Builder<>("namespace", "org1", "Organization")
462                                 .setPropertyString("name", "Google")
463                                 .build())
464                 .build();
465 
466         AppSearchBatchResult<String, Void> putResult =
467                 checkIsBatchResultSuccess(mDb1.putAsync(
468                         new PutDocumentsRequest.Builder().addGenericDocuments(person).build()));
469         assertThat(putResult.getSuccesses()).containsExactly("person1", null);
470         assertThat(putResult.getFailures()).isEmpty();
471 
472         GetByDocumentIdRequest getByDocumentIdRequest =
473                 new GetByDocumentIdRequest.Builder("namespace")
474                         .addIds("person1")
475                         .build();
476         List<GenericDocument> outDocuments = doGet(mDb1, getByDocumentIdRequest);
477         assertThat(outDocuments).hasSize(1);
478         assertThat(outDocuments).containsExactly(person);
479 
480         // Verify search using property filter
481         SearchResults searchResults = mDb1.search("worksFor.name:Google", new SearchSpec.Builder()
482                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
483                 .build());
484         outDocuments = convertSearchResultsToDocuments(searchResults);
485         assertThat(outDocuments).hasSize(1);
486         assertThat(outDocuments).containsExactly(person);
487 
488         // Change the schema to add another nested document property to 'Person'
489         // The added property has 'optional' cardinality, so this change is compatible and indexed
490         // documents should still be searchable.
491         //
492         // New section id assignment for 'Person':
493         // - "almaMater.name", string type, (nested) indexed. Section id = 0
494         // - "name": string type, indexed. Section id = 1
495         // - "worksFor.name": string type, (nested) indexed. Section id = 2
496         AppSearchSchema newPersonSchema = new AppSearchSchema.Builder("Person")
497                 .addProperty(new StringPropertyConfig.Builder("name")
498                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
499                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
500                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
501                         .build())
502                 .addProperty(new DocumentPropertyConfig.Builder("worksFor", "Organization")
503                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
504                         .setShouldIndexNestedProperties(true)
505                         .build())
506                 .addProperty(new DocumentPropertyConfig.Builder("almaMater", "Organization")
507                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
508                         .setShouldIndexNestedProperties(true)
509                         .build())
510                 .build();
511         mDb1.setSchemaAsync(
512                 new SetSchemaRequest.Builder().addSchemas(newPersonSchema, organizationSchema)
513                         .build()).get();
514         Set<AppSearchSchema> outSchemaTypes = mDb1.getSchemaAsync().get().getSchemas();
515         assertThat(outSchemaTypes).containsExactly(newPersonSchema, organizationSchema);
516 
517         getByDocumentIdRequest = new GetByDocumentIdRequest.Builder("namespace")
518                 .addIds("person1")
519                 .build();
520         outDocuments = doGet(mDb1, getByDocumentIdRequest);
521         assertThat(outDocuments).hasSize(1);
522         assertThat(outDocuments).containsExactly(person);
523 
524         // Verify that index rebuild was triggered correctly. The same query "worksFor.name:Google"
525         // should still match the same result.
526         searchResults = mDb1.search("worksFor.name:Google", new SearchSpec.Builder()
527                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
528                 .build());
529         outDocuments = convertSearchResultsToDocuments(searchResults);
530         assertThat(outDocuments).hasSize(1);
531         assertThat(outDocuments).containsExactly(person);
532 
533         // In new_schema the 'name' property is now indexed at section id 1. If searching for
534         // "name:Google" matched the document, this means that index rebuild was not triggered
535         // correctly and Icing is still searching the old index, where 'worksFor.name' was
536         // indexed at section id 1.
537         searchResults = mDb1.search("name:Google", new SearchSpec.Builder()
538                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
539                 .build());
540         outDocuments = convertSearchResultsToDocuments(searchResults);
541         assertThat(outDocuments).isEmpty();
542     }
543 
544     @Test
testSetSchemaWithValidCycle_allowCircularReferences()545     public void testSetSchemaWithValidCycle_allowCircularReferences() throws Exception {
546         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SET_SCHEMA_CIRCULAR_REFERENCES));
547 
548         // Create schema with valid cycle: Person -> Organization -> Person...
549         AppSearchSchema personSchema = new AppSearchSchema.Builder("Person")
550                 .addProperty(new StringPropertyConfig.Builder("name")
551                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
552                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
553                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
554                         .build())
555                 .addProperty(new DocumentPropertyConfig.Builder("worksFor", "Organization")
556                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
557                         .setShouldIndexNestedProperties(true)
558                         .build())
559                 .build();
560         AppSearchSchema organizationSchema = new AppSearchSchema.Builder("Organization")
561                 .addProperty(new StringPropertyConfig.Builder("name")
562                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
563                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
564                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
565                         .build())
566                 .addProperty(new DocumentPropertyConfig.Builder("funder", "Person")
567                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
568                         .setShouldIndexNestedProperties(false)
569                         .build())
570                 .build();
571         mDb1.setSchemaAsync(
572                 new SetSchemaRequest.Builder().addSchemas(personSchema, organizationSchema)
573                         .build()).get();
574 
575         // Test that documents following the circular schema are indexable, and that its sections
576         // are searchable
577         GenericDocument person = new GenericDocument.Builder<>("namespace", "person1", "Person")
578                 .setPropertyString("name", "John")
579                 .setPropertyDocument("worksFor")
580                 .build();
581         GenericDocument org = new GenericDocument.Builder<>("namespace", "org1", "Organization")
582                 .setPropertyString("name", "Org")
583                 .setPropertyDocument("funder", person)
584                 .build();
585         GenericDocument person2 = new GenericDocument.Builder<>("namespace", "person2", "Person")
586                 .setPropertyString("name", "Jane")
587                 .setPropertyDocument("worksFor", org)
588                 .build();
589 
590         AppSearchBatchResult<String, Void> putResult =
591                 checkIsBatchResultSuccess(mDb1.putAsync(
592                         new PutDocumentsRequest.Builder().addGenericDocuments(person, org,
593                                 person2).build()));
594         assertThat(putResult.getSuccesses()).containsExactly("person1", null, "org1", null,
595                 "person2", null);
596         assertThat(putResult.getFailures()).isEmpty();
597 
598         GetByDocumentIdRequest getByDocumentIdRequest =
599                 new GetByDocumentIdRequest.Builder("namespace")
600                         .addIds("person1", "person2", "org1")
601                         .build();
602         List<GenericDocument> outDocuments = doGet(mDb1, getByDocumentIdRequest);
603         assertThat(outDocuments).hasSize(3);
604         assertThat(outDocuments).containsExactly(person, person2, org);
605 
606         // Both org1 and person2 should be returned for query "Org"
607         // For org1 this matches the 'name' property and for person2 this matches the
608         // 'worksFor.name' property.
609         SearchResults searchResults = mDb1.search("Org", new SearchSpec.Builder()
610                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
611                 .build());
612         outDocuments = convertSearchResultsToDocuments(searchResults);
613         assertThat(outDocuments).hasSize(2);
614         assertThat(outDocuments).containsExactly(person2, org);
615     }
616 
617     @Test
testSetSchemaWithInvalidCycle_circularReferencesSupported()618     public void testSetSchemaWithInvalidCycle_circularReferencesSupported() throws Exception {
619         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SET_SCHEMA_CIRCULAR_REFERENCES));
620 
621         // Create schema with invalid cycle: Person -> Organization -> Person... where all
622         // DocumentPropertyConfigs have setShouldIndexNestedProperties(true).
623         AppSearchSchema personSchema = new AppSearchSchema.Builder("Person")
624                 .addProperty(new StringPropertyConfig.Builder("name")
625                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
626                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
627                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
628                         .build())
629                 .addProperty(new DocumentPropertyConfig.Builder("worksFor", "Organization")
630                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
631                         .setShouldIndexNestedProperties(true)
632                         .build())
633                 .build();
634         AppSearchSchema organizationSchema = new AppSearchSchema.Builder("Organization")
635                 .addProperty(new StringPropertyConfig.Builder("name")
636                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
637                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
638                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
639                         .build())
640                 .addProperty(new DocumentPropertyConfig.Builder("funder", "Person")
641                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
642                         .setShouldIndexNestedProperties(true)
643                         .build())
644                 .build();
645 
646         SetSchemaRequest setSchemaRequest =
647                 new SetSchemaRequest.Builder().addSchemas(personSchema, organizationSchema).build();
648         ExecutionException executionException =
649                 assertThrows(ExecutionException.class,
650                         () -> mDb1.setSchemaAsync(setSchemaRequest).get());
651         assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
652         AppSearchException exception = (AppSearchException) executionException.getCause();
653         assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT);
654         assertThat(exception).hasMessageThat().containsMatch("Invalid cycle|Infinite loop");
655     }
656 
657     @Test
testSetSchemaWithValidCycle_circularReferencesNotSupported()658     public void testSetSchemaWithValidCycle_circularReferencesNotSupported() {
659         assumeFalse(mDb1.getFeatures().isFeatureSupported(Features.SET_SCHEMA_CIRCULAR_REFERENCES));
660 
661         // Create schema with valid cycle: Person -> Organization -> Person...
662         AppSearchSchema personSchema = new AppSearchSchema.Builder("Person")
663                 .addProperty(new StringPropertyConfig.Builder("name")
664                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
665                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
666                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
667                         .build())
668                 .addProperty(new DocumentPropertyConfig.Builder("worksFor", "Organization")
669                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
670                         .setShouldIndexNestedProperties(true)
671                         .build())
672                 .build();
673         AppSearchSchema organizationSchema = new AppSearchSchema.Builder("Organization")
674                 .addProperty(new StringPropertyConfig.Builder("name")
675                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
676                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
677                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
678                         .build())
679                 .addProperty(new DocumentPropertyConfig.Builder("funder", "Person")
680                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
681                         .setShouldIndexNestedProperties(false)
682                         .build())
683                 .build();
684 
685         SetSchemaRequest setSchemaRequest =
686                 new SetSchemaRequest.Builder().addSchemas(personSchema, organizationSchema).build();
687         ExecutionException executionException =
688                 assertThrows(ExecutionException.class,
689                         () -> mDb1.setSchemaAsync(setSchemaRequest).get());
690         assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
691         AppSearchException exception = (AppSearchException) executionException.getCause();
692         assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT);
693         assertThat(exception).hasMessageThat().containsMatch("Invalid cycle|Infinite loop");
694     }
695 
696 // @exportToFramework:startStrip()
697 
698     @Test
testSetSchema_addDocumentClasses()699     public void testSetSchema_addDocumentClasses() throws Exception {
700         mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
701                 .addDocumentClasses(EmailDocument.class).build()).get();
702     }
703 // @exportToFramework:endStrip()
704 
705     /** Test indexing maximum properties into a schema. */
706     @Test
testSetSchema_maxProperties()707     public void testSetSchema_maxProperties() throws Exception {
708         int maxProperties = mDb1.getFeatures().getMaxIndexedProperties();
709         AppSearchSchema.Builder schemaBuilder = new AppSearchSchema.Builder("testSchema");
710         for (int i = 0; i < maxProperties; i++) {
711             schemaBuilder.addProperty(new StringPropertyConfig.Builder("string" + i)
712                     .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
713                     .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
714                     .build());
715         }
716         AppSearchSchema maxSchema = schemaBuilder.build();
717         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(maxSchema).build()).get();
718         Set<AppSearchSchema> actual1 = mDb1.getSchemaAsync().get().getSchemas();
719         assertThat(actual1).containsExactly(maxSchema);
720 
721         schemaBuilder.addProperty(new StringPropertyConfig.Builder("toomuch")
722                 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
723                 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
724                 .build());
725         ExecutionException exception = assertThrows(ExecutionException.class, () ->
726                 mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
727                         .addSchemas(schemaBuilder.build()).setForceOverride(true).build()).get());
728         Throwable cause = exception.getCause();
729         assertThat(cause).isInstanceOf(AppSearchException.class);
730         assertThat(cause.getMessage()).isEqualTo("Too many properties to be indexed, max "
731                 + "number of properties allowed: " + maxProperties);
732     }
733 
734     @Test
testSetSchema_maxProperties_nestedSchemas()735     public void testSetSchema_maxProperties_nestedSchemas() throws Exception {
736         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SET_SCHEMA_CIRCULAR_REFERENCES));
737 
738         int maxProperties = mDb1.getFeatures().getMaxIndexedProperties();
739         AppSearchSchema.Builder personSchemaBuilder = new AppSearchSchema.Builder("Person");
740         for (int i = 0; i < maxProperties / 3 + 1; i++) {
741             personSchemaBuilder.addProperty(new StringPropertyConfig.Builder("string" + i)
742                     .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
743                     .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
744                     .build());
745         }
746         personSchemaBuilder.addProperty(new DocumentPropertyConfig.Builder("worksFor",
747                 "Organization")
748                 .setShouldIndexNestedProperties(false)
749                 .build());
750         personSchemaBuilder.addProperty(new DocumentPropertyConfig.Builder("address", "Address")
751                 .setShouldIndexNestedProperties(true)
752                 .build());
753 
754         AppSearchSchema.Builder orgSchemaBuilder = new AppSearchSchema.Builder("Organization");
755         for (int i = 0; i < maxProperties / 3; i++) {
756             orgSchemaBuilder.addProperty(new StringPropertyConfig.Builder("string" + i)
757                     .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
758                     .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
759                     .build());
760         }
761         orgSchemaBuilder.addProperty(new DocumentPropertyConfig.Builder("funder", "Person")
762                 .setShouldIndexNestedProperties(true)
763                 .build());
764 
765         AppSearchSchema.Builder addressSchemaBuilder = new AppSearchSchema.Builder("Address");
766         for (int i = 0; i < maxProperties / 3; i++) {
767             addressSchemaBuilder.addProperty(new StringPropertyConfig.Builder("string" + i)
768                     .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
769                     .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
770                     .build());
771         }
772 
773         AppSearchSchema personSchema = personSchemaBuilder.build();
774         AppSearchSchema orgSchema = orgSchemaBuilder.build();
775         AppSearchSchema addressSchema = addressSchemaBuilder.build();
776         mDb1.setSchemaAsync(
777                 new SetSchemaRequest.Builder()
778                         .addSchemas(personSchema, orgSchema, addressSchema)
779                         .build()).get();
780         Set<AppSearchSchema> schemas = mDb1.getSchemaAsync().get().getSchemas();
781         assertThat(schemas).containsExactly(personSchema, orgSchema, addressSchema);
782 
783         // Add one more property to bring the number of sections over the max limit
784         personSchemaBuilder.addProperty(new StringPropertyConfig.Builder("toomuch")
785                 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
786                 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
787                 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
788                 .build());
789         ExecutionException exception = assertThrows(ExecutionException.class,
790                 () -> mDb1.setSchemaAsync(
791                         new SetSchemaRequest.Builder()
792                                 .addSchemas(personSchemaBuilder.build(), orgSchema, addressSchema)
793                                 .setForceOverride(true)
794                                 .build()
795                 ).get());
796         Throwable cause = exception.getCause();
797         assertThat(cause).isInstanceOf(AppSearchException.class);
798         assertThat(cause.getMessage()).contains("Too many properties to be indexed");
799     }
800 
801 // @exportToFramework:startStrip()
802 
803     @Test
testGetSchema()804     public void testGetSchema() throws Exception {
805         AppSearchSchema emailSchema1 = new AppSearchSchema.Builder("Email1")
806                 .addProperty(new StringPropertyConfig.Builder("subject")
807                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
808                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
809                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
810                         .build()
811                 ).addProperty(new StringPropertyConfig.Builder("body")
812                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
813                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
814                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
815                         .build()
816                 ).build();
817         AppSearchSchema emailSchema2 = new AppSearchSchema.Builder("Email2")
818                 .addProperty(new StringPropertyConfig.Builder("subject")
819                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
820                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)  // Diff
821                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
822                         .build()
823                 ).addProperty(new StringPropertyConfig.Builder("body")
824                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
825                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)  // Diff
826                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
827                         .build()
828                 ).build();
829 
830         SetSchemaRequest request1 = new SetSchemaRequest.Builder()
831                 .addSchemas(emailSchema1).addDocumentClasses(EmailDocument.class).build();
832         SetSchemaRequest request2 = new SetSchemaRequest.Builder()
833                 .addSchemas(emailSchema2).addDocumentClasses(EmailDocument.class).build();
834 
835         mDb1.setSchemaAsync(request1).get();
836         mDb2.setSchemaAsync(request2).get();
837 
838         Set<AppSearchSchema> actual1 = mDb1.getSchemaAsync().get().getSchemas();
839         assertThat(actual1).hasSize(2);
840         assertThat(actual1).isEqualTo(request1.getSchemas());
841         Set<AppSearchSchema> actual2 = mDb2.getSchemaAsync().get().getSchemas();
842         assertThat(actual2).hasSize(2);
843         assertThat(actual2).isEqualTo(request2.getSchemas());
844     }
845 // @exportToFramework:endStrip()
846 
847     @Test
testGetSchema_allPropertyTypes()848     public void testGetSchema_allPropertyTypes() throws Exception {
849         AppSearchSchema inSchema = new AppSearchSchema.Builder("Test")
850                 .addProperty(new StringPropertyConfig.Builder("string")
851                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
852                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
853                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
854                         .build())
855                 .addProperty(new AppSearchSchema.LongPropertyConfig.Builder("long")
856                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
857                         .build())
858                 .addProperty(new AppSearchSchema.DoublePropertyConfig.Builder("double")
859                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
860                         .build())
861                 .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder("boolean")
862                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
863                         .build())
864                 .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder("bytes")
865                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
866                         .build())
867                 .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(
868                         "document", AppSearchEmail.SCHEMA_TYPE)
869                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
870                         .setShouldIndexNestedProperties(true)
871                         .build())
872                 .build();
873 
874         // Add it to AppSearch and then obtain it again
875         mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
876                 .addSchemas(inSchema, AppSearchEmail.SCHEMA).build()).get();
877         GetSchemaResponse response = mDb1.getSchemaAsync().get();
878         List<AppSearchSchema> schemas = new ArrayList<>(response.getSchemas());
879         assertThat(schemas).containsExactly(inSchema, AppSearchEmail.SCHEMA);
880         AppSearchSchema outSchema;
881         if (schemas.get(0).getSchemaType().equals("Test")) {
882             outSchema = schemas.get(0);
883         } else {
884             outSchema = schemas.get(1);
885         }
886         assertThat(outSchema.getSchemaType()).isEqualTo("Test");
887         assertThat(outSchema).isNotSameInstanceAs(inSchema);
888 
889         List<PropertyConfig> properties = outSchema.getProperties();
890         assertThat(properties).hasSize(6);
891 
892         assertThat(properties.get(0).getName()).isEqualTo("string");
893         assertThat(properties.get(0).getCardinality())
894                 .isEqualTo(PropertyConfig.CARDINALITY_REQUIRED);
895         assertThat(((StringPropertyConfig) properties.get(0)).getIndexingType())
896                 .isEqualTo(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS);
897         assertThat(((StringPropertyConfig) properties.get(0)).getTokenizerType())
898                 .isEqualTo(StringPropertyConfig.TOKENIZER_TYPE_PLAIN);
899 
900         assertThat(properties.get(1).getName()).isEqualTo("long");
901         assertThat(properties.get(1).getCardinality())
902                 .isEqualTo(PropertyConfig.CARDINALITY_OPTIONAL);
903         assertThat(properties.get(1)).isInstanceOf(AppSearchSchema.LongPropertyConfig.class);
904 
905         assertThat(properties.get(2).getName()).isEqualTo("double");
906         assertThat(properties.get(2).getCardinality())
907                 .isEqualTo(PropertyConfig.CARDINALITY_REPEATED);
908         assertThat(properties.get(2)).isInstanceOf(AppSearchSchema.DoublePropertyConfig.class);
909 
910         assertThat(properties.get(3).getName()).isEqualTo("boolean");
911         assertThat(properties.get(3).getCardinality())
912                 .isEqualTo(PropertyConfig.CARDINALITY_REQUIRED);
913         assertThat(properties.get(3)).isInstanceOf(AppSearchSchema.BooleanPropertyConfig.class);
914 
915         assertThat(properties.get(4).getName()).isEqualTo("bytes");
916         assertThat(properties.get(4).getCardinality())
917                 .isEqualTo(PropertyConfig.CARDINALITY_OPTIONAL);
918         assertThat(properties.get(4)).isInstanceOf(AppSearchSchema.BytesPropertyConfig.class);
919 
920         assertThat(properties.get(5).getName()).isEqualTo("document");
921         assertThat(properties.get(5).getCardinality())
922                 .isEqualTo(PropertyConfig.CARDINALITY_REPEATED);
923         assertThat(((AppSearchSchema.DocumentPropertyConfig) properties.get(5)).getSchemaType())
924                 .isEqualTo(AppSearchEmail.SCHEMA_TYPE);
925         assertThat(((AppSearchSchema.DocumentPropertyConfig) properties.get(5))
926                 .shouldIndexNestedProperties()).isEqualTo(true);
927     }
928 
929     @Test
testGetSchema_visibilitySetting()930     public void testGetSchema_visibilitySetting() throws Exception {
931         assumeTrue(mDb1.getFeatures().isFeatureSupported(
932                 Features.ADD_PERMISSIONS_AND_GET_VISIBILITY));
933         AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email1")
934                 .addProperty(new StringPropertyConfig.Builder("subject")
935                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
936                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
937                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
938                         .build()
939                 ).addProperty(new StringPropertyConfig.Builder("body")
940                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
941                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
942                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
943                         .build()
944                 ).build();
945 
946         byte[] shar256Cert1 = new byte[32];
947         Arrays.fill(shar256Cert1, (byte) 1);
948         byte[] shar256Cert2 = new byte[32];
949         Arrays.fill(shar256Cert2, (byte) 2);
950         PackageIdentifier packageIdentifier1 =
951                 new PackageIdentifier("pkgFoo", shar256Cert1);
952         PackageIdentifier packageIdentifier2 =
953                 new PackageIdentifier("pkgBar", shar256Cert2);
954         SetSchemaRequest request = new SetSchemaRequest.Builder()
955                 .addSchemas(emailSchema)
956                 .setSchemaTypeDisplayedBySystem("Email1", /*displayed=*/false)
957                 .setSchemaTypeVisibilityForPackage("Email1", /*visible=*/true,
958                         packageIdentifier1)
959                 .setSchemaTypeVisibilityForPackage("Email1", /*visible=*/true,
960                         packageIdentifier2)
961                 .addRequiredPermissionsForSchemaTypeVisibility("Email1",
962                         ImmutableSet.of(SetSchemaRequest.READ_SMS, SetSchemaRequest.READ_CALENDAR))
963                 .addRequiredPermissionsForSchemaTypeVisibility("Email1",
964                         ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA))
965                 .build();
966 
967         mDb1.setSchemaAsync(request).get();
968 
969         GetSchemaResponse getSchemaResponse = mDb1.getSchemaAsync().get();
970         Set<AppSearchSchema> actual = getSchemaResponse.getSchemas();
971         assertThat(actual).hasSize(1);
972         assertThat(actual).isEqualTo(request.getSchemas());
973         assertThat(getSchemaResponse.getSchemaTypesNotDisplayedBySystem())
974                 .containsExactly("Email1");
975         assertThat(getSchemaResponse.getSchemaTypesVisibleToPackages())
976                 .containsExactly("Email1", ImmutableSet.of(
977                         packageIdentifier1, packageIdentifier2));
978         assertThat(getSchemaResponse.getRequiredPermissionsForSchemaTypeVisibility())
979                 .containsExactly("Email1", ImmutableSet.of(
980                         ImmutableSet.of(SetSchemaRequest.READ_SMS,
981                                 SetSchemaRequest.READ_CALENDAR),
982                         ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA)));
983     }
984 
985     @Test
testGetSchema_visibilitySetting_oneSharedSchema()986     public void testGetSchema_visibilitySetting_oneSharedSchema() throws Exception {
987         assumeTrue(mDb1.getFeatures().isFeatureSupported(
988                 Features.ADD_PERMISSIONS_AND_GET_VISIBILITY));
989 
990         AppSearchSchema noteSchema = new AppSearchSchema.Builder("Note")
991                 .addProperty(new StringPropertyConfig.Builder("subject").build()).build();
992         SetSchemaRequest.Builder requestBuilder = new SetSchemaRequest.Builder()
993                 .addSchemas(AppSearchEmail.SCHEMA, noteSchema)
994                 .setSchemaTypeDisplayedBySystem(noteSchema.getSchemaType(), false)
995                 .setSchemaTypeVisibilityForPackage(
996                         noteSchema.getSchemaType(),
997                         true,
998                         new PackageIdentifier("com.some.package1", new byte[32]))
999                 .addRequiredPermissionsForSchemaTypeVisibility(
1000                         noteSchema.getSchemaType(),
1001                         Collections.singleton(SetSchemaRequest.READ_SMS));
1002         if (mDb1.getFeatures().isFeatureSupported(
1003                 Features.SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE)) {
1004             requestBuilder.setPubliclyVisibleSchema(
1005                     noteSchema.getSchemaType(),
1006                     new PackageIdentifier("com.some.package2", new byte[32]));
1007         }
1008         SetSchemaRequest request = requestBuilder.build();
1009         mDb1.setSchemaAsync(request).get();
1010 
1011         GetSchemaResponse getSchemaResponse = mDb1.getSchemaAsync().get();
1012         Set<AppSearchSchema> actual = getSchemaResponse.getSchemas();
1013         assertThat(actual).hasSize(2);
1014         assertThat(actual).isEqualTo(request.getSchemas());
1015 
1016         // Check visibility settings. Schemas without settings shouldn't appear in the result at
1017         // all, even with empty maps as values.
1018         assertThat(getSchemaResponse.getSchemaTypesNotDisplayedBySystem())
1019                 .containsExactly(noteSchema.getSchemaType());
1020         assertThat(getSchemaResponse.getSchemaTypesVisibleToPackages())
1021                 .containsExactly(
1022                         noteSchema.getSchemaType(),
1023                         ImmutableSet.of(new PackageIdentifier("com.some.package1", new byte[32])));
1024         assertThat(getSchemaResponse.getRequiredPermissionsForSchemaTypeVisibility())
1025                 .containsExactly(
1026                         noteSchema.getSchemaType(),
1027                         ImmutableSet.of(ImmutableSet.of(SetSchemaRequest.READ_SMS)));
1028         if (mDb1.getFeatures().isFeatureSupported(
1029                 Features.SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE)) {
1030             assertThat(getSchemaResponse.getPubliclyVisibleSchemas())
1031                     .containsExactly(
1032                             noteSchema.getSchemaType(),
1033                             new PackageIdentifier("com.some.package2", new byte[32]));
1034         }
1035     }
1036 
1037     @Test
testGetSchema_visibilitySetting_notSupported()1038     public void testGetSchema_visibilitySetting_notSupported() throws Exception {
1039         assumeFalse(mDb1.getFeatures().isFeatureSupported(
1040                 Features.ADD_PERMISSIONS_AND_GET_VISIBILITY));
1041         AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email1")
1042                 .addProperty(new StringPropertyConfig.Builder("subject")
1043                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
1044                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
1045                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
1046                         .build()
1047                 ).addProperty(new StringPropertyConfig.Builder("body")
1048                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
1049                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
1050                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
1051                         .build()
1052                 ).build();
1053 
1054         byte[] shar256Cert1 = new byte[32];
1055         Arrays.fill(shar256Cert1, (byte) 1);
1056         byte[] shar256Cert2 = new byte[32];
1057         Arrays.fill(shar256Cert2, (byte) 2);
1058         PackageIdentifier packageIdentifier1 =
1059                 new PackageIdentifier("pkgFoo", shar256Cert1);
1060         PackageIdentifier packageIdentifier2 =
1061                 new PackageIdentifier("pkgBar", shar256Cert2);
1062         SetSchemaRequest request = new SetSchemaRequest.Builder()
1063                 .addSchemas(emailSchema)
1064                 .setSchemaTypeDisplayedBySystem("Email1", /*displayed=*/false)
1065                 .setSchemaTypeVisibilityForPackage("Email1", /*visible=*/true,
1066                         packageIdentifier1)
1067                 .setSchemaTypeVisibilityForPackage("Email1", /*visible=*/true,
1068                         packageIdentifier2)
1069                 .build();
1070 
1071         mDb1.setSchemaAsync(request).get();
1072 
1073         GetSchemaResponse getSchemaResponse = mDb1.getSchemaAsync().get();
1074         Set<AppSearchSchema> actual = getSchemaResponse.getSchemas();
1075         assertThat(actual).hasSize(1);
1076         assertThat(actual).isEqualTo(request.getSchemas());
1077         assertThrows(
1078                 UnsupportedOperationException.class,
1079                 () -> getSchemaResponse.getSchemaTypesNotDisplayedBySystem());
1080         assertThrows(
1081                 UnsupportedOperationException.class,
1082                 () -> getSchemaResponse.getSchemaTypesVisibleToPackages());
1083         assertThrows(
1084                 UnsupportedOperationException.class,
1085                 () -> getSchemaResponse.getRequiredPermissionsForSchemaTypeVisibility());
1086     }
1087 
1088     @Test
testSetSchema_visibilitySettingPermission_notSupported()1089     public void testSetSchema_visibilitySettingPermission_notSupported() {
1090         assumeFalse(mDb1.getFeatures().isFeatureSupported(
1091                 Features.ADD_PERMISSIONS_AND_GET_VISIBILITY));
1092         AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email1").build();
1093 
1094         SetSchemaRequest request = new SetSchemaRequest.Builder()
1095                 .addSchemas(emailSchema)
1096                 .setSchemaTypeDisplayedBySystem("Email1", /*displayed=*/false)
1097                 .addRequiredPermissionsForSchemaTypeVisibility("Email1",
1098                         ImmutableSet.of(SetSchemaRequest.READ_SMS))
1099                 .build();
1100 
1101         assertThrows(UnsupportedOperationException.class, () ->
1102                 mDb1.setSchemaAsync(request).get());
1103     }
1104 
1105     @Test
testSetSchema_publiclyVisible()1106     public void testSetSchema_publiclyVisible() throws Exception {
1107         assumeTrue(mDb1.getFeatures()
1108                 .isFeatureSupported(Features.SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE));
1109 
1110         PackageIdentifier pkg = new PackageIdentifier(mContext.getPackageName(), new byte[32]);
1111         SetSchemaRequest request = new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA)
1112                 .setPubliclyVisibleSchema("builtin:Email", pkg).build();
1113 
1114         mDb1.setSchemaAsync(request).get();
1115         GetSchemaResponse getSchemaResponse = mDb1.getSchemaAsync().get();
1116 
1117         assertThat(getSchemaResponse.getSchemas()).containsExactly(AppSearchEmail.SCHEMA);
1118         assertThat(getSchemaResponse.getPubliclyVisibleSchemas())
1119                 .isEqualTo(ImmutableMap.of("builtin:Email", pkg));
1120 
1121         AppSearchEmail email = new AppSearchEmail.Builder("namespace", "id1")
1122                 .setSubject("testPut example").build();
1123 
1124         // mDb1 and mDb2 are in the same package, so we can't REALLY test out public acl. But we
1125         // can make sure they their own documents under the Public ACL.
1126         AppSearchBatchResult<String, Void> putResult =
1127                 checkIsBatchResultSuccess(mDb1.putAsync(
1128                         new PutDocumentsRequest.Builder().addGenericDocuments(email).build()));
1129         assertThat(putResult.getSuccesses()).containsExactly("id1", null);
1130         assertThat(putResult.getFailures()).isEmpty();
1131 
1132         GetByDocumentIdRequest getByDocumentIdRequest =
1133                 new GetByDocumentIdRequest.Builder("namespace")
1134                         .addIds("id1")
1135                         .build();
1136         List<GenericDocument> outDocuments = doGet(mDb1, getByDocumentIdRequest);
1137         assertThat(outDocuments).hasSize(1);
1138         assertThat(outDocuments).containsExactly(email);
1139     }
1140 
1141     @Test
testSetSchema_publiclyVisible_unsupported()1142     public void testSetSchema_publiclyVisible_unsupported() {
1143         assumeFalse(mDb1.getFeatures()
1144                 .isFeatureSupported(Features.SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE));
1145 
1146         SetSchemaRequest request = new SetSchemaRequest.Builder()
1147                 .addSchemas(new AppSearchSchema.Builder("Email").build())
1148                 .setPubliclyVisibleSchema("Email",
1149                         new PackageIdentifier(mContext.getPackageName(), new byte[32])).build();
1150         Exception e = assertThrows(UnsupportedOperationException.class,
1151                 () -> mDb1.setSchemaAsync(request).get());
1152         assertThat(e.getMessage()).isEqualTo("Publicly visible schema are not supported on this "
1153                 + "AppSearch implementation.");
1154     }
1155 
1156     @Test
testSetSchema_visibleToConfig()1157     public void testSetSchema_visibleToConfig() throws Exception {
1158         assumeTrue(mDb1.getFeatures()
1159                 .isFeatureSupported(Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG));
1160         byte[] cert1 = new byte[32];
1161         byte[] cert2 = new byte[32];
1162         Arrays.fill(cert1, (byte) 1);
1163         Arrays.fill(cert2, (byte) 2);
1164         PackageIdentifier pkg1 = new PackageIdentifier("package1", cert1);
1165         PackageIdentifier pkg2 = new PackageIdentifier("package2", cert2);
1166         SchemaVisibilityConfig config1 = new SchemaVisibilityConfig.Builder()
1167                 .setPubliclyVisibleTargetPackage(pkg1)
1168                 .addRequiredPermissions(ImmutableSet.of(1, 2)).build();
1169         SchemaVisibilityConfig config2 = new SchemaVisibilityConfig.Builder()
1170                 .setPubliclyVisibleTargetPackage(pkg2)
1171                 .addRequiredPermissions(ImmutableSet.of(3, 4)).build();
1172         SetSchemaRequest request = new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA)
1173                 .addSchemaTypeVisibleToConfig("builtin:Email", config1)
1174                 .addSchemaTypeVisibleToConfig("builtin:Email", config2)
1175                 .build();
1176         mDb1.setSchemaAsync(request).get();
1177 
1178         GetSchemaResponse getSchemaResponse = mDb1.getSchemaAsync().get();
1179         assertThat(getSchemaResponse.getSchemas()).containsExactly(AppSearchEmail.SCHEMA);
1180         assertThat(getSchemaResponse.getSchemaTypesVisibleToConfigs())
1181                 .isEqualTo(ImmutableMap.of("builtin:Email", ImmutableSet.of(config1, config2)));
1182     }
1183 
1184     @Test
testSetSchema_visibleToConfig_unsupported()1185     public void testSetSchema_visibleToConfig_unsupported() {
1186         assumeFalse(mDb1.getFeatures()
1187                 .isFeatureSupported(Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG));
1188 
1189         SchemaVisibilityConfig config = new SchemaVisibilityConfig.Builder()
1190                 .addRequiredPermissions(ImmutableSet.of(1, 2)).build();
1191         SetSchemaRequest request = new SetSchemaRequest.Builder()
1192                 .addSchemas(new AppSearchSchema.Builder("Email").build())
1193                 .addSchemaTypeVisibleToConfig("Email", config).build();
1194         Exception e = assertThrows(UnsupportedOperationException.class,
1195                 () -> mDb1.setSchemaAsync(request).get());
1196         assertThat(e.getMessage()).isEqualTo("Schema visible to config are not supported on"
1197                 + " this AppSearch implementation.");
1198     }
1199 
1200     @Test
testGetSchema_longPropertyIndexingType()1201     public void testGetSchema_longPropertyIndexingType() throws Exception {
1202         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.NUMERIC_SEARCH));
1203         AppSearchSchema inSchema = new AppSearchSchema.Builder("Test")
1204                 .addProperty(new LongPropertyConfig.Builder("indexableLong")
1205                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
1206                         .setIndexingType(LongPropertyConfig.INDEXING_TYPE_RANGE)
1207                         .build()
1208                 ).addProperty(new LongPropertyConfig.Builder("long")
1209                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
1210                         .setIndexingType(LongPropertyConfig.INDEXING_TYPE_NONE)
1211                         .build()
1212                 ).build();
1213 
1214         SetSchemaRequest request = new SetSchemaRequest.Builder()
1215                 .addSchemas(inSchema).build();
1216 
1217         mDb1.setSchemaAsync(request).get();
1218 
1219         Set<AppSearchSchema> actual = mDb1.getSchemaAsync().get().getSchemas();
1220         assertThat(actual).hasSize(1);
1221         assertThat(actual).containsExactlyElementsIn(request.getSchemas());
1222     }
1223 
1224     @Test
testGetSchema_longPropertyIndexingTypeNone_succeeds()1225     public void testGetSchema_longPropertyIndexingTypeNone_succeeds() throws Exception {
1226         AppSearchSchema inSchema = new AppSearchSchema.Builder("Test")
1227                 .addProperty(new LongPropertyConfig.Builder("long")
1228                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
1229                         .setIndexingType(LongPropertyConfig.INDEXING_TYPE_NONE)
1230                         .build()
1231                 ).build();
1232 
1233         SetSchemaRequest request = new SetSchemaRequest.Builder()
1234                 .addSchemas(inSchema).build();
1235 
1236         mDb1.setSchemaAsync(request).get();
1237 
1238         Set<AppSearchSchema> actual = mDb1.getSchemaAsync().get().getSchemas();
1239         assertThat(actual).hasSize(1);
1240         assertThat(actual).containsExactlyElementsIn(request.getSchemas());
1241     }
1242 
1243     @Test
testGetSchema_longPropertyIndexingTypeRange_notSupported()1244     public void testGetSchema_longPropertyIndexingTypeRange_notSupported() throws Exception {
1245         assumeFalse(mDb1.getFeatures().isFeatureSupported(Features.NUMERIC_SEARCH));
1246         AppSearchSchema inSchema = new AppSearchSchema.Builder("Test")
1247                 .addProperty(new LongPropertyConfig.Builder("indexableLong")
1248                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
1249                         .setIndexingType(LongPropertyConfig.INDEXING_TYPE_RANGE)
1250                         .build()
1251                 ).build();
1252 
1253         SetSchemaRequest request = new SetSchemaRequest.Builder()
1254                 .addSchemas(inSchema).build();
1255 
1256         UnsupportedOperationException e = assertThrows(UnsupportedOperationException.class, () ->
1257                 mDb1.setSchemaAsync(request).get());
1258         assertThat(e.getMessage()).isEqualTo("LongProperty.INDEXING_TYPE_RANGE is not "
1259                 + "supported on this AppSearch implementation.");
1260     }
1261 
1262     @Test
testGetSchema_joinableValueType()1263     public void testGetSchema_joinableValueType() throws Exception {
1264         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
1265         AppSearchSchema inSchema = new AppSearchSchema.Builder("Test")
1266                 .addProperty(new StringPropertyConfig.Builder("normalStr")
1267                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
1268                         .build()
1269                 ).addProperty(new StringPropertyConfig.Builder("optionalQualifiedIdStr")
1270                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
1271                         .setJoinableValueType(StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID)
1272                         .build()
1273                 ).addProperty(new StringPropertyConfig.Builder("requiredQualifiedIdStr")
1274                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
1275                         .setJoinableValueType(StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID)
1276                         .build()
1277                 ).build();
1278 
1279         SetSchemaRequest request = new SetSchemaRequest.Builder()
1280                 .addSchemas(inSchema).build();
1281 
1282         mDb1.setSchemaAsync(request).get();
1283 
1284         Set<AppSearchSchema> actual = mDb1.getSchemaAsync().get().getSchemas();
1285         assertThat(actual).hasSize(1);
1286         assertThat(actual).containsExactlyElementsIn(request.getSchemas());
1287     }
1288 
1289     @Test
testGetSchema_joinableValueTypeNone_succeeds()1290     public void testGetSchema_joinableValueTypeNone_succeeds() throws Exception {
1291         AppSearchSchema inSchema = new AppSearchSchema.Builder("Test")
1292                 .addProperty(new StringPropertyConfig.Builder("optionalString")
1293                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
1294                         .setJoinableValueType(StringPropertyConfig.JOINABLE_VALUE_TYPE_NONE)
1295                         .build()
1296                 ).addProperty(new StringPropertyConfig.Builder("requiredString")
1297                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
1298                         .setJoinableValueType(StringPropertyConfig.JOINABLE_VALUE_TYPE_NONE)
1299                         .build()
1300                 ).addProperty(new StringPropertyConfig.Builder("repeatedString")
1301                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
1302                         .setJoinableValueType(StringPropertyConfig.JOINABLE_VALUE_TYPE_NONE)
1303                         .build()
1304                 ).build();
1305 
1306         SetSchemaRequest request = new SetSchemaRequest.Builder()
1307                 .addSchemas(inSchema).build();
1308 
1309         mDb1.setSchemaAsync(request).get();
1310 
1311         Set<AppSearchSchema> actual = mDb1.getSchemaAsync().get().getSchemas();
1312         assertThat(actual).hasSize(1);
1313         assertThat(actual).containsExactlyElementsIn(request.getSchemas());
1314     }
1315 
1316     @Test
testGetSchema_joinableValueTypeQualifiedId_notSupported()1317     public void testGetSchema_joinableValueTypeQualifiedId_notSupported() throws Exception {
1318         assumeFalse(mDb1.getFeatures().isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
1319         AppSearchSchema inSchema = new AppSearchSchema.Builder("Test")
1320                 .addProperty(new StringPropertyConfig.Builder("qualifiedId")
1321                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
1322                         .setJoinableValueType(StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID)
1323                         .build()
1324                 ).build();
1325 
1326         SetSchemaRequest request = new SetSchemaRequest.Builder()
1327                 .addSchemas(inSchema).build();
1328 
1329         UnsupportedOperationException e = assertThrows(UnsupportedOperationException.class, () ->
1330                 mDb1.setSchemaAsync(request).get());
1331         assertThat(e.getMessage()).isEqualTo(
1332                 "StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID is not supported on this "
1333                         + "AppSearch implementation.");
1334     }
1335 
1336     @Test
testGetNamespaces()1337     public void testGetNamespaces() throws Exception {
1338         // Schema registration
1339         mDb1.setSchemaAsync(
1340                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
1341         assertThat(mDb1.getNamespacesAsync().get()).isEmpty();
1342 
1343         // Index a document
1344         checkIsBatchResultSuccess(mDb1.putAsync(new PutDocumentsRequest.Builder()
1345                 .addGenericDocuments(new AppSearchEmail.Builder("namespace1", "id1").build())
1346                 .build()));
1347         assertThat(mDb1.getNamespacesAsync().get()).containsExactly("namespace1");
1348 
1349         // Index additional data
1350         checkIsBatchResultSuccess(mDb1.putAsync(new PutDocumentsRequest.Builder()
1351                 .addGenericDocuments(
1352                         new AppSearchEmail.Builder("namespace2", "id1").build(),
1353                         new AppSearchEmail.Builder("namespace2", "id2").build(),
1354                         new AppSearchEmail.Builder("namespace3", "id1").build())
1355                 .build()));
1356         assertThat(mDb1.getNamespacesAsync().get()).containsExactly(
1357                 "namespace1", "namespace2", "namespace3");
1358 
1359         // Remove namespace2/id2 -- namespace2 should still exist because of namespace2/id1
1360         checkIsBatchResultSuccess(
1361                 mDb1.removeAsync(new RemoveByDocumentIdRequest.Builder("namespace2").addIds(
1362                         "id2").build()));
1363         assertThat(mDb1.getNamespacesAsync().get()).containsExactly(
1364                 "namespace1", "namespace2", "namespace3");
1365 
1366         // Remove namespace2/id1 -- namespace2 should now be gone
1367         checkIsBatchResultSuccess(
1368                 mDb1.removeAsync(new RemoveByDocumentIdRequest.Builder("namespace2").addIds(
1369                         "id1").build()));
1370         assertThat(mDb1.getNamespacesAsync().get()).containsExactly("namespace1", "namespace3");
1371 
1372         // Make sure the list of namespaces is preserved after restart
1373         mDb1.close();
1374         mDb1 = createSearchSessionAsync(DB_NAME_1).get();
1375         assertThat(mDb1.getNamespacesAsync().get()).containsExactly("namespace1", "namespace3");
1376     }
1377 
1378     @Test
testGetNamespaces_dbIsolation()1379     public void testGetNamespaces_dbIsolation() throws Exception {
1380         // Schema registration
1381         mDb1.setSchemaAsync(
1382                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
1383         mDb2.setSchemaAsync(
1384                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
1385         assertThat(mDb1.getNamespacesAsync().get()).isEmpty();
1386         assertThat(mDb2.getNamespacesAsync().get()).isEmpty();
1387 
1388         // Index documents
1389         checkIsBatchResultSuccess(mDb1.putAsync(new PutDocumentsRequest.Builder()
1390                 .addGenericDocuments(new AppSearchEmail.Builder("namespace1_db1", "id1").build())
1391                 .build()));
1392         checkIsBatchResultSuccess(mDb1.putAsync(new PutDocumentsRequest.Builder()
1393                 .addGenericDocuments(new AppSearchEmail.Builder("namespace2_db1", "id1").build())
1394                 .build()));
1395         checkIsBatchResultSuccess(mDb2.putAsync(new PutDocumentsRequest.Builder()
1396                 .addGenericDocuments(new AppSearchEmail.Builder("namespace_db2", "id1").build())
1397                 .build()));
1398         assertThat(mDb1.getNamespacesAsync().get())
1399                 .containsExactly("namespace1_db1", "namespace2_db1");
1400         assertThat(mDb2.getNamespacesAsync().get()).containsExactly("namespace_db2");
1401 
1402         // Make sure the list of namespaces is preserved after restart
1403         mDb1.close();
1404         mDb1 = createSearchSessionAsync(DB_NAME_1).get();
1405         assertThat(mDb1.getNamespacesAsync().get())
1406                 .containsExactly("namespace1_db1", "namespace2_db1");
1407         assertThat(mDb2.getNamespacesAsync().get()).containsExactly("namespace_db2");
1408     }
1409 
1410     @Test
testGetSchema_emptyDB()1411     public void testGetSchema_emptyDB() throws Exception {
1412         GetSchemaResponse getSchemaResponse = mDb1.getSchemaAsync().get();
1413         assertThat(getSchemaResponse.getVersion()).isEqualTo(0);
1414     }
1415 
1416     @Test
testPutDocuments()1417     public void testPutDocuments() throws Exception {
1418         // Schema registration
1419         mDb1.setSchemaAsync(
1420                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
1421 
1422         // Index a document
1423         AppSearchEmail email = new AppSearchEmail.Builder("namespace", "id1")
1424                 .setFrom("from@example.com")
1425                 .setTo("to1@example.com", "to2@example.com")
1426                 .setSubject("testPut example")
1427                 .setBody("This is the body of the testPut email")
1428                 .build();
1429 
1430         AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb1.putAsync(
1431                 new PutDocumentsRequest.Builder().addGenericDocuments(email).build()));
1432         assertThat(result.getSuccesses()).containsExactly("id1", null);
1433         assertThat(result.getFailures()).isEmpty();
1434     }
1435 
1436     @Test
testPutDocuments_emptyProperties()1437     public void testPutDocuments_emptyProperties() throws Exception {
1438         // Schema registration. Due to b/204677124 is fixed in Android T. We have different
1439         // behaviour when set empty array to bytes and documents between local and platform storage.
1440         // This test only test String, long, boolean and double, for byte array and Document will be
1441         // test in backend's specific test.
1442         AppSearchSchema schema = new AppSearchSchema.Builder("testSchema")
1443                 .addProperty(new StringPropertyConfig.Builder("string")
1444                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
1445                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
1446                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
1447                         .build())
1448                 .addProperty(new AppSearchSchema.LongPropertyConfig.Builder("long")
1449                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
1450                         .build())
1451                 .addProperty(new AppSearchSchema.DoublePropertyConfig.Builder("double")
1452                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
1453                         .build())
1454                 .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder("boolean")
1455                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
1456                         .build())
1457                 .build();
1458         mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
1459                 .addSchemas(schema, AppSearchEmail.SCHEMA).build()).get();
1460 
1461         // Index a document
1462         GenericDocument document = new GenericDocument.Builder<>("namespace", "id1", "testSchema")
1463                 .setPropertyBoolean("boolean")
1464                 .setPropertyString("string")
1465                 .setPropertyDouble("double")
1466                 .setPropertyLong("long")
1467                 .build();
1468 
1469         AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb1.putAsync(
1470                 new PutDocumentsRequest.Builder().addGenericDocuments(document).build()));
1471         assertThat(result.getSuccesses()).containsExactly("id1", null);
1472         assertThat(result.getFailures()).isEmpty();
1473 
1474         GetByDocumentIdRequest request = new GetByDocumentIdRequest.Builder("namespace")
1475                 .addIds("id1")
1476                 .build();
1477         List<GenericDocument> outDocuments = doGet(mDb1, request);
1478         assertThat(outDocuments).hasSize(1);
1479         GenericDocument outDocument = outDocuments.get(0);
1480         assertThat(outDocument.getPropertyBooleanArray("boolean")).isEmpty();
1481         assertThat(outDocument.getPropertyStringArray("string")).isEmpty();
1482         assertThat(outDocument.getPropertyDoubleArray("double")).isEmpty();
1483         assertThat(outDocument.getPropertyLongArray("long")).isEmpty();
1484     }
1485 
1486     @Test
testPutLargeDocumentBatch()1487     public void testPutLargeDocumentBatch() throws Exception {
1488         // Schema registration
1489         AppSearchSchema schema = new AppSearchSchema.Builder("Type").addProperty(
1490                         new StringPropertyConfig.Builder("body")
1491                                 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
1492                                 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
1493                                 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
1494                                 .build())
1495                 .build();
1496         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
1497 
1498         // Creates a large batch of Documents, since we have max document size in Framework which is
1499         // 512KiB, we will create 1KiB * 4000 docs = 4MiB total size > 1MiB binder transaction limit
1500         char[] chars = new char[1024];  // 1KiB
1501         Arrays.fill(chars, ' ');
1502         String body = String.valueOf(chars) + "the end.";
1503         List<GenericDocument> inDocuments = new ArrayList<>();
1504         GetByDocumentIdRequest.Builder getByDocumentIdRequestBuilder =
1505                 new GetByDocumentIdRequest.Builder("namespace");
1506         for (int i = 0; i < 4000; i++) {
1507             GenericDocument inDocument = new GenericDocument.Builder<>(
1508                     "namespace", "id" + i, "Type")
1509                     .setPropertyString("body", body)
1510                     .build();
1511             inDocuments.add(inDocument);
1512             getByDocumentIdRequestBuilder.addIds("id" + i);
1513         }
1514 
1515         // Index documents.
1516         AppSearchBatchResult<String, Void> result =
1517                 mDb1.putAsync(new PutDocumentsRequest.Builder().addGenericDocuments(inDocuments)
1518                         .build()).get();
1519         assertThat(result.isSuccess()).isTrue();
1520 
1521         // Query those documents and verify they are same with the input. This also verify
1522         // AppSearchResult could handle large batch.
1523         SearchResults searchResults = mDb1.search("end", new SearchSpec.Builder()
1524                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
1525                 .setResultCountPerPage(4000)
1526                 .build());
1527         List<GenericDocument> outDocuments = convertSearchResultsToDocuments(searchResults);
1528 
1529         // Create a map to assert the output is same to the input in O(n).
1530         // containsExactlyElementsIn will create two iterators and the complexity is O(n^2).
1531         Map<String, GenericDocument> outMap = new ArrayMap<>(outDocuments.size());
1532         for (int i = 0; i < outDocuments.size(); i++) {
1533             outMap.put(outDocuments.get(i).getId(), outDocuments.get(i));
1534         }
1535         for (int i = 0; i < inDocuments.size(); i++) {
1536             GenericDocument inDocument = inDocuments.get(i);
1537             assertThat(inDocument).isEqualTo(outMap.get(inDocument.getId()));
1538             outMap.remove(inDocument.getId());
1539         }
1540         assertThat(outMap).isEmpty();
1541 
1542         // Get by document ID and verify they are same with the input. This also verify
1543         // AppSearchBatchResult could handle large batch.
1544         AppSearchBatchResult<String, GenericDocument> batchResult = mDb1.getByDocumentIdAsync(
1545                 getByDocumentIdRequestBuilder.build()).get();
1546         assertThat(batchResult.isSuccess()).isTrue();
1547         for (int i = 0; i < inDocuments.size(); i++) {
1548             GenericDocument inDocument = inDocuments.get(i);
1549             assertThat(batchResult.getSuccesses().get(inDocument.getId())).isEqualTo(inDocument);
1550         }
1551     }
1552 
1553 // @exportToFramework:startStrip()
1554 
1555     @Test
testPut_addDocumentClasses()1556     public void testPut_addDocumentClasses() throws Exception {
1557         // Schema registration
1558         mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
1559                 .addDocumentClasses(EmailDocument.class).build()).get();
1560 
1561         // Index a document
1562         EmailDocument email = new EmailDocument();
1563         email.namespace = "namespace";
1564         email.id = "id1";
1565         email.subject = "testPut example";
1566         email.body = "This is the body of the testPut email";
1567 
1568         AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb1.putAsync(
1569                 new PutDocumentsRequest.Builder().addDocuments(email).build()));
1570         assertThat(result.getSuccesses()).containsExactly("id1", null);
1571         assertThat(result.getFailures()).isEmpty();
1572     }
1573 
1574     @Test
testPutDocuments_takenActions()1575     public void testPutDocuments_takenActions() throws Exception {
1576         assumeTrue(mDb1.getFeatures()
1577                 .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
1578 
1579         // Schema registration
1580         mDb1.setSchemaAsync(
1581                         new SetSchemaRequest.Builder()
1582                                 .addDocumentClasses(SearchAction.class, ClickAction.class,
1583                                         ImpressionAction.class, DismissAction.class)
1584                                 .build())
1585                 .get();
1586 
1587         // Put a SearchAction and ClickAction document
1588         SearchAction searchAction =
1589                 new SearchAction.Builder("namespace", "search", /* actionTimestampMillis= */1000)
1590                         .setDocumentTtlMillis(0)
1591                         .setQuery("query")
1592                         .setFetchedResultCount(10)
1593                         .build();
1594         ClickAction clickAction =
1595                 new ClickAction.Builder("namespace", "click", /* actionTimestampMillis= */2000)
1596                         .setDocumentTtlMillis(0)
1597                         .setQuery("query")
1598                         .setReferencedQualifiedId("pkg$db/ns#refId1")
1599                         .setResultRankInBlock(1)
1600                         .setResultRankGlobal(3)
1601                         .setTimeStayOnResultMillis(1024)
1602                         .build();
1603         ImpressionAction impressionAction =
1604                 new ImpressionAction.Builder(
1605                         "namespace", "impression", /* actionTimestampMillis= */3000)
1606                         .setDocumentTtlMillis(0)
1607                         .setQuery("query")
1608                         .setReferencedQualifiedId("pkg$db/ns#refId2")
1609                         .setResultRankInBlock(2)
1610                         .setResultRankGlobal(4)
1611                         .build();
1612         DismissAction dismissAction =
1613                 new DismissAction.Builder(
1614                         "namespace", "dismiss", /* actionTimestampMillis= */4000)
1615                         .setDocumentTtlMillis(0)
1616                         .setQuery("query")
1617                         .setReferencedQualifiedId("pkg$db/ns#refId3")
1618                         .setResultRankInBlock(3)
1619                         .setResultRankGlobal(5)
1620                         .build();
1621 
1622         AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb1.putAsync(
1623                 new PutDocumentsRequest.Builder()
1624                         .addTakenActions(searchAction, clickAction, impressionAction, dismissAction)
1625                         .build()));
1626         assertThat(result.getSuccesses()).containsEntry("search", null);
1627         assertThat(result.getSuccesses()).containsEntry("click", null);
1628         assertThat(result.getSuccesses()).containsEntry("impression", null);
1629         assertThat(result.getSuccesses()).containsEntry("dismiss", null);
1630         assertThat(result.getFailures()).isEmpty();
1631     }
1632 // @exportToFramework:endStrip()
1633 
1634     @Test
testPutDocuments_takenActionGenericDocuments()1635     public void testPutDocuments_takenActionGenericDocuments() throws Exception {
1636         assumeTrue(mDb1.getFeatures()
1637                 .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
1638 
1639         // Schema registration
1640         AppSearchSchema searchActionSchema = new AppSearchSchema.Builder("builtin:SearchAction")
1641                 .addProperty(new LongPropertyConfig.Builder("actionType")
1642                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
1643                         .build())
1644                 .addProperty(new StringPropertyConfig.Builder("query")
1645                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
1646                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
1647                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
1648                         .build()
1649                 ).build();
1650         AppSearchSchema clickActionSchema = new AppSearchSchema.Builder("builtin:ClickAction")
1651                 .addProperty(new LongPropertyConfig.Builder("actionType")
1652                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
1653                         .build())
1654                 .addProperty(new StringPropertyConfig.Builder("query")
1655                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
1656                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
1657                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
1658                         .build()
1659                 ).addProperty(new StringPropertyConfig.Builder("referencedQualifiedId")
1660                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
1661                         .setJoinableValueType(StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID)
1662                         .build()
1663                 ).build();
1664         AppSearchSchema impressionActionSchema =
1665                 new AppSearchSchema.Builder("builtin:ImpressionAction")
1666                         .addProperty(new LongPropertyConfig.Builder("actionType")
1667                                 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
1668                                 .build())
1669                         .addProperty(new StringPropertyConfig.Builder("query")
1670                                 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
1671                                 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
1672                                 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
1673                                 .build()
1674                         ).addProperty(new StringPropertyConfig.Builder("referencedQualifiedId")
1675                                 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
1676                                 .setJoinableValueType(
1677                                         StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID)
1678                                 .build()
1679                         ).build();
1680         AppSearchSchema dismissActionSchema =
1681                 new AppSearchSchema.Builder("builtin:DismissAction")
1682                         .addProperty(new LongPropertyConfig.Builder("actionType")
1683                                 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
1684                                 .build())
1685                         .addProperty(new StringPropertyConfig.Builder("query")
1686                                 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
1687                                 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
1688                                 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
1689                                 .build()
1690                         ).addProperty(new StringPropertyConfig.Builder("referencedQualifiedId")
1691                                 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
1692                                 .setJoinableValueType(
1693                                         StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID)
1694                                 .build()
1695                         ).build();
1696 
1697         mDb1.setSchemaAsync(
1698                 new SetSchemaRequest.Builder().addSchemas(searchActionSchema, clickActionSchema,
1699                                 impressionActionSchema, dismissActionSchema)
1700                         .build()).get();
1701 
1702         // Put search action, click action and impression action generic documents.
1703         GenericDocument searchAction =
1704                 new GenericDocument.Builder<>("namespace", "search", "builtin:SearchAction")
1705                         .setCreationTimestampMillis(1000)
1706                         .setPropertyLong("actionType", ACTION_TYPE_SEARCH)
1707                         .setPropertyString("query", "body")
1708                         .build();
1709         GenericDocument clickAction =
1710                 new GenericDocument.Builder<>("namespace", "click", "builtin:ClickAction")
1711                         .setCreationTimestampMillis(2000)
1712                         .setPropertyLong("actionType", ACTION_TYPE_CLICK)
1713                         .setPropertyString("query", "body")
1714                         .setPropertyString("referencedQualifiedId", "pkg$db/ns#refId1")
1715                         .build();
1716         GenericDocument impressionAction =
1717                 new GenericDocument.Builder<>("namespace", "impression", "builtin:ImpressionAction")
1718                         .setCreationTimestampMillis(3000)
1719                         .setPropertyLong("actionType", ACTION_TYPE_IMPRESSION)
1720                         .setPropertyString("query", "body")
1721                         .setPropertyString("referencedQualifiedId", "pkg$db/ns#refId2")
1722                         .build();
1723         GenericDocument dismissAction =
1724                 new GenericDocument.Builder<>("namespace", "dismiss", "builtin:DismissAction")
1725                         .setCreationTimestampMillis(4000)
1726                         .setPropertyLong("actionType", ACTION_TYPE_DISMISS)
1727                         .setPropertyString("query", "body")
1728                         .setPropertyString("referencedQualifiedId", "pkg$db/ns#refId3")
1729                         .build();
1730 
1731         AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb1.putAsync(
1732                 new PutDocumentsRequest.Builder()
1733                         .addTakenActionGenericDocuments(searchAction, clickAction, impressionAction,
1734                                 dismissAction)
1735                         .build()));
1736         assertThat(result.getSuccesses()).containsEntry("search", null);
1737         assertThat(result.getSuccesses()).containsEntry("click", null);
1738         assertThat(result.getSuccesses()).containsEntry("impression", null);
1739         assertThat(result.getSuccesses()).containsEntry("dismiss", null);
1740         assertThat(result.getFailures()).isEmpty();
1741     }
1742 
1743     @Test
testUpdateSchema()1744     public void testUpdateSchema() throws Exception {
1745         // Schema registration
1746         AppSearchSchema oldEmailSchema = new AppSearchSchema.Builder(AppSearchEmail.SCHEMA_TYPE)
1747                 .addProperty(new StringPropertyConfig.Builder("subject")
1748                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
1749                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
1750                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
1751                         .build())
1752                 .build();
1753         AppSearchSchema newEmailSchema = new AppSearchSchema.Builder(AppSearchEmail.SCHEMA_TYPE)
1754                 .addProperty(new StringPropertyConfig.Builder("subject")
1755                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
1756                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
1757                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
1758                         .build())
1759                 .addProperty(new StringPropertyConfig.Builder("body")
1760                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
1761                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
1762                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
1763                         .build())
1764                 .build();
1765         AppSearchSchema giftSchema = new AppSearchSchema.Builder("Gift")
1766                 .addProperty(new AppSearchSchema.LongPropertyConfig.Builder("price")
1767                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
1768                         .build())
1769                 .build();
1770         mDb1.setSchemaAsync(
1771                 new SetSchemaRequest.Builder().addSchemas(oldEmailSchema).build()).get();
1772 
1773         // Try to index a gift. This should fail as it's not in the schema.
1774         GenericDocument gift =
1775                 new GenericDocument.Builder<>("namespace", "gift1", "Gift").setPropertyLong("price",
1776                         5).build();
1777         AppSearchBatchResult<String, Void> result =
1778                 mDb1.putAsync(
1779                         new PutDocumentsRequest.Builder().addGenericDocuments(gift).build()).get();
1780         assertThat(result.isSuccess()).isFalse();
1781         assertThat(result.getFailures().get("gift1").getResultCode())
1782                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
1783 
1784         // Update the schema to include the gift and update email with a new field
1785         mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
1786                 .addSchemas(newEmailSchema, giftSchema).build()).get();
1787 
1788         // Try to index the document again, which should now work
1789         checkIsBatchResultSuccess(
1790                 mDb1.putAsync(
1791                         new PutDocumentsRequest.Builder().addGenericDocuments(gift).build()));
1792 
1793         // Indexing an email with a body should also work
1794         AppSearchEmail email = new AppSearchEmail.Builder("namespace", "email1")
1795                 .setSubject("testPut example")
1796                 .setBody("This is the body of the testPut email")
1797                 .build();
1798         checkIsBatchResultSuccess(
1799                 mDb1.putAsync(
1800                         new PutDocumentsRequest.Builder().addGenericDocuments(email).build()));
1801     }
1802 
1803     @Test
testRemoveSchema()1804     public void testRemoveSchema() throws Exception {
1805         // Schema registration
1806         AppSearchSchema emailSchema = new AppSearchSchema.Builder(AppSearchEmail.SCHEMA_TYPE)
1807                 .addProperty(new StringPropertyConfig.Builder("subject")
1808                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
1809                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
1810                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
1811                         .build())
1812                 .build();
1813         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(emailSchema).build()).get();
1814 
1815         // Index an email and check it present.
1816         AppSearchEmail email = new AppSearchEmail.Builder("namespace", "email1")
1817                 .setSubject("testPut example")
1818                 .build();
1819         checkIsBatchResultSuccess(
1820                 mDb1.putAsync(
1821                         new PutDocumentsRequest.Builder().addGenericDocuments(email).build()));
1822         List<GenericDocument> outDocuments =
1823                 doGet(mDb1, "namespace", "email1");
1824         assertThat(outDocuments).hasSize(1);
1825         AppSearchEmail outEmail = new AppSearchEmail(outDocuments.get(0));
1826         assertThat(outEmail).isEqualTo(email);
1827 
1828         // Try to remove the email schema. This should fail as it's an incompatible change.
1829         SetSchemaRequest setSchemaRequest = new SetSchemaRequest.Builder().build();
1830         ExecutionException executionException = assertThrows(ExecutionException.class,
1831                 () -> mDb1.setSchemaAsync(setSchemaRequest).get());
1832         assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
1833         AppSearchException failResult1 = (AppSearchException) executionException.getCause();
1834         assertThat(failResult1).hasMessageThat().contains("Schema is incompatible");
1835         assertThat(failResult1).hasMessageThat().contains(
1836                 "Deleted types: {builtin:Email}");
1837 
1838         // Try to remove the email schema again, which should now work as we set forceOverride to
1839         // be true.
1840         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
1841 
1842         // Make sure the indexed email is gone.
1843         AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentIdAsync(
1844                 new GetByDocumentIdRequest.Builder("namespace")
1845                         .addIds("email1")
1846                         .build()).get();
1847         assertThat(getResult.isSuccess()).isFalse();
1848         assertThat(getResult.getFailures().get("email1").getResultCode())
1849                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
1850 
1851         // Try to index an email again. This should fail as the schema has been removed.
1852         AppSearchEmail email2 = new AppSearchEmail.Builder("namespace", "email2")
1853                 .setSubject("testPut example")
1854                 .build();
1855         AppSearchBatchResult<String, Void> failResult2 = mDb1.putAsync(
1856                 new PutDocumentsRequest.Builder().addGenericDocuments(email2).build()).get();
1857         assertThat(failResult2.isSuccess()).isFalse();
1858         assertThat(failResult2.getFailures().get("email2").getErrorMessage())
1859                 .isEqualTo("Schema type config '" + mContext.getPackageName() + "$" + DB_NAME_1
1860                         + "/builtin:Email' not found");
1861     }
1862 
1863     @Test
testRemoveSchema_twoDatabases()1864     public void testRemoveSchema_twoDatabases() throws Exception {
1865         // Schema registration in mDb1 and mDb2
1866         AppSearchSchema emailSchema = new AppSearchSchema.Builder(AppSearchEmail.SCHEMA_TYPE)
1867                 .addProperty(new StringPropertyConfig.Builder("subject")
1868                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
1869                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
1870                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
1871                         .build())
1872                 .build();
1873         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(emailSchema).build()).get();
1874         mDb2.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(emailSchema).build()).get();
1875 
1876         // Index an email and check it present in database1.
1877         AppSearchEmail email1 = new AppSearchEmail.Builder("namespace", "email1")
1878                 .setSubject("testPut example")
1879                 .build();
1880         checkIsBatchResultSuccess(
1881                 mDb1.putAsync(
1882                         new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
1883         List<GenericDocument> outDocuments =
1884                 doGet(mDb1, "namespace", "email1");
1885         assertThat(outDocuments).hasSize(1);
1886         AppSearchEmail outEmail = new AppSearchEmail(outDocuments.get(0));
1887         assertThat(outEmail).isEqualTo(email1);
1888 
1889         // Index an email and check it present in database2.
1890         AppSearchEmail email2 = new AppSearchEmail.Builder("namespace", "email2")
1891                 .setSubject("testPut example")
1892                 .build();
1893         checkIsBatchResultSuccess(
1894                 mDb2.putAsync(
1895                         new PutDocumentsRequest.Builder().addGenericDocuments(email2).build()));
1896         outDocuments = doGet(mDb2, "namespace", "email2");
1897         assertThat(outDocuments).hasSize(1);
1898         outEmail = new AppSearchEmail(outDocuments.get(0));
1899         assertThat(outEmail).isEqualTo(email2);
1900 
1901         // Try to remove the email schema in database1. This should fail as it's an incompatible
1902         // change.
1903         SetSchemaRequest setSchemaRequest = new SetSchemaRequest.Builder().build();
1904         ExecutionException executionException = assertThrows(ExecutionException.class,
1905                 () -> mDb1.setSchemaAsync(setSchemaRequest).get());
1906         assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
1907         AppSearchException failResult1 = (AppSearchException) executionException.getCause();
1908         assertThat(failResult1).hasMessageThat().contains("Schema is incompatible");
1909         assertThat(failResult1).hasMessageThat().contains(
1910                 "Deleted types: {builtin:Email}");
1911 
1912         // Try to remove the email schema again, which should now work as we set forceOverride to
1913         // be true.
1914         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
1915 
1916         // Make sure the indexed email is gone in database 1.
1917         AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentIdAsync(
1918                 new GetByDocumentIdRequest.Builder("namespace")
1919                         .addIds("email1").build()).get();
1920         assertThat(getResult.isSuccess()).isFalse();
1921         assertThat(getResult.getFailures().get("email1").getResultCode())
1922                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
1923 
1924         // Try to index an email again. This should fail as the schema has been removed.
1925         AppSearchEmail email3 = new AppSearchEmail.Builder("namespace", "email3")
1926                 .setSubject("testPut example")
1927                 .build();
1928         AppSearchBatchResult<String, Void> failResult2 = mDb1.putAsync(
1929                 new PutDocumentsRequest.Builder().addGenericDocuments(email3).build()).get();
1930         assertThat(failResult2.isSuccess()).isFalse();
1931         assertThat(failResult2.getFailures().get("email3").getErrorMessage())
1932                 .isEqualTo("Schema type config '" + mContext.getPackageName() + "$" + DB_NAME_1
1933                         + "/builtin:Email' not found");
1934 
1935         // Make sure email in database 2 still present.
1936         outDocuments = doGet(mDb2, "namespace", "email2");
1937         assertThat(outDocuments).hasSize(1);
1938         outEmail = new AppSearchEmail(outDocuments.get(0));
1939         assertThat(outEmail).isEqualTo(email2);
1940 
1941         // Make sure email could still be indexed in database 2.
1942         checkIsBatchResultSuccess(
1943                 mDb2.putAsync(
1944                         new PutDocumentsRequest.Builder().addGenericDocuments(email2).build()));
1945     }
1946 
1947     @Test
testGetDocuments()1948     public void testGetDocuments() throws Exception {
1949         // Schema registration
1950         mDb1.setSchemaAsync(
1951                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
1952 
1953         // Index a document
1954         AppSearchEmail inEmail =
1955                 new AppSearchEmail.Builder("namespace", "id1")
1956                         .setFrom("from@example.com")
1957                         .setTo("to1@example.com", "to2@example.com")
1958                         .setSubject("testPut example")
1959                         .setBody("This is the body of the testPut email")
1960                         .build();
1961         checkIsBatchResultSuccess(mDb1.putAsync(
1962                 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
1963 
1964         // Get the document
1965         List<GenericDocument> outDocuments = doGet(mDb1, "namespace", "id1");
1966         assertThat(outDocuments).hasSize(1);
1967         AppSearchEmail outEmail = new AppSearchEmail(outDocuments.get(0));
1968         assertThat(outEmail).isEqualTo(inEmail);
1969 
1970         // Can't get the document in the other instance.
1971         AppSearchBatchResult<String, GenericDocument> failResult = mDb2.getByDocumentIdAsync(
1972                 new GetByDocumentIdRequest.Builder("namespace").addIds(
1973                         "id1").build()).get();
1974         assertThat(failResult.isSuccess()).isFalse();
1975         assertThat(failResult.getFailures().get("id1").getResultCode())
1976                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
1977     }
1978 
1979 // @exportToFramework:startStrip()
1980 
1981     @Test
testGet_addDocumentClasses()1982     public void testGet_addDocumentClasses() throws Exception {
1983         // Schema registration
1984         mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
1985                 .addDocumentClasses(EmailDocument.class).build()).get();
1986 
1987         // Index a document
1988         EmailDocument inEmail = new EmailDocument();
1989         inEmail.namespace = "namespace";
1990         inEmail.id = "id1";
1991         inEmail.subject = "testPut example";
1992         inEmail.body = "This is the body of the testPut inEmail";
1993         checkIsBatchResultSuccess(mDb1.putAsync(
1994                 new PutDocumentsRequest.Builder().addDocuments(inEmail).build()));
1995 
1996         // Get the document
1997         List<GenericDocument> outDocuments = doGet(mDb1, "namespace", "id1");
1998         assertThat(outDocuments).hasSize(1);
1999         EmailDocument outEmail = outDocuments.get(0).toDocumentClass(EmailDocument.class);
2000         assertThat(inEmail.id).isEqualTo(outEmail.id);
2001         assertThat(inEmail.subject).isEqualTo(outEmail.subject);
2002         assertThat(inEmail.body).isEqualTo(outEmail.body);
2003     }
2004 // @exportToFramework:endStrip()
2005 
2006 
2007     @Test
testGetDocuments_projection()2008     public void testGetDocuments_projection() throws Exception {
2009         // Schema registration
2010         mDb1.setSchemaAsync(
2011                 new SetSchemaRequest.Builder()
2012                         .addSchemas(AppSearchEmail.SCHEMA)
2013                         .build()).get();
2014 
2015         // Index two documents
2016         AppSearchEmail email1 =
2017                 new AppSearchEmail.Builder("namespace", "id1")
2018                         .setCreationTimestampMillis(1000)
2019                         .setFrom("from@example.com")
2020                         .setTo("to1@example.com", "to2@example.com")
2021                         .setSubject("testPut example")
2022                         .setBody("This is the body of the testPut email")
2023                         .build();
2024         AppSearchEmail email2 =
2025                 new AppSearchEmail.Builder("namespace", "id2")
2026                         .setCreationTimestampMillis(1000)
2027                         .setFrom("from@example.com")
2028                         .setTo("to1@example.com", "to2@example.com")
2029                         .setSubject("testPut example")
2030                         .setBody("This is the body of the testPut email")
2031                         .build();
2032         checkIsBatchResultSuccess(mDb1.putAsync(
2033                 new PutDocumentsRequest.Builder()
2034                         .addGenericDocuments(email1, email2).build()));
2035 
2036         // Get with type property paths {"Email", ["subject", "to"]}
2037         GetByDocumentIdRequest request = new GetByDocumentIdRequest.Builder("namespace")
2038                 .addIds("id1", "id2")
2039                 .addProjection(
2040                         AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("subject", "to"))
2041                 .build();
2042         List<GenericDocument> outDocuments = doGet(mDb1, request);
2043 
2044         // The two email documents should have been returned with only the "subject" and "to"
2045         // properties.
2046         AppSearchEmail expected1 =
2047                 new AppSearchEmail.Builder("namespace", "id2")
2048                         .setCreationTimestampMillis(1000)
2049                         .setTo("to1@example.com", "to2@example.com")
2050                         .setSubject("testPut example")
2051                         .build();
2052         AppSearchEmail expected2 =
2053                 new AppSearchEmail.Builder("namespace", "id1")
2054                         .setCreationTimestampMillis(1000)
2055                         .setTo("to1@example.com", "to2@example.com")
2056                         .setSubject("testPut example")
2057                         .build();
2058         assertThat(outDocuments).containsExactly(expected1, expected2);
2059     }
2060 
2061     @Test
testGetDocuments_projectionEmpty()2062     public void testGetDocuments_projectionEmpty() throws Exception {
2063         // Schema registration
2064         mDb1.setSchemaAsync(
2065                 new SetSchemaRequest.Builder()
2066                         .addSchemas(AppSearchEmail.SCHEMA)
2067                         .build()).get();
2068 
2069         // Index two documents
2070         AppSearchEmail email1 =
2071                 new AppSearchEmail.Builder("namespace", "id1")
2072                         .setCreationTimestampMillis(1000)
2073                         .setFrom("from@example.com")
2074                         .setTo("to1@example.com", "to2@example.com")
2075                         .setSubject("testPut example")
2076                         .setBody("This is the body of the testPut email")
2077                         .build();
2078         AppSearchEmail email2 =
2079                 new AppSearchEmail.Builder("namespace", "id2")
2080                         .setCreationTimestampMillis(1000)
2081                         .setFrom("from@example.com")
2082                         .setTo("to1@example.com", "to2@example.com")
2083                         .setSubject("testPut example")
2084                         .setBody("This is the body of the testPut email")
2085                         .build();
2086         checkIsBatchResultSuccess(mDb1.putAsync(
2087                 new PutDocumentsRequest.Builder()
2088                         .addGenericDocuments(email1, email2).build()));
2089 
2090         // Get with type property paths {"Email", ["subject", "to"]}
2091         GetByDocumentIdRequest request = new GetByDocumentIdRequest.Builder("namespace").addIds(
2092                 "id1",
2093                 "id2").addProjection(AppSearchEmail.SCHEMA_TYPE, Collections.emptyList()).build();
2094         List<GenericDocument> outDocuments = doGet(mDb1, request);
2095 
2096         // The two email documents should have been returned without any properties.
2097         AppSearchEmail expected1 =
2098                 new AppSearchEmail.Builder("namespace", "id2")
2099                         .setCreationTimestampMillis(1000)
2100                         .build();
2101         AppSearchEmail expected2 =
2102                 new AppSearchEmail.Builder("namespace", "id1")
2103                         .setCreationTimestampMillis(1000)
2104                         .build();
2105         assertThat(outDocuments).containsExactly(expected1, expected2);
2106     }
2107 
2108     @Test
testGetDocuments_projectionNonExistentType()2109     public void testGetDocuments_projectionNonExistentType() throws Exception {
2110         // Schema registration
2111         mDb1.setSchemaAsync(
2112                 new SetSchemaRequest.Builder()
2113                         .addSchemas(AppSearchEmail.SCHEMA)
2114                         .build()).get();
2115 
2116         // Index two documents
2117         AppSearchEmail email1 =
2118                 new AppSearchEmail.Builder("namespace", "id1")
2119                         .setCreationTimestampMillis(1000)
2120                         .setFrom("from@example.com")
2121                         .setTo("to1@example.com", "to2@example.com")
2122                         .setSubject("testPut example")
2123                         .setBody("This is the body of the testPut email")
2124                         .build();
2125         AppSearchEmail email2 =
2126                 new AppSearchEmail.Builder("namespace", "id2")
2127                         .setCreationTimestampMillis(1000)
2128                         .setFrom("from@example.com")
2129                         .setTo("to1@example.com", "to2@example.com")
2130                         .setSubject("testPut example")
2131                         .setBody("This is the body of the testPut email")
2132                         .build();
2133         checkIsBatchResultSuccess(mDb1.putAsync(
2134                 new PutDocumentsRequest.Builder()
2135                         .addGenericDocuments(email1, email2).build()));
2136 
2137         // Get with type property paths {"Email", ["subject", "to"]}
2138         GetByDocumentIdRequest request = new GetByDocumentIdRequest.Builder("namespace")
2139                 .addIds("id1", "id2")
2140                 .addProjection("NonExistentType", Collections.emptyList())
2141                 .addProjection(AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("subject", "to"))
2142                 .build();
2143         List<GenericDocument> outDocuments = doGet(mDb1, request);
2144 
2145         // The two email documents should have been returned with only the "subject" and "to"
2146         // properties.
2147         AppSearchEmail expected1 =
2148                 new AppSearchEmail.Builder("namespace", "id2")
2149                         .setCreationTimestampMillis(1000)
2150                         .setTo("to1@example.com", "to2@example.com")
2151                         .setSubject("testPut example")
2152                         .build();
2153         AppSearchEmail expected2 =
2154                 new AppSearchEmail.Builder("namespace", "id1")
2155                         .setCreationTimestampMillis(1000)
2156                         .setTo("to1@example.com", "to2@example.com")
2157                         .setSubject("testPut example")
2158                         .build();
2159         assertThat(outDocuments).containsExactly(expected1, expected2);
2160     }
2161 
2162     @Test
testGetDocuments_wildcardProjection()2163     public void testGetDocuments_wildcardProjection() throws Exception {
2164         // Schema registration
2165         mDb1.setSchemaAsync(
2166                 new SetSchemaRequest.Builder()
2167                         .addSchemas(AppSearchEmail.SCHEMA)
2168                         .build()).get();
2169 
2170         // Index two documents
2171         AppSearchEmail email1 =
2172                 new AppSearchEmail.Builder("namespace", "id1")
2173                         .setCreationTimestampMillis(1000)
2174                         .setFrom("from@example.com")
2175                         .setTo("to1@example.com", "to2@example.com")
2176                         .setSubject("testPut example")
2177                         .setBody("This is the body of the testPut email")
2178                         .build();
2179         AppSearchEmail email2 =
2180                 new AppSearchEmail.Builder("namespace", "id2")
2181                         .setCreationTimestampMillis(1000)
2182                         .setFrom("from@example.com")
2183                         .setTo("to1@example.com", "to2@example.com")
2184                         .setSubject("testPut example")
2185                         .setBody("This is the body of the testPut email")
2186                         .build();
2187         checkIsBatchResultSuccess(mDb1.putAsync(
2188                 new PutDocumentsRequest.Builder()
2189                         .addGenericDocuments(email1, email2).build()));
2190 
2191         // Get with type property paths {"Email", ["subject", "to"]}
2192         GetByDocumentIdRequest request = new GetByDocumentIdRequest.Builder("namespace")
2193                 .addIds("id1", "id2")
2194                 .addProjection(
2195                         GetByDocumentIdRequest.PROJECTION_SCHEMA_TYPE_WILDCARD,
2196                         ImmutableList.of("subject", "to"))
2197                 .build();
2198         List<GenericDocument> outDocuments = doGet(mDb1, request);
2199 
2200         // The two email documents should have been returned with only the "subject" and "to"
2201         // properties.
2202         AppSearchEmail expected1 =
2203                 new AppSearchEmail.Builder("namespace", "id2")
2204                         .setCreationTimestampMillis(1000)
2205                         .setTo("to1@example.com", "to2@example.com")
2206                         .setSubject("testPut example")
2207                         .build();
2208         AppSearchEmail expected2 =
2209                 new AppSearchEmail.Builder("namespace", "id1")
2210                         .setCreationTimestampMillis(1000)
2211                         .setTo("to1@example.com", "to2@example.com")
2212                         .setSubject("testPut example")
2213                         .build();
2214         assertThat(outDocuments).containsExactly(expected1, expected2);
2215     }
2216 
2217     @Test
testGetDocuments_wildcardProjectionEmpty()2218     public void testGetDocuments_wildcardProjectionEmpty() throws Exception {
2219         // Schema registration
2220         mDb1.setSchemaAsync(
2221                 new SetSchemaRequest.Builder()
2222                         .addSchemas(AppSearchEmail.SCHEMA)
2223                         .build()).get();
2224 
2225         // Index two documents
2226         AppSearchEmail email1 =
2227                 new AppSearchEmail.Builder("namespace", "id1")
2228                         .setCreationTimestampMillis(1000)
2229                         .setFrom("from@example.com")
2230                         .setTo("to1@example.com", "to2@example.com")
2231                         .setSubject("testPut example")
2232                         .setBody("This is the body of the testPut email")
2233                         .build();
2234         AppSearchEmail email2 =
2235                 new AppSearchEmail.Builder("namespace", "id2")
2236                         .setCreationTimestampMillis(1000)
2237                         .setFrom("from@example.com")
2238                         .setTo("to1@example.com", "to2@example.com")
2239                         .setSubject("testPut example")
2240                         .setBody("This is the body of the testPut email")
2241                         .build();
2242         checkIsBatchResultSuccess(mDb1.putAsync(
2243                 new PutDocumentsRequest.Builder()
2244                         .addGenericDocuments(email1, email2).build()));
2245 
2246         // Get with type property paths {"Email", ["subject", "to"]}
2247         GetByDocumentIdRequest request = new GetByDocumentIdRequest.Builder("namespace").addIds(
2248                 "id1",
2249                 "id2").addProjection(GetByDocumentIdRequest.PROJECTION_SCHEMA_TYPE_WILDCARD,
2250                 Collections.emptyList()).build();
2251         List<GenericDocument> outDocuments = doGet(mDb1, request);
2252 
2253         // The two email documents should have been returned without any properties.
2254         AppSearchEmail expected1 =
2255                 new AppSearchEmail.Builder("namespace", "id2")
2256                         .setCreationTimestampMillis(1000)
2257                         .build();
2258         AppSearchEmail expected2 =
2259                 new AppSearchEmail.Builder("namespace", "id1")
2260                         .setCreationTimestampMillis(1000)
2261                         .build();
2262         assertThat(outDocuments).containsExactly(expected1, expected2);
2263     }
2264 
2265     @Test
testGetDocuments_wildcardProjectionNonExistentType()2266     public void testGetDocuments_wildcardProjectionNonExistentType() throws Exception {
2267         // Schema registration
2268         mDb1.setSchemaAsync(
2269                 new SetSchemaRequest.Builder()
2270                         .addSchemas(AppSearchEmail.SCHEMA)
2271                         .build()).get();
2272 
2273         // Index two documents
2274         AppSearchEmail email1 =
2275                 new AppSearchEmail.Builder("namespace", "id1")
2276                         .setCreationTimestampMillis(1000)
2277                         .setFrom("from@example.com")
2278                         .setTo("to1@example.com", "to2@example.com")
2279                         .setSubject("testPut example")
2280                         .setBody("This is the body of the testPut email")
2281                         .build();
2282         AppSearchEmail email2 =
2283                 new AppSearchEmail.Builder("namespace", "id2")
2284                         .setCreationTimestampMillis(1000)
2285                         .setFrom("from@example.com")
2286                         .setTo("to1@example.com", "to2@example.com")
2287                         .setSubject("testPut example")
2288                         .setBody("This is the body of the testPut email")
2289                         .build();
2290         checkIsBatchResultSuccess(mDb1.putAsync(
2291                 new PutDocumentsRequest.Builder()
2292                         .addGenericDocuments(email1, email2).build()));
2293 
2294         // Get with type property paths {"Email", ["subject", "to"]}
2295         GetByDocumentIdRequest request = new GetByDocumentIdRequest.Builder("namespace")
2296                 .addIds("id1", "id2")
2297                 .addProjection("NonExistentType", Collections.emptyList())
2298                 .addProjection(
2299                         GetByDocumentIdRequest.PROJECTION_SCHEMA_TYPE_WILDCARD,
2300                         ImmutableList.of("subject", "to"))
2301                 .build();
2302         List<GenericDocument> outDocuments = doGet(mDb1, request);
2303 
2304         // The two email documents should have been returned with only the "subject" and "to"
2305         // properties.
2306         AppSearchEmail expected1 =
2307                 new AppSearchEmail.Builder("namespace", "id2")
2308                         .setCreationTimestampMillis(1000)
2309                         .setTo("to1@example.com", "to2@example.com")
2310                         .setSubject("testPut example")
2311                         .build();
2312         AppSearchEmail expected2 =
2313                 new AppSearchEmail.Builder("namespace", "id1")
2314                         .setCreationTimestampMillis(1000)
2315                         .setTo("to1@example.com", "to2@example.com")
2316                         .setSubject("testPut example")
2317                         .build();
2318         assertThat(outDocuments).containsExactly(expected1, expected2);
2319     }
2320 
2321     @Test
testQuery()2322     public void testQuery() throws Exception {
2323         // Schema registration
2324         mDb1.setSchemaAsync(
2325                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
2326 
2327         // Index a document
2328         AppSearchEmail inEmail =
2329                 new AppSearchEmail.Builder("namespace", "id1")
2330                         .setFrom("from@example.com")
2331                         .setTo("to1@example.com", "to2@example.com")
2332                         .setSubject("testPut example")
2333                         .setBody("This is the body of the testPut email")
2334                         .build();
2335         checkIsBatchResultSuccess(mDb1.putAsync(
2336                 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
2337 
2338         // Query for the document
2339         SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
2340                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
2341                 .build());
2342         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
2343         assertThat(documents).hasSize(1);
2344         assertThat(documents.get(0)).isEqualTo(inEmail);
2345 
2346         // Multi-term query
2347         searchResults = mDb1.search("body email", new SearchSpec.Builder()
2348                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
2349                 .build());
2350         documents = convertSearchResultsToDocuments(searchResults);
2351         assertThat(documents).hasSize(1);
2352         assertThat(documents.get(0)).isEqualTo(inEmail);
2353     }
2354 
2355     @Test
testQuery_getNextPage()2356     public void testQuery_getNextPage() throws Exception {
2357         // Schema registration
2358         mDb1.setSchemaAsync(
2359                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
2360         Set<AppSearchEmail> emailSet = new HashSet<>();
2361         PutDocumentsRequest.Builder putDocumentsRequestBuilder = new PutDocumentsRequest.Builder();
2362         // Index 31 documents
2363         for (int i = 0; i < 31; i++) {
2364             AppSearchEmail inEmail =
2365                     new AppSearchEmail.Builder("namespace", "id" + i)
2366                             .setFrom("from@example.com")
2367                             .setTo("to1@example.com", "to2@example.com")
2368                             .setSubject("testPut example")
2369                             .setBody("This is the body of the testPut email")
2370                             .build();
2371             emailSet.add(inEmail);
2372             putDocumentsRequestBuilder.addGenericDocuments(inEmail);
2373         }
2374         checkIsBatchResultSuccess(mDb1.putAsync(putDocumentsRequestBuilder.build()));
2375 
2376         // Set number of results per page is 7.
2377         SearchResults searchResults = mDb1.search("body",
2378                 new SearchSpec.Builder()
2379                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
2380                         .setResultCountPerPage(7)
2381                         .build());
2382         List<GenericDocument> documents = new ArrayList<>();
2383 
2384         int pageNumber = 0;
2385         List<SearchResult> results;
2386 
2387         // keep loading next page until it's empty.
2388         do {
2389             results = searchResults.getNextPageAsync().get();
2390             ++pageNumber;
2391             for (SearchResult result : results) {
2392                 documents.add(result.getGenericDocument());
2393             }
2394         } while (results.size() > 0);
2395 
2396         // check all document presents
2397         assertThat(documents).containsExactlyElementsIn(emailSet);
2398         assertThat(pageNumber).isEqualTo(6); // 5 (upper(31/7)) + 1 (final empty page)
2399     }
2400 
2401     @Test
testQueryIndexableLongProperty_numericSearchEnabledSucceeds()2402     public void testQueryIndexableLongProperty_numericSearchEnabledSucceeds() throws Exception {
2403         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.NUMERIC_SEARCH));
2404 
2405         // Schema registration
2406         AppSearchSchema transactionSchema = new AppSearchSchema.Builder("transaction")
2407                 .addProperty(new LongPropertyConfig.Builder("price")
2408                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
2409                         .setIndexingType(LongPropertyConfig.INDEXING_TYPE_RANGE)
2410                         .build()
2411                 ).addProperty(new LongPropertyConfig.Builder("cost")
2412                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
2413                         .setIndexingType(LongPropertyConfig.INDEXING_TYPE_RANGE)
2414                         .build()
2415                 ).build();
2416         mDb1.setSchemaAsync(
2417                 new SetSchemaRequest.Builder().addSchemas(transactionSchema).build()).get();
2418 
2419         // Index some documents
2420         GenericDocument doc1 =
2421                 new GenericDocument.Builder<>("namespace", "id1", "transaction")
2422                         .setPropertyLong("price", 10)
2423                         .setCreationTimestampMillis(1000)
2424                         .build();
2425         GenericDocument doc2 =
2426                 new GenericDocument.Builder<>("namespace", "id2", "transaction")
2427                         .setPropertyLong("price", 25)
2428                         .setCreationTimestampMillis(1000)
2429                         .build();
2430         GenericDocument doc3 =
2431                 new GenericDocument.Builder<>("namespace", "id3", "transaction")
2432                         .setPropertyLong("cost", 2)
2433                         .setCreationTimestampMillis(1000)
2434                         .build();
2435         checkIsBatchResultSuccess(mDb1.putAsync(
2436                 new PutDocumentsRequest.Builder().addGenericDocuments(doc1, doc2, doc3).build()));
2437 
2438         // Query for the document
2439         SearchResults searchResults = mDb1.search("price < 20",
2440                 new SearchSpec.Builder()
2441                         .setNumericSearchEnabled(true)
2442                         .build());
2443         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
2444         assertThat(documents).hasSize(1);
2445         assertThat(documents.get(0)).isEqualTo(doc1);
2446 
2447         searchResults = mDb1.search("price == 25",
2448                 new SearchSpec.Builder()
2449                         .setNumericSearchEnabled(true)
2450                         .build());
2451         documents = convertSearchResultsToDocuments(searchResults);
2452         assertThat(documents).hasSize(1);
2453         assertThat(documents.get(0)).isEqualTo(doc2);
2454 
2455         searchResults = mDb1.search("cost > 2",
2456                 new SearchSpec.Builder()
2457                         .setNumericSearchEnabled(true)
2458                         .build());
2459         documents = convertSearchResultsToDocuments(searchResults);
2460         assertThat(documents).isEmpty();
2461 
2462         searchResults = mDb1.search("cost >= 2",
2463                 new SearchSpec.Builder()
2464                         .setNumericSearchEnabled(true)
2465                         .build());
2466         documents = convertSearchResultsToDocuments(searchResults);
2467         assertThat(documents).hasSize(1);
2468         assertThat(documents.get(0)).isEqualTo(doc3);
2469 
2470         searchResults = mDb1.search("price <= 25",
2471                 new SearchSpec.Builder()
2472                         .setNumericSearchEnabled(true)
2473                         .build());
2474         documents = convertSearchResultsToDocuments(searchResults);
2475         assertThat(documents).hasSize(2);
2476         assertThat(documents.get(0)).isEqualTo(doc2);
2477         assertThat(documents.get(1)).isEqualTo(doc1);
2478     }
2479 
2480     @Test
testQueryIndexableLongProperty_numericSearchNotEnabled()2481     public void testQueryIndexableLongProperty_numericSearchNotEnabled() throws Exception {
2482         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.NUMERIC_SEARCH));
2483 
2484         // Schema registration
2485         AppSearchSchema transactionSchema = new AppSearchSchema.Builder("transaction")
2486                 .addProperty(new LongPropertyConfig.Builder("price")
2487                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
2488                         .setIndexingType(LongPropertyConfig.INDEXING_TYPE_RANGE)
2489                         .build()
2490                 ).build();
2491         mDb1.setSchemaAsync(
2492                 new SetSchemaRequest.Builder().addSchemas(transactionSchema).build()).get();
2493 
2494         // Index some documents
2495         GenericDocument doc =
2496                 new GenericDocument.Builder<>("namespace", "id1", "transaction")
2497                         .setPropertyLong("price", 10)
2498                         .setCreationTimestampMillis(1000)
2499                         .build();
2500         checkIsBatchResultSuccess(mDb1.putAsync(
2501                 new PutDocumentsRequest.Builder().addGenericDocuments(doc).build()));
2502 
2503         // Query for the document
2504         // Use advanced query but disable NUMERIC_SEARCH in the SearchSpec.
2505         SearchResults searchResults = mDb1.search("price < 20",
2506                 new SearchSpec.Builder()
2507                         .setNumericSearchEnabled(false)
2508                         .build());
2509 
2510         ExecutionException executionException = assertThrows(ExecutionException.class,
2511                 () -> searchResults.getNextPageAsync().get());
2512         assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
2513         AppSearchException exception = (AppSearchException) executionException.getCause();
2514         assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT);
2515         assertThat(exception).hasMessageThat().contains("Attempted use of unenabled feature");
2516         assertThat(exception).hasMessageThat().contains(Features.NUMERIC_SEARCH);
2517     }
2518 
2519     @Test
testQuery_relevanceScoring()2520     public void testQuery_relevanceScoring() throws Exception {
2521         // Schema registration
2522         mDb1.setSchemaAsync(
2523                 new SetSchemaRequest.Builder()
2524                         .addSchemas(AppSearchEmail.SCHEMA)
2525                         .build()).get();
2526 
2527         // Index two documents
2528         AppSearchEmail email1 =
2529                 new AppSearchEmail.Builder("namespace", "id1")
2530                         .setCreationTimestampMillis(1000)
2531                         .setFrom("from@example.com")
2532                         .setTo("to1@example.com", "to2@example.com")
2533                         .setSubject("Mary had a little lamb")
2534                         .setBody("A little lamb, little lamb")
2535                         .build();
2536         AppSearchEmail email2 =
2537                 new AppSearchEmail.Builder("namespace", "id2")
2538                         .setCreationTimestampMillis(1000)
2539                         .setFrom("from@example.com")
2540                         .setTo("to1@example.com", "to2@example.com")
2541                         .setSubject("I'm a little teapot")
2542                         .setBody("short and stout. Here is my handle, here is my spout.")
2543                         .build();
2544         checkIsBatchResultSuccess(mDb1.putAsync(
2545                 new PutDocumentsRequest.Builder()
2546                         .addGenericDocuments(email1, email2).build()));
2547 
2548         // Query for "little". It should match both emails.
2549         SearchResults searchResults = mDb1.search("little", new SearchSpec.Builder()
2550                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
2551                 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE)
2552                 .build());
2553         List<SearchResult> results = retrieveAllSearchResults(searchResults);
2554 
2555         // The email1 should be ranked higher because 'little' appears three times in email1 and
2556         // only once in email2.
2557         assertThat(results).hasSize(2);
2558         assertThat(results.get(0).getGenericDocument()).isEqualTo(email1);
2559         assertThat(results.get(0).getRankingSignal()).isGreaterThan(
2560                 results.get(1).getRankingSignal());
2561         assertThat(results.get(1).getGenericDocument()).isEqualTo(email2);
2562         assertThat(results.get(1).getRankingSignal()).isGreaterThan(0);
2563 
2564         // Query for "little OR stout". It should match both emails.
2565         searchResults = mDb1.search("little OR stout", new SearchSpec.Builder()
2566                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
2567                 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE)
2568                 .build());
2569         results = retrieveAllSearchResults(searchResults);
2570 
2571         // The email2 should be ranked higher because 'little' appears once and "stout", which is a
2572         // rarer term, appears once. email1 only has the three 'little' appearances.
2573         assertThat(results).hasSize(2);
2574         assertThat(results.get(0).getGenericDocument()).isEqualTo(email2);
2575         assertThat(results.get(0).getRankingSignal()).isGreaterThan(
2576                 results.get(1).getRankingSignal());
2577         assertThat(results.get(1).getGenericDocument()).isEqualTo(email1);
2578         assertThat(results.get(1).getRankingSignal()).isGreaterThan(0);
2579     }
2580 
2581     @Test
testQuery_advancedRanking()2582     public void testQuery_advancedRanking() throws Exception {
2583         assumeTrue(mDb1.getFeatures().isFeatureSupported(
2584                 Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION));
2585 
2586         // Schema registration
2587         mDb1.setSchemaAsync(
2588                 new SetSchemaRequest.Builder()
2589                         .addSchemas(AppSearchEmail.SCHEMA)
2590                         .build()).get();
2591 
2592         // Index a document
2593         AppSearchEmail inEmail =
2594                 new AppSearchEmail.Builder("namespace", "id1")
2595                         .setFrom("from@example.com")
2596                         .setTo("to1@example.com", "to2@example.com")
2597                         .setSubject("testPut example")
2598                         .setBody("This is the body of the testPut email")
2599                         .setScore(3)
2600                         .build();
2601         checkIsBatchResultSuccess(mDb1.putAsync(
2602                 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
2603 
2604         // Query for the document, and set an advanced ranking expression that evaluates to 6.
2605         SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
2606                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
2607                 // "abs(pow(2, 2) - 6)" should be evaluated to 2.
2608                 // "this.documentScore()" should be evaluated to 3.
2609                 .setRankingStrategy("abs(pow(2, 2) - 6) * this.documentScore()")
2610                 .build());
2611         List<SearchResult> results = retrieveAllSearchResults(searchResults);
2612         assertThat(results).hasSize(1);
2613         assertThat(results.get(0).getGenericDocument()).isEqualTo(inEmail);
2614         assertThat(results.get(0).getRankingSignal()).isEqualTo(6);
2615     }
2616 
2617     @Test
testQuery_advancedRankingWithPropertyWeights()2618     public void testQuery_advancedRankingWithPropertyWeights() throws Exception {
2619         assumeTrue(mDb1.getFeatures().isFeatureSupported(
2620                 Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION));
2621         assumeTrue(mDb1.getFeatures().isFeatureSupported(
2622                 Features.SEARCH_SPEC_PROPERTY_WEIGHTS));
2623 
2624         // Schema registration
2625         mDb1.setSchemaAsync(
2626                 new SetSchemaRequest.Builder()
2627                         .addSchemas(AppSearchEmail.SCHEMA)
2628                         .build()).get();
2629 
2630         // Index a document
2631         AppSearchEmail inEmail =
2632                 new AppSearchEmail.Builder("namespace", "id1")
2633                         .setFrom("test from")
2634                         .setTo("test to")
2635                         .setSubject("subject")
2636                         .setBody("test body")
2637                         .build();
2638         checkIsBatchResultSuccess(mDb1.putAsync(
2639                 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
2640 
2641         // Query for the document, and set an advanced ranking expression that evaluates to 0.7.
2642         SearchResults searchResults = mDb1.search("test", new SearchSpec.Builder()
2643                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
2644                 .setPropertyWeights(AppSearchEmail.SCHEMA_TYPE,
2645                         ImmutableMap.of("from", 0.1, "to", 0.2,
2646                                 "subject", 2.0, "body", 0.4))
2647                 // this.propertyWeights() returns normalized property weights, in which each
2648                 // weight is divided by the maximum weight.
2649                 // As a result, this expression will evaluates to the list {0.1 / 2.0, 0.2 / 2.0,
2650                 // 0.4 / 2.0}, since the matched properties are "from", "to" and "body", and the
2651                 // maximum weight provided is 2.0.
2652                 // Thus, sum(this.propertyWeights()) will be evaluated to 0.05 + 0.1 + 0.2 = 0.35.
2653                 .setRankingStrategy("sum(this.propertyWeights())")
2654                 .build());
2655         List<SearchResult> results = retrieveAllSearchResults(searchResults);
2656         assertThat(results).hasSize(1);
2657         assertThat(results.get(0).getGenericDocument()).isEqualTo(inEmail);
2658         assertThat(results.get(0).getRankingSignal()).isEqualTo(0.35);
2659     }
2660 
2661     @Test
testQuery_advancedRankingWithJoin()2662     public void testQuery_advancedRankingWithJoin() throws Exception {
2663         assumeTrue(mDb1.getFeatures().isFeatureSupported(
2664                 Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION));
2665         assumeTrue(mDb1.getFeatures()
2666                 .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
2667 
2668         // A full example of how join might be used
2669         AppSearchSchema actionSchema = new AppSearchSchema.Builder("ViewAction")
2670                 .addProperty(new StringPropertyConfig.Builder("entityId")
2671                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
2672                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
2673                         .setJoinableValueType(StringPropertyConfig
2674                                 .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
2675                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
2676                         .build()
2677                 ).addProperty(new StringPropertyConfig.Builder("note")
2678                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
2679                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
2680                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
2681                         .build()
2682                 ).build();
2683 
2684         // Schema registration
2685         mDb1.setSchemaAsync(
2686                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA, actionSchema)
2687                         .build()).get();
2688 
2689         // Index a document
2690         AppSearchEmail inEmail =
2691                 new AppSearchEmail.Builder("namespace", "id1")
2692                         .setFrom("from@example.com")
2693                         .setTo("to1@example.com", "to2@example.com")
2694                         .setSubject("testPut example")
2695                         .setBody("This is the body of the testPut email")
2696                         .setScore(1)
2697                         .build();
2698 
2699         String qualifiedId = DocumentIdUtil.createQualifiedId(mContext.getPackageName(), DB_NAME_1,
2700                 "namespace", "id1");
2701         GenericDocument viewAction1 = new GenericDocument.Builder<>("NS", "id2", "ViewAction")
2702                 .setScore(1)
2703                 .setPropertyString("entityId", qualifiedId)
2704                 .setPropertyString("note", "Viewed email on Monday").build();
2705         GenericDocument viewAction2 = new GenericDocument.Builder<>("NS", "id3", "ViewAction")
2706                 .setScore(2)
2707                 .setPropertyString("entityId", qualifiedId)
2708                 .setPropertyString("note", "Viewed email on Tuesday").build();
2709         checkIsBatchResultSuccess(mDb1.putAsync(
2710                 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail, viewAction1,
2711                         viewAction2).build()));
2712 
2713         SearchSpec nestedSearchSpec =
2714                 new SearchSpec.Builder()
2715                         .setRankingStrategy("2 * this.documentScore()")
2716                         .setOrder(SearchSpec.ORDER_ASCENDING)
2717                         .build();
2718 
2719         JoinSpec js = new JoinSpec.Builder("entityId")
2720                 .setNestedSearch("", nestedSearchSpec)
2721                 .build();
2722 
2723         SearchResults searchResults = mDb1.search("body email", new SearchSpec.Builder()
2724                 // this.childrenRankingSignals() evaluates to the list {1 * 2, 2 * 2}.
2725                 // Thus, sum(this.childrenRankingSignals()) evaluates to 6.
2726                 .setRankingStrategy("sum(this.childrenRankingSignals())")
2727                 .setJoinSpec(js)
2728                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
2729                 .build());
2730 
2731         List<SearchResult> sr = searchResults.getNextPageAsync().get();
2732 
2733         assertThat(sr).hasSize(1);
2734         assertThat(sr.get(0).getGenericDocument().getId()).isEqualTo("id1");
2735         assertThat(sr.get(0).getJoinedResults()).hasSize(2);
2736         assertThat(sr.get(0).getJoinedResults().get(0).getGenericDocument()).isEqualTo(viewAction1);
2737         assertThat(sr.get(0).getJoinedResults().get(1).getGenericDocument()).isEqualTo(viewAction2);
2738         assertThat(sr.get(0).getRankingSignal()).isEqualTo(6.0);
2739     }
2740 
2741 // @exportToFramework:startStrip()
2742     @Test
testQueryRankByClickActions_useTakenAction()2743     public void testQueryRankByClickActions_useTakenAction() throws Exception {
2744         assumeTrue(mDb1.getFeatures()
2745                 .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
2746 
2747         // Schema registration
2748         mDb1.setSchemaAsync(
2749                         new SetSchemaRequest.Builder()
2750                                 .addSchemas(AppSearchEmail.SCHEMA)
2751                                 .addDocumentClasses(SearchAction.class, ClickAction.class,
2752                                         ImpressionAction.class, DismissAction.class)
2753                                 .build())
2754                 .get();
2755 
2756         // Index several email documents
2757         AppSearchEmail inEmail1 =
2758                 new AppSearchEmail.Builder("namespace", "email1")
2759                         .setFrom("from@example.com")
2760                         .setTo("to1@example.com", "to2@example.com")
2761                         .setSubject("testPut example")
2762                         .setBody("This is the body of the testPut email")
2763                         .setScore(1)
2764                         .build();
2765         AppSearchEmail inEmail2 =
2766                 new AppSearchEmail.Builder("namespace", "email2")
2767                         .setFrom("from@example.com")
2768                         .setTo("to1@example.com", "to2@example.com")
2769                         .setSubject("testPut example")
2770                         .setBody("This is the body of the testPut email")
2771                         .setScore(1)
2772                         .build();
2773 
2774         String qualifiedId1 = DocumentIdUtil.createQualifiedId(
2775                 mContext.getPackageName(), DB_NAME_1, inEmail1);
2776         String qualifiedId2 = DocumentIdUtil.createQualifiedId(
2777                 mContext.getPackageName(), DB_NAME_1, inEmail2);
2778 
2779         SearchAction searchAction =
2780                 new SearchAction.Builder("namespace", "search1", /* actionTimestampMillis= */1000)
2781                         .setDocumentTtlMillis(0)
2782                         .setQuery("body")
2783                         .setFetchedResultCount(20)
2784                         .build();
2785         ClickAction clickAction1 =
2786                 new ClickAction.Builder("namespace", "click1", /* actionTimestampMillis= */2000)
2787                         .setDocumentTtlMillis(0)
2788                         .setQuery("body")
2789                         .setReferencedQualifiedId(qualifiedId1)
2790                         .setResultRankInBlock(1)
2791                         .setResultRankGlobal(1)
2792                         .setTimeStayOnResultMillis(512)
2793                         .build();
2794         ClickAction clickAction2 =
2795                 new ClickAction.Builder("namespace", "click2", /* actionTimestampMillis= */3000)
2796                         .setDocumentTtlMillis(0)
2797                         .setQuery("body")
2798                         .setReferencedQualifiedId(qualifiedId2)
2799                         .setResultRankInBlock(2)
2800                         .setResultRankGlobal(2)
2801                         .setTimeStayOnResultMillis(128)
2802                         .build();
2803         ClickAction clickAction3 =
2804                 new ClickAction.Builder("namespace", "click3", /* actionTimestampMillis= */4000)
2805                         .setDocumentTtlMillis(0)
2806                         .setQuery("body")
2807                         .setReferencedQualifiedId(qualifiedId1)
2808                         .setResultRankInBlock(2)
2809                         .setResultRankGlobal(2)
2810                         .setTimeStayOnResultMillis(256)
2811                         .build();
2812         ImpressionAction impressionAction1 =
2813                 new ImpressionAction.Builder(
2814                         "namespace", "impression1", /* actionTimestampMillis= */5000)
2815                         .setDocumentTtlMillis(0)
2816                         .setQuery("body")
2817                         .setReferencedQualifiedId(qualifiedId2)
2818                         .setResultRankInBlock(2)
2819                         .setResultRankGlobal(2)
2820                         .build();
2821         DismissAction dismissAction1 =
2822                 new DismissAction.Builder(
2823                         "namespace", "dismiss1", /* actionTimestampMillis= */6000)
2824                         .setDocumentTtlMillis(0)
2825                         .setQuery("body")
2826                         .setReferencedQualifiedId(qualifiedId2)
2827                         .setResultRankInBlock(2)
2828                         .setResultRankGlobal(2)
2829                         .build();
2830 
2831         checkIsBatchResultSuccess(mDb1.putAsync(
2832                 new PutDocumentsRequest.Builder()
2833                         .addGenericDocuments(inEmail1, inEmail2)
2834                         .addTakenActions(searchAction, clickAction1, clickAction2, clickAction3,
2835                                 impressionAction1, dismissAction1)
2836                         .build()));
2837 
2838         SearchSpec nestedSearchSpec =
2839                 new SearchSpec.Builder()
2840                         .setRankingStrategy(SearchSpec.RANKING_STRATEGY_DOCUMENT_SCORE)
2841                         .setOrder(SearchSpec.ORDER_DESCENDING)
2842                         .addFilterDocumentClasses(ClickAction.class)
2843                         .build();
2844 
2845         // Note: SearchSpec.Builder#setMaxJoinedResultCount only limits the number of child
2846         // documents returned. It does not affect the number of child documents that are scored.
2847         JoinSpec js = new JoinSpec.Builder("referencedQualifiedId")
2848                 .setNestedSearch("query:body", nestedSearchSpec)
2849                 .setAggregationScoringStrategy(JoinSpec.AGGREGATION_SCORING_RESULT_COUNT)
2850                 .setMaxJoinedResultCount(0)
2851                 .build();
2852 
2853         // Search "body" for AppSearchEmail documents, ranking by ClickAction signals with
2854         // query = "body".
2855         SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
2856                 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_JOIN_AGGREGATE_SCORE)
2857                 .setOrder(SearchSpec.ORDER_DESCENDING)
2858                 .setJoinSpec(js)
2859                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
2860                 .addFilterSchemas(AppSearchEmail.SCHEMA_TYPE)
2861                 .build());
2862 
2863         List<SearchResult> sr = searchResults.getNextPageAsync().get();
2864 
2865         assertThat(sr).hasSize(2);
2866         assertThat(sr.get(0).getGenericDocument().getId()).isEqualTo("email1");
2867         assertThat(sr.get(0).getRankingSignal()).isEqualTo(2.0);
2868         assertThat(sr.get(1).getGenericDocument().getId()).isEqualTo("email2");
2869         assertThat(sr.get(1).getRankingSignal()).isEqualTo(1.0);
2870     }
2871 
2872     @Test
testQueryRankByImpressionActions_useTakenAction()2873     public void testQueryRankByImpressionActions_useTakenAction() throws Exception {
2874         assumeTrue(mDb1.getFeatures()
2875                 .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
2876 
2877         // Schema registration
2878         mDb1.setSchemaAsync(
2879                         new SetSchemaRequest.Builder()
2880                                 .addSchemas(AppSearchEmail.SCHEMA)
2881                                 .addDocumentClasses(SearchAction.class, ClickAction.class,
2882                                         ImpressionAction.class, DismissAction.class)
2883                                 .build())
2884                 .get();
2885 
2886         // Index several email documents
2887         AppSearchEmail inEmail1 =
2888                 new AppSearchEmail.Builder("namespace", "email1")
2889                         .setFrom("from@example.com")
2890                         .setTo("to1@example.com", "to2@example.com")
2891                         .setSubject("testPut example")
2892                         .setBody("This is the body of the testPut email")
2893                         .setScore(1)
2894                         .build();
2895         AppSearchEmail inEmail2 =
2896                 new AppSearchEmail.Builder("namespace", "email2")
2897                         .setFrom("from@example.com")
2898                         .setTo("to1@example.com", "to2@example.com")
2899                         .setSubject("testPut example")
2900                         .setBody("This is the body of the testPut email")
2901                         .setScore(1)
2902                         .build();
2903 
2904         String qualifiedId1 = DocumentIdUtil.createQualifiedId(
2905                 mContext.getPackageName(), DB_NAME_1, inEmail1);
2906         String qualifiedId2 = DocumentIdUtil.createQualifiedId(
2907                 mContext.getPackageName(), DB_NAME_1, inEmail2);
2908 
2909         SearchAction searchAction =
2910                 new SearchAction.Builder("namespace", "search1", /* actionTimestampMillis= */1000)
2911                         .setDocumentTtlMillis(0)
2912                         .setQuery("body")
2913                         .setFetchedResultCount(20)
2914                         .build();
2915         ClickAction clickAction1 =
2916                 new ClickAction.Builder("namespace", "click1", /* actionTimestampMillis= */2000)
2917                         .setDocumentTtlMillis(0)
2918                         .setQuery("body")
2919                         .setReferencedQualifiedId(qualifiedId1)
2920                         .setResultRankInBlock(1)
2921                         .setResultRankGlobal(1)
2922                         .setTimeStayOnResultMillis(512)
2923                         .build();
2924         ClickAction clickAction2 =
2925                 new ClickAction.Builder("namespace", "click2", /* actionTimestampMillis= */3000)
2926                         .setDocumentTtlMillis(0)
2927                         .setQuery("body")
2928                         .setReferencedQualifiedId(qualifiedId2)
2929                         .setResultRankInBlock(2)
2930                         .setResultRankGlobal(2)
2931                         .setTimeStayOnResultMillis(128)
2932                         .build();
2933         ClickAction clickAction3 =
2934                 new ClickAction.Builder("namespace", "click3", /* actionTimestampMillis= */4000)
2935                         .setDocumentTtlMillis(0)
2936                         .setQuery("body")
2937                         .setReferencedQualifiedId(qualifiedId1)
2938                         .setResultRankInBlock(2)
2939                         .setResultRankGlobal(2)
2940                         .setTimeStayOnResultMillis(256)
2941                         .build();
2942         ImpressionAction impressionAction1 =
2943                 new ImpressionAction.Builder(
2944                         "namespace", "impression1", /* actionTimestampMillis= */5000)
2945                         .setDocumentTtlMillis(0)
2946                         .setQuery("body")
2947                         .setReferencedQualifiedId(qualifiedId2)
2948                         .setResultRankInBlock(2)
2949                         .setResultRankGlobal(2)
2950                         .build();
2951         DismissAction dismissAction1 =
2952                 new DismissAction.Builder(
2953                         "namespace", "dismiss1", /* actionTimestampMillis= */6000)
2954                         .setDocumentTtlMillis(0)
2955                         .setQuery("body")
2956                         .setReferencedQualifiedId(qualifiedId2)
2957                         .setResultRankInBlock(2)
2958                         .setResultRankGlobal(2)
2959                         .build();
2960 
2961         checkIsBatchResultSuccess(mDb1.putAsync(
2962                 new PutDocumentsRequest.Builder()
2963                         .addGenericDocuments(inEmail1, inEmail2)
2964                         .addTakenActions(searchAction, clickAction1, clickAction2, clickAction3,
2965                                 impressionAction1, dismissAction1)
2966                         .build()));
2967 
2968         SearchSpec nestedSearchSpec =
2969                 new SearchSpec.Builder()
2970                         .setRankingStrategy(SearchSpec.RANKING_STRATEGY_DOCUMENT_SCORE)
2971                         .setOrder(SearchSpec.ORDER_DESCENDING)
2972                         .addFilterDocumentClasses(ImpressionAction.class)
2973                         .build();
2974 
2975         // Note: SearchSpec.Builder#setMaxJoinedResultCount only limits the number of child
2976         // documents returned. It does not affect the number of child documents that are scored.
2977         JoinSpec js = new JoinSpec.Builder("referencedQualifiedId")
2978                 .setNestedSearch("query:body", nestedSearchSpec)
2979                 .setAggregationScoringStrategy(JoinSpec.AGGREGATION_SCORING_RESULT_COUNT)
2980                 .setMaxJoinedResultCount(0)
2981                 .build();
2982 
2983         // Search "body" for AppSearchEmail documents, ranking by ImpressionAction signals with
2984         // query = "body".
2985         SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
2986                 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_JOIN_AGGREGATE_SCORE)
2987                 .setOrder(SearchSpec.ORDER_DESCENDING)
2988                 .setJoinSpec(js)
2989                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
2990                 .addFilterSchemas(AppSearchEmail.SCHEMA_TYPE)
2991                 .build());
2992 
2993         List<SearchResult> sr = searchResults.getNextPageAsync().get();
2994 
2995         assertThat(sr).hasSize(2);
2996         assertThat(sr.get(0).getGenericDocument().getId()).isEqualTo("email2");
2997         assertThat(sr.get(0).getRankingSignal()).isEqualTo(1.0);
2998         assertThat(sr.get(1).getGenericDocument().getId()).isEqualTo("email1");
2999         assertThat(sr.get(1).getRankingSignal()).isEqualTo(0.0);
3000     }
3001 
3002     @Test
testQueryRankByDismissActions_useTakenAction()3003     public void testQueryRankByDismissActions_useTakenAction() throws Exception {
3004         assumeTrue(mDb1.getFeatures()
3005                 .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
3006 
3007         // Schema registration
3008         mDb1.setSchemaAsync(
3009                         new SetSchemaRequest.Builder()
3010                                 .addSchemas(AppSearchEmail.SCHEMA)
3011                                 .addDocumentClasses(SearchAction.class, ClickAction.class,
3012                                         ImpressionAction.class, DismissAction.class)
3013                                 .build())
3014                 .get();
3015 
3016         // Index several email documents
3017         AppSearchEmail inEmail1 =
3018                 new AppSearchEmail.Builder("namespace", "email1")
3019                         .setFrom("from@example.com")
3020                         .setTo("to1@example.com", "to2@example.com")
3021                         .setSubject("testPut example")
3022                         .setBody("This is the body of the testPut email")
3023                         .setScore(1)
3024                         .build();
3025         AppSearchEmail inEmail2 =
3026                 new AppSearchEmail.Builder("namespace", "email2")
3027                         .setFrom("from@example.com")
3028                         .setTo("to1@example.com", "to2@example.com")
3029                         .setSubject("testPut example")
3030                         .setBody("This is the body of the testPut email")
3031                         .setScore(1)
3032                         .build();
3033 
3034         String qualifiedId1 = DocumentIdUtil.createQualifiedId(
3035                 mContext.getPackageName(), DB_NAME_1, inEmail1);
3036         String qualifiedId2 = DocumentIdUtil.createQualifiedId(
3037                 mContext.getPackageName(), DB_NAME_1, inEmail2);
3038 
3039         SearchAction searchAction =
3040                 new SearchAction.Builder("namespace", "search1", /* actionTimestampMillis= */1000)
3041                         .setDocumentTtlMillis(0)
3042                         .setQuery("body")
3043                         .setFetchedResultCount(20)
3044                         .build();
3045         ClickAction clickAction1 =
3046                 new ClickAction.Builder("namespace", "click1", /* actionTimestampMillis= */2000)
3047                         .setDocumentTtlMillis(0)
3048                         .setQuery("body")
3049                         .setReferencedQualifiedId(qualifiedId1)
3050                         .setResultRankInBlock(1)
3051                         .setResultRankGlobal(1)
3052                         .setTimeStayOnResultMillis(512)
3053                         .build();
3054         ClickAction clickAction2 =
3055                 new ClickAction.Builder("namespace", "click2", /* actionTimestampMillis= */3000)
3056                         .setDocumentTtlMillis(0)
3057                         .setQuery("body")
3058                         .setReferencedQualifiedId(qualifiedId2)
3059                         .setResultRankInBlock(2)
3060                         .setResultRankGlobal(2)
3061                         .setTimeStayOnResultMillis(128)
3062                         .build();
3063         ClickAction clickAction3 =
3064                 new ClickAction.Builder("namespace", "click3", /* actionTimestampMillis= */4000)
3065                         .setDocumentTtlMillis(0)
3066                         .setQuery("body")
3067                         .setReferencedQualifiedId(qualifiedId1)
3068                         .setResultRankInBlock(2)
3069                         .setResultRankGlobal(2)
3070                         .setTimeStayOnResultMillis(256)
3071                         .build();
3072         ImpressionAction impressionAction1 =
3073                 new ImpressionAction.Builder(
3074                         "namespace", "impression1", /* actionTimestampMillis= */5000)
3075                         .setDocumentTtlMillis(0)
3076                         .setQuery("body")
3077                         .setReferencedQualifiedId(qualifiedId2)
3078                         .setResultRankInBlock(2)
3079                         .setResultRankGlobal(2)
3080                         .build();
3081         DismissAction dismissAction1 =
3082                 new DismissAction.Builder(
3083                         "namespace", "dismiss1", /* actionTimestampMillis= */6000)
3084                         .setDocumentTtlMillis(0)
3085                         .setQuery("body")
3086                         .setReferencedQualifiedId(qualifiedId2)
3087                         .setResultRankInBlock(2)
3088                         .setResultRankGlobal(2)
3089                         .build();
3090         DismissAction dismissAction2 =
3091                 new DismissAction.Builder(
3092                         "namespace", "dismiss2", /* actionTimestampMillis= */7000)
3093                         .setDocumentTtlMillis(0)
3094                         .setQuery("body")
3095                         .setReferencedQualifiedId(qualifiedId2)
3096                         .setResultRankInBlock(2)
3097                         .setResultRankGlobal(2)
3098                         .build();
3099         DismissAction dismissAction3 =
3100                 new DismissAction.Builder(
3101                         "namespace", "dismiss3", /* actionTimestampMillis= */8000)
3102                         .setDocumentTtlMillis(0)
3103                         .setQuery("body")
3104                         .setReferencedQualifiedId(qualifiedId2)
3105                         .setResultRankInBlock(2)
3106                         .setResultRankGlobal(2)
3107                         .build();
3108 
3109         checkIsBatchResultSuccess(mDb1.putAsync(
3110                 new PutDocumentsRequest.Builder()
3111                         .addGenericDocuments(inEmail1, inEmail2)
3112                         .addTakenActions(searchAction, clickAction1, clickAction2, clickAction3,
3113                                 impressionAction1, dismissAction1, dismissAction2, dismissAction3)
3114                         .build()));
3115 
3116         SearchSpec nestedSearchSpec =
3117                 new SearchSpec.Builder()
3118                         .setRankingStrategy(SearchSpec.RANKING_STRATEGY_DOCUMENT_SCORE)
3119                         .setOrder(SearchSpec.ORDER_DESCENDING)
3120                         .addFilterDocumentClasses(DismissAction.class)
3121                         .build();
3122 
3123         // Note: SearchSpec.Builder#setMaxJoinedResultCount only limits the number of child
3124         // documents returned. It does not affect the number of child documents that are scored.
3125         JoinSpec js = new JoinSpec.Builder("referencedQualifiedId")
3126                 .setNestedSearch("query:body", nestedSearchSpec)
3127                 .setMaxJoinedResultCount(0)
3128                 .build();
3129 
3130         // Search "body" for AppSearchEmail documents, ranking by DismissAction signals with
3131         // query = "body" via advanced scoring language syntax to assign negative weights.
3132         SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
3133                 .setRankingStrategy("-len(this.childrenRankingSignals())")
3134                 .setOrder(SearchSpec.ORDER_DESCENDING)
3135                 .setJoinSpec(js)
3136                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
3137                 .addFilterSchemas(AppSearchEmail.SCHEMA_TYPE)
3138                 .build());
3139 
3140         List<SearchResult> sr = searchResults.getNextPageAsync().get();
3141 
3142         assertThat(sr).hasSize(2);
3143         assertThat(sr.get(0).getGenericDocument().getId()).isEqualTo("email1");
3144         assertThat(sr.get(0).getRankingSignal()).isEqualTo(-0.0);
3145         assertThat(sr.get(1).getGenericDocument().getId()).isEqualTo("email2");
3146         assertThat(sr.get(1).getRankingSignal()).isEqualTo(-3.0);
3147 
3148     }
3149 // @exportToFramework:endStrip()
3150 
3151     @Test
testQueryRankByClickActions_useTakenActionGenericDocument()3152     public void testQueryRankByClickActions_useTakenActionGenericDocument() throws Exception {
3153         assumeTrue(mDb1.getFeatures()
3154                 .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
3155 
3156         AppSearchSchema searchActionSchema = new AppSearchSchema.Builder("builtin:SearchAction")
3157                 .addProperty(new LongPropertyConfig.Builder("actionType")
3158                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3159                         .build())
3160                 .addProperty(new StringPropertyConfig.Builder("query")
3161                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3162                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
3163                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
3164                         .build()
3165                 ).build();
3166         AppSearchSchema clickActionSchema = new AppSearchSchema.Builder("builtin:ClickAction")
3167                 .addProperty(new LongPropertyConfig.Builder("actionType")
3168                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3169                         .build())
3170                 .addProperty(new StringPropertyConfig.Builder("query")
3171                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3172                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
3173                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
3174                         .build()
3175                 ).addProperty(new StringPropertyConfig.Builder("referencedQualifiedId")
3176                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3177                         .setJoinableValueType(StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID)
3178                         .build()
3179                 ).build();
3180         AppSearchSchema impressionActionSchema =
3181                 new AppSearchSchema.Builder("builtin:ImpressionAction")
3182                         .addProperty(new LongPropertyConfig.Builder("actionType")
3183                                 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3184                                 .build())
3185                         .addProperty(new StringPropertyConfig.Builder("query")
3186                                 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3187                                 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
3188                                 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
3189                                 .build()
3190                         ).addProperty(new StringPropertyConfig.Builder("referencedQualifiedId")
3191                                 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3192                                 .setJoinableValueType(
3193                                         StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID)
3194                                 .build()
3195                         ).build();
3196         AppSearchSchema dismissActionSchema =
3197                 new AppSearchSchema.Builder("builtin:DismissAction")
3198                         .addProperty(new LongPropertyConfig.Builder("actionType")
3199                                 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3200                                 .build())
3201                         .addProperty(new StringPropertyConfig.Builder("query")
3202                                 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3203                                 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
3204                                 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
3205                                 .build()
3206                         ).addProperty(new StringPropertyConfig.Builder("referencedQualifiedId")
3207                                 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3208                                 .setJoinableValueType(
3209                                         StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID)
3210                                 .build()
3211                         ).build();
3212 
3213         // Schema registration
3214         mDb1.setSchemaAsync(
3215                 new SetSchemaRequest.Builder()
3216                         .addSchemas(AppSearchEmail.SCHEMA, searchActionSchema, clickActionSchema,
3217                                 impressionActionSchema, dismissActionSchema)
3218                         .build())
3219                 .get();
3220 
3221         // Index several email documents
3222         AppSearchEmail inEmail1 =
3223                 new AppSearchEmail.Builder("namespace", "email1")
3224                         .setFrom("from@example.com")
3225                         .setTo("to1@example.com", "to2@example.com")
3226                         .setSubject("testPut example")
3227                         .setBody("This is the body of the testPut email")
3228                         .setScore(1)
3229                         .build();
3230         AppSearchEmail inEmail2 =
3231                 new AppSearchEmail.Builder("namespace", "email2")
3232                         .setFrom("from@example.com")
3233                         .setTo("to1@example.com", "to2@example.com")
3234                         .setSubject("testPut example")
3235                         .setBody("This is the body of the testPut email")
3236                         .setScore(1)
3237                         .build();
3238 
3239         String qualifiedId1 = DocumentIdUtil.createQualifiedId(
3240                 mContext.getPackageName(), DB_NAME_1, inEmail1);
3241         String qualifiedId2 = DocumentIdUtil.createQualifiedId(
3242                 mContext.getPackageName(), DB_NAME_1, inEmail2);
3243 
3244         GenericDocument searchAction =
3245                 new GenericDocument.Builder<>("namespace", "search1", "builtin:SearchAction")
3246                         .setCreationTimestampMillis(1000)
3247                         .setPropertyLong("actionType", ACTION_TYPE_SEARCH)
3248                         .setPropertyString("query", "body")
3249                         .build();
3250         GenericDocument clickAction1 =
3251                 new GenericDocument.Builder<>("namespace", "click1", "builtin:ClickAction")
3252                         .setCreationTimestampMillis(2000)
3253                         .setPropertyLong("actionType", ACTION_TYPE_CLICK)
3254                         .setPropertyString("query", "body")
3255                         .setPropertyString("referencedQualifiedId", qualifiedId1)
3256                         .build();
3257         GenericDocument clickAction2 =
3258                 new GenericDocument.Builder<>("namespace", "click2", "builtin:ClickAction")
3259                         .setCreationTimestampMillis(3000)
3260                         .setPropertyLong("actionType", ACTION_TYPE_CLICK)
3261                         .setPropertyString("query", "body")
3262                         .setPropertyString("referencedQualifiedId", qualifiedId2)
3263                         .build();
3264         GenericDocument clickAction3 =
3265                 new GenericDocument.Builder<>("namespace", "click3", "builtin:ClickAction")
3266                         .setCreationTimestampMillis(4000)
3267                         .setPropertyLong("actionType", ACTION_TYPE_CLICK)
3268                         .setPropertyString("query", "body")
3269                         .setPropertyString("referencedQualifiedId", qualifiedId1)
3270                         .build();
3271         GenericDocument impressionAction1 =
3272                 new GenericDocument.Builder<>(
3273                         "namespace", "impression1", "builtin:ImpressionAction")
3274                         .setCreationTimestampMillis(5000)
3275                         .setPropertyLong("actionType", ACTION_TYPE_IMPRESSION)
3276                         .setPropertyString("query", "body")
3277                         .setPropertyString("referencedQualifiedId", qualifiedId2)
3278                         .build();
3279         GenericDocument dismissAction1 =
3280                 new GenericDocument.Builder<>("namespace", "dismiss1", "builtin:DismissAction")
3281                         .setCreationTimestampMillis(6000)
3282                         .setPropertyLong("actionType", ACTION_TYPE_DISMISS)
3283                         .setPropertyString("query", "body")
3284                         .setPropertyString("referencedQualifiedId", qualifiedId2)
3285                         .build();
3286 
3287         checkIsBatchResultSuccess(mDb1.putAsync(
3288                 new PutDocumentsRequest.Builder()
3289                         .addGenericDocuments(inEmail1, inEmail2)
3290                         .addTakenActionGenericDocuments(
3291                                 searchAction, clickAction1, clickAction2, clickAction3,
3292                                 impressionAction1, dismissAction1)
3293                         .build()));
3294 
3295         SearchSpec nestedSearchSpec =
3296                 new SearchSpec.Builder()
3297                         .setRankingStrategy(SearchSpec.RANKING_STRATEGY_DOCUMENT_SCORE)
3298                         .setOrder(SearchSpec.ORDER_DESCENDING)
3299                         .addFilterSchemas("builtin:ClickAction")
3300                         .build();
3301 
3302         // Note: SearchSpec.Builder#setMaxJoinedResultCount only limits the number of child
3303         // documents returned. It does not affect the number of child documents that are scored.
3304         JoinSpec js = new JoinSpec.Builder("referencedQualifiedId")
3305                 .setNestedSearch("query:body", nestedSearchSpec)
3306                 .setAggregationScoringStrategy(JoinSpec.AGGREGATION_SCORING_RESULT_COUNT)
3307                 .setMaxJoinedResultCount(0)
3308                 .build();
3309 
3310         // Search "body" for AppSearchEmail documents, ranking by ClickAction signals with
3311         // query = "body".
3312         SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
3313                 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_JOIN_AGGREGATE_SCORE)
3314                 .setOrder(SearchSpec.ORDER_DESCENDING)
3315                 .setJoinSpec(js)
3316                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
3317                 .addFilterSchemas(AppSearchEmail.SCHEMA_TYPE)
3318                 .build());
3319 
3320         List<SearchResult> sr = searchResults.getNextPageAsync().get();
3321 
3322         assertThat(sr).hasSize(2);
3323         assertThat(sr.get(0).getGenericDocument().getId()).isEqualTo("email1");
3324         assertThat(sr.get(0).getRankingSignal()).isEqualTo(2.0);
3325         assertThat(sr.get(1).getGenericDocument().getId()).isEqualTo("email2");
3326         assertThat(sr.get(1).getRankingSignal()).isEqualTo(1.0);
3327     }
3328 
3329     @Test
testQueryRankByImpressionActions_useTakenActionGenericDocument()3330     public void testQueryRankByImpressionActions_useTakenActionGenericDocument() throws Exception {
3331         assumeTrue(mDb1.getFeatures()
3332                 .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
3333 
3334         AppSearchSchema searchActionSchema = new AppSearchSchema.Builder("builtin:SearchAction")
3335                 .addProperty(new LongPropertyConfig.Builder("actionType")
3336                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3337                         .build())
3338                 .addProperty(new StringPropertyConfig.Builder("query")
3339                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3340                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
3341                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
3342                         .build()
3343                 ).build();
3344         AppSearchSchema clickActionSchema = new AppSearchSchema.Builder("builtin:ClickAction")
3345                 .addProperty(new LongPropertyConfig.Builder("actionType")
3346                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3347                         .build())
3348                 .addProperty(new StringPropertyConfig.Builder("query")
3349                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3350                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
3351                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
3352                         .build()
3353                 ).addProperty(new StringPropertyConfig.Builder("referencedQualifiedId")
3354                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3355                         .setJoinableValueType(StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID)
3356                         .build()
3357                 ).build();
3358         AppSearchSchema impressionActionSchema =
3359                 new AppSearchSchema.Builder("builtin:ImpressionAction")
3360                         .addProperty(new LongPropertyConfig.Builder("actionType")
3361                                 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3362                                 .build())
3363                         .addProperty(new StringPropertyConfig.Builder("query")
3364                                 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3365                                 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
3366                                 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
3367                                 .build()
3368                         ).addProperty(new StringPropertyConfig.Builder("referencedQualifiedId")
3369                                 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3370                                 .setJoinableValueType(
3371                                         StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID)
3372                                 .build()
3373                         ).build();
3374         AppSearchSchema dismissActionSchema =
3375                 new AppSearchSchema.Builder("builtin:DismissAction")
3376                         .addProperty(new LongPropertyConfig.Builder("actionType")
3377                                 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3378                                 .build())
3379                         .addProperty(new StringPropertyConfig.Builder("query")
3380                                 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3381                                 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
3382                                 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
3383                                 .build()
3384                         ).addProperty(new StringPropertyConfig.Builder("referencedQualifiedId")
3385                                 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3386                                 .setJoinableValueType(
3387                                         StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID)
3388                                 .build()
3389                         ).build();
3390 
3391         // Schema registration
3392         mDb1.setSchemaAsync(
3393                 new SetSchemaRequest.Builder()
3394                         .addSchemas(AppSearchEmail.SCHEMA, searchActionSchema, clickActionSchema,
3395                                 impressionActionSchema, dismissActionSchema)
3396                         .build())
3397                 .get();
3398 
3399         // Index several email documents
3400         AppSearchEmail inEmail1 =
3401                 new AppSearchEmail.Builder("namespace", "email1")
3402                         .setFrom("from@example.com")
3403                         .setTo("to1@example.com", "to2@example.com")
3404                         .setSubject("testPut example")
3405                         .setBody("This is the body of the testPut email")
3406                         .setScore(1)
3407                         .build();
3408         AppSearchEmail inEmail2 =
3409                 new AppSearchEmail.Builder("namespace", "email2")
3410                         .setFrom("from@example.com")
3411                         .setTo("to1@example.com", "to2@example.com")
3412                         .setSubject("testPut example")
3413                         .setBody("This is the body of the testPut email")
3414                         .setScore(1)
3415                         .build();
3416 
3417         String qualifiedId1 = DocumentIdUtil.createQualifiedId(
3418                 mContext.getPackageName(), DB_NAME_1, inEmail1);
3419         String qualifiedId2 = DocumentIdUtil.createQualifiedId(
3420                 mContext.getPackageName(), DB_NAME_1, inEmail2);
3421 
3422         GenericDocument searchAction =
3423                 new GenericDocument.Builder<>("namespace", "search1", "builtin:SearchAction")
3424                         .setCreationTimestampMillis(1000)
3425                         .setPropertyLong("actionType", ACTION_TYPE_SEARCH)
3426                         .setPropertyString("query", "body")
3427                         .build();
3428         GenericDocument clickAction1 =
3429                 new GenericDocument.Builder<>("namespace", "click1", "builtin:ClickAction")
3430                         .setCreationTimestampMillis(2000)
3431                         .setPropertyLong("actionType", ACTION_TYPE_CLICK)
3432                         .setPropertyString("query", "body")
3433                         .setPropertyString("referencedQualifiedId", qualifiedId1)
3434                         .build();
3435         GenericDocument clickAction2 =
3436                 new GenericDocument.Builder<>("namespace", "click2", "builtin:ClickAction")
3437                         .setCreationTimestampMillis(3000)
3438                         .setPropertyLong("actionType", ACTION_TYPE_CLICK)
3439                         .setPropertyString("query", "body")
3440                         .setPropertyString("referencedQualifiedId", qualifiedId2)
3441                         .build();
3442         GenericDocument clickAction3 =
3443                 new GenericDocument.Builder<>("namespace", "click3", "builtin:ClickAction")
3444                         .setCreationTimestampMillis(4000)
3445                         .setPropertyLong("actionType", ACTION_TYPE_CLICK)
3446                         .setPropertyString("query", "body")
3447                         .setPropertyString("referencedQualifiedId", qualifiedId1)
3448                         .build();
3449         GenericDocument impressionAction1 =
3450                 new GenericDocument.Builder<>(
3451                         "namespace", "impression1", "builtin:ImpressionAction")
3452                         .setCreationTimestampMillis(5000)
3453                         .setPropertyLong("actionType", ACTION_TYPE_IMPRESSION)
3454                         .setPropertyString("query", "body")
3455                         .setPropertyString("referencedQualifiedId", qualifiedId2)
3456                         .build();
3457         GenericDocument dismissAction1 =
3458                 new GenericDocument.Builder<>("namespace", "dismiss1", "builtin:DismissAction")
3459                         .setCreationTimestampMillis(6000)
3460                         .setPropertyLong("actionType", ACTION_TYPE_DISMISS)
3461                         .setPropertyString("query", "body")
3462                         .setPropertyString("referencedQualifiedId", qualifiedId2)
3463                         .build();
3464 
3465         checkIsBatchResultSuccess(mDb1.putAsync(
3466                 new PutDocumentsRequest.Builder()
3467                         .addGenericDocuments(inEmail1, inEmail2)
3468                         .addTakenActionGenericDocuments(
3469                                 searchAction, clickAction1, clickAction2, clickAction3,
3470                                 impressionAction1, dismissAction1)
3471                         .build()));
3472 
3473         SearchSpec nestedSearchSpec =
3474                 new SearchSpec.Builder()
3475                         .setRankingStrategy(SearchSpec.RANKING_STRATEGY_DOCUMENT_SCORE)
3476                         .setOrder(SearchSpec.ORDER_DESCENDING)
3477                         .addFilterSchemas("builtin:ImpressionAction")
3478                         .build();
3479 
3480         // Note: SearchSpec.Builder#setMaxJoinedResultCount only limits the number of child
3481         // documents returned. It does not affect the number of child documents that are scored.
3482         JoinSpec js = new JoinSpec.Builder("referencedQualifiedId")
3483                 .setNestedSearch("query:body", nestedSearchSpec)
3484                 .setAggregationScoringStrategy(JoinSpec.AGGREGATION_SCORING_RESULT_COUNT)
3485                 .setMaxJoinedResultCount(0)
3486                 .build();
3487 
3488         // Search "body" for AppSearchEmail documents, ranking by ImpressionAction signals with
3489         // query = "body".
3490         SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
3491                 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_JOIN_AGGREGATE_SCORE)
3492                 .setOrder(SearchSpec.ORDER_DESCENDING)
3493                 .setJoinSpec(js)
3494                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
3495                 .addFilterSchemas(AppSearchEmail.SCHEMA_TYPE)
3496                 .build());
3497 
3498         List<SearchResult> sr = searchResults.getNextPageAsync().get();
3499 
3500         assertThat(sr).hasSize(2);
3501         assertThat(sr.get(0).getGenericDocument().getId()).isEqualTo("email2");
3502         assertThat(sr.get(0).getRankingSignal()).isEqualTo(1.0);
3503         assertThat(sr.get(1).getGenericDocument().getId()).isEqualTo("email1");
3504         assertThat(sr.get(1).getRankingSignal()).isEqualTo(0.0);
3505     }
3506 
3507     @Test
testQueryRankByDismissActions_useTakenActionGenericDocument()3508     public void testQueryRankByDismissActions_useTakenActionGenericDocument() throws Exception {
3509         assumeTrue(mDb1.getFeatures()
3510                 .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
3511 
3512         AppSearchSchema searchActionSchema = new AppSearchSchema.Builder("builtin:SearchAction")
3513                 .addProperty(new LongPropertyConfig.Builder("actionType")
3514                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3515                         .build())
3516                 .addProperty(new StringPropertyConfig.Builder("query")
3517                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3518                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
3519                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
3520                         .build()
3521                 ).build();
3522         AppSearchSchema clickActionSchema = new AppSearchSchema.Builder("builtin:ClickAction")
3523                 .addProperty(new LongPropertyConfig.Builder("actionType")
3524                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3525                         .build())
3526                 .addProperty(new StringPropertyConfig.Builder("query")
3527                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3528                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
3529                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
3530                         .build()
3531                 ).addProperty(new StringPropertyConfig.Builder("referencedQualifiedId")
3532                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3533                         .setJoinableValueType(StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID)
3534                         .build()
3535                 ).build();
3536         AppSearchSchema impressionActionSchema =
3537                 new AppSearchSchema.Builder("builtin:ImpressionAction")
3538                         .addProperty(new LongPropertyConfig.Builder("actionType")
3539                                 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3540                                 .build())
3541                         .addProperty(new StringPropertyConfig.Builder("query")
3542                                 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3543                                 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
3544                                 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
3545                                 .build()
3546                         ).addProperty(new StringPropertyConfig.Builder("referencedQualifiedId")
3547                                 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3548                                 .setJoinableValueType(
3549                                         StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID)
3550                                 .build()
3551                         ).build();
3552         AppSearchSchema dismissActionSchema =
3553                 new AppSearchSchema.Builder("builtin:DismissAction")
3554                         .addProperty(new LongPropertyConfig.Builder("actionType")
3555                                 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3556                                 .build())
3557                         .addProperty(new StringPropertyConfig.Builder("query")
3558                                 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3559                                 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
3560                                 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
3561                                 .build()
3562                         ).addProperty(new StringPropertyConfig.Builder("referencedQualifiedId")
3563                                 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3564                                 .setJoinableValueType(
3565                                         StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID)
3566                                 .build()
3567                         ).build();
3568 
3569         // Schema registration
3570         mDb1.setSchemaAsync(
3571                 new SetSchemaRequest.Builder()
3572                         .addSchemas(AppSearchEmail.SCHEMA, searchActionSchema, clickActionSchema,
3573                                 impressionActionSchema, dismissActionSchema)
3574                         .build())
3575                 .get();
3576 
3577         // Index several email documents
3578         AppSearchEmail inEmail1 =
3579                 new AppSearchEmail.Builder("namespace", "email1")
3580                         .setFrom("from@example.com")
3581                         .setTo("to1@example.com", "to2@example.com")
3582                         .setSubject("testPut example")
3583                         .setBody("This is the body of the testPut email")
3584                         .setScore(1)
3585                         .build();
3586         AppSearchEmail inEmail2 =
3587                 new AppSearchEmail.Builder("namespace", "email2")
3588                         .setFrom("from@example.com")
3589                         .setTo("to1@example.com", "to2@example.com")
3590                         .setSubject("testPut example")
3591                         .setBody("This is the body of the testPut email")
3592                         .setScore(1)
3593                         .build();
3594 
3595         String qualifiedId1 = DocumentIdUtil.createQualifiedId(
3596                 mContext.getPackageName(), DB_NAME_1, inEmail1);
3597         String qualifiedId2 = DocumentIdUtil.createQualifiedId(
3598                 mContext.getPackageName(), DB_NAME_1, inEmail2);
3599 
3600         GenericDocument searchAction =
3601                 new GenericDocument.Builder<>("namespace", "search1", "builtin:SearchAction")
3602                         .setCreationTimestampMillis(1000)
3603                         .setPropertyLong("actionType", ACTION_TYPE_SEARCH)
3604                         .setPropertyString("query", "body")
3605                         .build();
3606         GenericDocument clickAction1 =
3607                 new GenericDocument.Builder<>("namespace", "click1", "builtin:ClickAction")
3608                         .setCreationTimestampMillis(2000)
3609                         .setPropertyLong("actionType", ACTION_TYPE_CLICK)
3610                         .setPropertyString("query", "body")
3611                         .setPropertyString("referencedQualifiedId", qualifiedId1)
3612                         .build();
3613         GenericDocument clickAction2 =
3614                 new GenericDocument.Builder<>("namespace", "click2", "builtin:ClickAction")
3615                         .setCreationTimestampMillis(3000)
3616                         .setPropertyLong("actionType", ACTION_TYPE_CLICK)
3617                         .setPropertyString("query", "body")
3618                         .setPropertyString("referencedQualifiedId", qualifiedId2)
3619                         .build();
3620         GenericDocument clickAction3 =
3621                 new GenericDocument.Builder<>("namespace", "click3", "builtin:ClickAction")
3622                         .setCreationTimestampMillis(4000)
3623                         .setPropertyLong("actionType", ACTION_TYPE_CLICK)
3624                         .setPropertyString("query", "body")
3625                         .setPropertyString("referencedQualifiedId", qualifiedId1)
3626                         .build();
3627         GenericDocument impressionAction1 =
3628                 new GenericDocument.Builder<>(
3629                         "namespace", "impression1", "builtin:ImpressionAction")
3630                         .setCreationTimestampMillis(5000)
3631                         .setPropertyLong("actionType", ACTION_TYPE_IMPRESSION)
3632                         .setPropertyString("query", "body")
3633                         .setPropertyString("referencedQualifiedId", qualifiedId2)
3634                         .build();
3635         GenericDocument dismissAction1 =
3636                 new GenericDocument.Builder<>("namespace", "dismiss1", "builtin:DismissAction")
3637                         .setCreationTimestampMillis(6000)
3638                         .setPropertyLong("actionType", ACTION_TYPE_DISMISS)
3639                         .setPropertyString("query", "body")
3640                         .setPropertyString("referencedQualifiedId", qualifiedId2)
3641                         .build();
3642         GenericDocument dismissAction2 =
3643                 new GenericDocument.Builder<>("namespace", "dismiss2", "builtin:DismissAction")
3644                         .setCreationTimestampMillis(7000)
3645                         .setPropertyLong("actionType", ACTION_TYPE_DISMISS)
3646                         .setPropertyString("query", "body")
3647                         .setPropertyString("referencedQualifiedId", qualifiedId2)
3648                         .build();
3649         GenericDocument dismissAction3 =
3650                 new GenericDocument.Builder<>("namespace", "dismiss3", "builtin:DismissAction")
3651                         .setCreationTimestampMillis(8000)
3652                         .setPropertyLong("actionType", ACTION_TYPE_DISMISS)
3653                         .setPropertyString("query", "body")
3654                         .setPropertyString("referencedQualifiedId", qualifiedId2)
3655                         .build();
3656 
3657         checkIsBatchResultSuccess(mDb1.putAsync(
3658                 new PutDocumentsRequest.Builder()
3659                         .addGenericDocuments(inEmail1, inEmail2)
3660                         .addTakenActionGenericDocuments(
3661                                 searchAction, clickAction1, clickAction2, clickAction3,
3662                                 impressionAction1, dismissAction1, dismissAction2, dismissAction3)
3663                         .build()));
3664 
3665         SearchSpec nestedSearchSpec =
3666                 new SearchSpec.Builder()
3667                         .setRankingStrategy(SearchSpec.RANKING_STRATEGY_DOCUMENT_SCORE)
3668                         .setOrder(SearchSpec.ORDER_DESCENDING)
3669                         .addFilterSchemas("builtin:DismissAction")
3670                         .build();
3671 
3672         // Note: SearchSpec.Builder#setMaxJoinedResultCount only limits the number of child
3673         // documents returned. It does not affect the number of child documents that are scored.
3674         JoinSpec js = new JoinSpec.Builder("referencedQualifiedId")
3675                 .setNestedSearch("query:body", nestedSearchSpec)
3676                 .setMaxJoinedResultCount(0)
3677                 .build();
3678 
3679         // Search "body" for AppSearchEmail documents, ranking by DismissAction signals with
3680         // query = "body" via advanced scoring language syntax to assign negative weights.
3681         SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
3682                 .setRankingStrategy("-len(this.childrenRankingSignals())")
3683                 .setOrder(SearchSpec.ORDER_DESCENDING)
3684                 .setJoinSpec(js)
3685                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
3686                 .addFilterSchemas(AppSearchEmail.SCHEMA_TYPE)
3687                 .build());
3688 
3689         List<SearchResult> sr = searchResults.getNextPageAsync().get();
3690 
3691         assertThat(sr).hasSize(2);
3692         assertThat(sr.get(0).getGenericDocument().getId()).isEqualTo("email1");
3693         assertThat(sr.get(0).getRankingSignal()).isEqualTo(-0.0);
3694         assertThat(sr.get(1).getGenericDocument().getId()).isEqualTo("email2");
3695         assertThat(sr.get(1).getRankingSignal()).isEqualTo(-3.0);
3696     }
3697 
3698     @Test
testQuery_invalidAdvancedRanking()3699     public void testQuery_invalidAdvancedRanking() throws Exception {
3700         assumeTrue(mDb1.getFeatures().isFeatureSupported(
3701                 Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION));
3702 
3703         // Schema registration
3704         mDb1.setSchemaAsync(
3705                 new SetSchemaRequest.Builder()
3706                         .addSchemas(AppSearchEmail.SCHEMA)
3707                         .build()).get();
3708 
3709         // Index a document
3710         AppSearchEmail inEmail =
3711                 new AppSearchEmail.Builder("namespace", "id1")
3712                         .setFrom("from@example.com")
3713                         .setTo("to1@example.com", "to2@example.com")
3714                         .setSubject("testPut example")
3715                         .setBody("This is the body of the testPut email")
3716                         .build();
3717         checkIsBatchResultSuccess(mDb1.putAsync(
3718                 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
3719 
3720         // Query for the document, but set an invalid advanced ranking expression.
3721         SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
3722                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
3723                 .setRankingStrategy("sqrt()")
3724                 .build());
3725         ExecutionException executionException = assertThrows(ExecutionException.class,
3726                 () -> searchResults.getNextPageAsync().get());
3727         assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
3728         AppSearchException exception = (AppSearchException) executionException.getCause();
3729         assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT);
3730         assertThat(exception).hasMessageThat().contains(
3731                 "Math functions must have at least one argument.");
3732     }
3733 
3734     @Test
testQuery_invalidAdvancedRankingWithChildrenRankingSignals()3735     public void testQuery_invalidAdvancedRankingWithChildrenRankingSignals() throws Exception {
3736         assumeTrue(mDb1.getFeatures().isFeatureSupported(
3737                 Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION));
3738         assumeTrue(mDb1.getFeatures()
3739                 .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
3740 
3741         // Schema registration
3742         mDb1.setSchemaAsync(
3743                 new SetSchemaRequest.Builder()
3744                         .addSchemas(AppSearchEmail.SCHEMA)
3745                         .build()).get();
3746 
3747         // Index a document
3748         AppSearchEmail inEmail =
3749                 new AppSearchEmail.Builder("namespace", "id1")
3750                         .setFrom("from@example.com")
3751                         .setTo("to1@example.com", "to2@example.com")
3752                         .setSubject("testPut example")
3753                         .setBody("This is the body of the testPut email")
3754                         .build();
3755         checkIsBatchResultSuccess(mDb1.putAsync(
3756                 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
3757 
3758         SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
3759                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
3760                 // Using this.childrenRankingSignals() without the context of a join is invalid.
3761                 .setRankingStrategy("sum(this.childrenRankingSignals())")
3762                 .build());
3763         ExecutionException executionException = assertThrows(ExecutionException.class,
3764                 () -> searchResults.getNextPageAsync().get());
3765         assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
3766         AppSearchException exception = (AppSearchException) executionException.getCause();
3767         assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT);
3768         assertThat(exception).hasMessageThat().contains(
3769                 "childrenRankingSignals must only be used with join");
3770     }
3771 
3772     @Test
testQuery_unsupportedAdvancedRanking()3773     public void testQuery_unsupportedAdvancedRanking() throws Exception {
3774         // Assume that advanced ranking has not been supported.
3775         assumeFalse(mDb1.getFeatures().isFeatureSupported(
3776                 Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION));
3777 
3778         // Schema registration
3779         mDb1.setSchemaAsync(
3780                 new SetSchemaRequest.Builder()
3781                         .addSchemas(AppSearchEmail.SCHEMA)
3782                         .build()).get();
3783 
3784         // Index a document
3785         AppSearchEmail inEmail =
3786                 new AppSearchEmail.Builder("namespace", "id1")
3787                         .setFrom("from@example.com")
3788                         .setTo("to1@example.com", "to2@example.com")
3789                         .setSubject("testPut example")
3790                         .setBody("This is the body of the testPut email")
3791                         .build();
3792         checkIsBatchResultSuccess(mDb1.putAsync(
3793                 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
3794 
3795         // Query for the document, and set a valid advanced ranking expression.
3796         SearchSpec searchSpec = new SearchSpec.Builder()
3797                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
3798                 .setRankingStrategy("sqrt(4)")
3799                 .build();
3800         UnsupportedOperationException e = assertThrows(UnsupportedOperationException.class,
3801                 () -> mDb1.search("body", searchSpec));
3802         assertThat(e).hasMessageThat().contains(
3803                 Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION + " is not available on this "
3804                         + "AppSearch implementation.");
3805     }
3806 
3807     @Test
testQuery_typeFilter()3808     public void testQuery_typeFilter() throws Exception {
3809         // Schema registration
3810         AppSearchSchema genericSchema = new AppSearchSchema.Builder("Generic")
3811                 .addProperty(new StringPropertyConfig.Builder("foo")
3812                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3813                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
3814                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
3815                         .build()
3816                 ).build();
3817         mDb1.setSchemaAsync(
3818                 new SetSchemaRequest.Builder()
3819                         .addSchemas(AppSearchEmail.SCHEMA)
3820                         .addSchemas(genericSchema)
3821                         .build()).get();
3822 
3823         // Index a document
3824         AppSearchEmail inEmail =
3825                 new AppSearchEmail.Builder("namespace", "id1")
3826                         .setFrom("from@example.com")
3827                         .setTo("to1@example.com", "to2@example.com")
3828                         .setSubject("testPut example")
3829                         .setBody("This is the body of the testPut email")
3830                         .build();
3831         GenericDocument inDoc = new GenericDocument.Builder<>("namespace", "id2", "Generic")
3832                 .setPropertyString("foo", "body").build();
3833         checkIsBatchResultSuccess(mDb1.putAsync(
3834                 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail, inDoc).build()));
3835 
3836         // Query for the documents
3837         SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
3838                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
3839                 .build());
3840         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
3841         assertThat(documents).hasSize(2);
3842         assertThat(documents).containsExactly(inEmail, inDoc);
3843 
3844         // Query only for Document
3845         searchResults = mDb1.search("body", new SearchSpec.Builder()
3846                 .addFilterSchemas("Generic", "Generic") // duplicate type in filter won't matter.
3847                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
3848                 .build());
3849         documents = convertSearchResultsToDocuments(searchResults);
3850         assertThat(documents).hasSize(1);
3851         assertThat(documents).containsExactly(inDoc);
3852 
3853         // Query only for non-existent type
3854         searchResults = mDb1.search("body", new SearchSpec.Builder()
3855                 .addFilterSchemas("nonExistType")
3856                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
3857                 .build());
3858         documents = convertSearchResultsToDocuments(searchResults);
3859         assertThat(documents).isEmpty();
3860     }
3861 
3862     @Test
testQuery_packageFilter()3863     public void testQuery_packageFilter() throws Exception {
3864         // Schema registration
3865         mDb1.setSchemaAsync(
3866                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
3867 
3868         // Index documents
3869         AppSearchEmail email =
3870                 new AppSearchEmail.Builder("namespace", "id1")
3871                         .setFrom("from@example.com")
3872                         .setTo("to1@example.com", "to2@example.com")
3873                         .setSubject("foo")
3874                         .setBody("This is the body of the testPut email")
3875                         .build();
3876         checkIsBatchResultSuccess(mDb1.putAsync(
3877                 new PutDocumentsRequest.Builder().addGenericDocuments(email).build()));
3878 
3879         // Query for the document within our package
3880         SearchResults searchResults = mDb1.search("foo", new SearchSpec.Builder()
3881                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
3882                 .addFilterPackageNames(ApplicationProvider.getApplicationContext().getPackageName())
3883                 .build());
3884         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
3885         assertThat(documents).containsExactly(email);
3886 
3887         // Query for the document in some other package, which won't exist
3888         searchResults = mDb1.search("foo", new SearchSpec.Builder()
3889                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
3890                 .addFilterPackageNames("some.other.package")
3891                 .build());
3892         List<SearchResult> results = searchResults.getNextPageAsync().get();
3893         assertThat(results).isEmpty();
3894     }
3895 
3896     @Test
testQuery_namespaceFilter()3897     public void testQuery_namespaceFilter() throws Exception {
3898         // Schema registration
3899         mDb1.setSchemaAsync(
3900                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
3901 
3902         // Index two documents
3903         AppSearchEmail expectedEmail =
3904                 new AppSearchEmail.Builder("expectedNamespace", "id1")
3905                         .setFrom("from@example.com")
3906                         .setTo("to1@example.com", "to2@example.com")
3907                         .setSubject("testPut example")
3908                         .setBody("This is the body of the testPut email")
3909                         .build();
3910         AppSearchEmail unexpectedEmail =
3911                 new AppSearchEmail.Builder("unexpectedNamespace", "id1")
3912                         .setFrom("from@example.com")
3913                         .setTo("to1@example.com", "to2@example.com")
3914                         .setSubject("testPut example")
3915                         .setBody("This is the body of the testPut email")
3916                         .build();
3917         checkIsBatchResultSuccess(mDb1.putAsync(
3918                 new PutDocumentsRequest.Builder()
3919                         .addGenericDocuments(expectedEmail, unexpectedEmail).build()));
3920 
3921         // Query for all namespaces
3922         SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
3923                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
3924                 .build());
3925         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
3926         assertThat(documents).hasSize(2);
3927         assertThat(documents).containsExactly(expectedEmail, unexpectedEmail);
3928 
3929         // Query only for expectedNamespace
3930         searchResults = mDb1.search("body",
3931                 new SearchSpec.Builder()
3932                         .addFilterNamespaces("expectedNamespace")
3933                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
3934                         .build());
3935         documents = convertSearchResultsToDocuments(searchResults);
3936         assertThat(documents).hasSize(1);
3937         assertThat(documents).containsExactly(expectedEmail);
3938 
3939         // Query only for non-existent namespace
3940         searchResults = mDb1.search("body",
3941                 new SearchSpec.Builder()
3942                         .addFilterNamespaces("nonExistNamespace")
3943                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
3944                         .build());
3945         documents = convertSearchResultsToDocuments(searchResults);
3946         assertThat(documents).isEmpty();
3947     }
3948 
3949     @Test
testQuery_getPackageName()3950     public void testQuery_getPackageName() throws Exception {
3951         // Schema registration
3952         mDb1.setSchemaAsync(
3953                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
3954 
3955         // Index a document
3956         AppSearchEmail inEmail =
3957                 new AppSearchEmail.Builder("namespace", "id1")
3958                         .setFrom("from@example.com")
3959                         .setTo("to1@example.com", "to2@example.com")
3960                         .setSubject("testPut example")
3961                         .setBody("This is the body of the testPut email")
3962                         .build();
3963         checkIsBatchResultSuccess(mDb1.putAsync(
3964                 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
3965 
3966         // Query for the document
3967         SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
3968                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
3969                 .build());
3970 
3971         List<SearchResult> results;
3972         List<GenericDocument> documents = new ArrayList<>();
3973         // keep loading next page until it's empty.
3974         do {
3975             results = searchResults.getNextPageAsync().get();
3976             for (SearchResult result : results) {
3977                 assertThat(result.getGenericDocument()).isEqualTo(inEmail);
3978                 assertThat(result.getPackageName()).isEqualTo(
3979                         ApplicationProvider.getApplicationContext().getPackageName());
3980                 documents.add(result.getGenericDocument());
3981             }
3982         } while (results.size() > 0);
3983         assertThat(documents).hasSize(1);
3984     }
3985 
3986     @Test
testQuery_getDatabaseName()3987     public void testQuery_getDatabaseName() throws Exception {
3988         // Schema registration
3989         mDb1.setSchemaAsync(
3990                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
3991 
3992         // Index a document
3993         AppSearchEmail inEmail =
3994                 new AppSearchEmail.Builder("namespace", "id1")
3995                         .setFrom("from@example.com")
3996                         .setTo("to1@example.com", "to2@example.com")
3997                         .setSubject("testPut example")
3998                         .setBody("This is the body of the testPut email")
3999                         .build();
4000         checkIsBatchResultSuccess(mDb1.putAsync(
4001                 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
4002 
4003         // Query for the document
4004         SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
4005                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
4006                 .build());
4007 
4008         List<SearchResult> results;
4009         List<GenericDocument> documents = new ArrayList<>();
4010         // keep loading next page until it's empty.
4011         do {
4012             results = searchResults.getNextPageAsync().get();
4013             for (SearchResult result : results) {
4014                 assertThat(result.getGenericDocument()).isEqualTo(inEmail);
4015                 assertThat(result.getDatabaseName()).isEqualTo(DB_NAME_1);
4016                 documents.add(result.getGenericDocument());
4017             }
4018         } while (results.size() > 0);
4019         assertThat(documents).hasSize(1);
4020 
4021         // Schema registration for another database
4022         mDb2.setSchemaAsync(
4023                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
4024 
4025         checkIsBatchResultSuccess(mDb2.putAsync(
4026                 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
4027 
4028         // Query for the document
4029         searchResults = mDb2.search("body", new SearchSpec.Builder()
4030                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
4031                 .build());
4032 
4033         documents = new ArrayList<>();
4034         // keep loading next page until it's empty.
4035         do {
4036             results = searchResults.getNextPageAsync().get();
4037             for (SearchResult result : results) {
4038                 assertThat(result.getGenericDocument()).isEqualTo(inEmail);
4039                 assertThat(result.getDatabaseName()).isEqualTo(DB_NAME_2);
4040                 documents.add(result.getGenericDocument());
4041             }
4042         } while (results.size() > 0);
4043         assertThat(documents).hasSize(1);
4044     }
4045 
4046     @Test
testQuery_projection()4047     public void testQuery_projection() throws Exception {
4048         // Schema registration
4049         mDb1.setSchemaAsync(
4050                 new SetSchemaRequest.Builder()
4051                         .addSchemas(AppSearchEmail.SCHEMA)
4052                         .addSchemas(new AppSearchSchema.Builder("Note")
4053                                 .addProperty(new StringPropertyConfig.Builder("title")
4054                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
4055                                         .setIndexingType(
4056                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
4057                                         .setTokenizerType(
4058                                                 StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
4059                                         .build())
4060                                 .addProperty(new StringPropertyConfig.Builder("body")
4061                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
4062                                         .setIndexingType(
4063                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
4064                                         .setTokenizerType(
4065                                                 StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
4066                                         .build())
4067                                 .build())
4068                         .build()).get();
4069 
4070         // Index two documents
4071         AppSearchEmail email =
4072                 new AppSearchEmail.Builder("namespace", "id1")
4073                         .setCreationTimestampMillis(1000)
4074                         .setFrom("from@example.com")
4075                         .setTo("to1@example.com", "to2@example.com")
4076                         .setSubject("testPut example")
4077                         .setBody("This is the body of the testPut email")
4078                         .build();
4079         GenericDocument note =
4080                 new GenericDocument.Builder<>("namespace", "id2", "Note")
4081                         .setCreationTimestampMillis(1000)
4082                         .setPropertyString("title", "Note title")
4083                         .setPropertyString("body", "Note body").build();
4084         checkIsBatchResultSuccess(mDb1.putAsync(
4085                 new PutDocumentsRequest.Builder()
4086                         .addGenericDocuments(email, note).build()));
4087 
4088         // Query with type property paths {"Email", ["body", "to"]}
4089         SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
4090                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
4091                 .addProjection(AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("body", "to"))
4092                 .build());
4093         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
4094 
4095         // The email document should have been returned with only the "body" and "to"
4096         // properties. The note document should have been returned with all of its properties.
4097         AppSearchEmail expectedEmail =
4098                 new AppSearchEmail.Builder("namespace", "id1")
4099                         .setCreationTimestampMillis(1000)
4100                         .setTo("to1@example.com", "to2@example.com")
4101                         .setBody("This is the body of the testPut email")
4102                         .build();
4103         GenericDocument expectedNote =
4104                 new GenericDocument.Builder<>("namespace", "id2", "Note")
4105                         .setCreationTimestampMillis(1000)
4106                         .setPropertyString("title", "Note title")
4107                         .setPropertyString("body", "Note body").build();
4108         assertThat(documents).containsExactly(expectedNote, expectedEmail);
4109     }
4110 
4111     @Test
testQuery_projectionEmpty()4112     public void testQuery_projectionEmpty() throws Exception {
4113         // Schema registration
4114         mDb1.setSchemaAsync(
4115                 new SetSchemaRequest.Builder()
4116                         .addSchemas(AppSearchEmail.SCHEMA)
4117                         .addSchemas(new AppSearchSchema.Builder("Note")
4118                                 .addProperty(new StringPropertyConfig.Builder("title")
4119                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
4120                                         .setIndexingType(
4121                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
4122                                         .setTokenizerType(
4123                                                 StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
4124                                         .build())
4125                                 .addProperty(new StringPropertyConfig.Builder("body")
4126                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
4127                                         .setIndexingType(
4128                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
4129                                         .setTokenizerType(
4130                                                 StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
4131                                         .build())
4132                                 .build())
4133                         .build()).get();
4134 
4135         // Index two documents
4136         AppSearchEmail email =
4137                 new AppSearchEmail.Builder("namespace", "id1")
4138                         .setCreationTimestampMillis(1000)
4139                         .setFrom("from@example.com")
4140                         .setTo("to1@example.com", "to2@example.com")
4141                         .setSubject("testPut example")
4142                         .setBody("This is the body of the testPut email")
4143                         .build();
4144         GenericDocument note =
4145                 new GenericDocument.Builder<>("namespace", "id2", "Note")
4146                         .setCreationTimestampMillis(1000)
4147                         .setPropertyString("title", "Note title")
4148                         .setPropertyString("body", "Note body").build();
4149         checkIsBatchResultSuccess(mDb1.putAsync(
4150                 new PutDocumentsRequest.Builder()
4151                         .addGenericDocuments(email, note).build()));
4152 
4153         // Query with type property paths {"Email", []}
4154         SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
4155                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
4156                 .addProjection(AppSearchEmail.SCHEMA_TYPE, Collections.emptyList())
4157                 .build());
4158         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
4159 
4160         // The email document should have been returned without any properties. The note document
4161         // should have been returned with all of its properties.
4162         AppSearchEmail expectedEmail =
4163                 new AppSearchEmail.Builder("namespace", "id1")
4164                         .setCreationTimestampMillis(1000)
4165                         .build();
4166         GenericDocument expectedNote =
4167                 new GenericDocument.Builder<>("namespace", "id2", "Note")
4168                         .setCreationTimestampMillis(1000)
4169                         .setPropertyString("title", "Note title")
4170                         .setPropertyString("body", "Note body").build();
4171         assertThat(documents).containsExactly(expectedNote, expectedEmail);
4172     }
4173 
4174     @Test
testQuery_projectionNonExistentType()4175     public void testQuery_projectionNonExistentType() throws Exception {
4176         // Schema registration
4177         mDb1.setSchemaAsync(
4178                 new SetSchemaRequest.Builder()
4179                         .addSchemas(AppSearchEmail.SCHEMA)
4180                         .addSchemas(new AppSearchSchema.Builder("Note")
4181                                 .addProperty(new StringPropertyConfig.Builder("title")
4182                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
4183                                         .setIndexingType(
4184                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
4185                                         .setTokenizerType(
4186                                                 StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
4187                                         .build())
4188                                 .addProperty(new StringPropertyConfig.Builder("body")
4189                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
4190                                         .setIndexingType(
4191                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
4192                                         .setTokenizerType(
4193                                                 StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
4194                                         .build())
4195                                 .build())
4196                         .build()).get();
4197 
4198         // Index two documents
4199         AppSearchEmail email =
4200                 new AppSearchEmail.Builder("namespace", "id1")
4201                         .setCreationTimestampMillis(1000)
4202                         .setFrom("from@example.com")
4203                         .setTo("to1@example.com", "to2@example.com")
4204                         .setSubject("testPut example")
4205                         .setBody("This is the body of the testPut email")
4206                         .build();
4207         GenericDocument note =
4208                 new GenericDocument.Builder<>("namespace", "id2", "Note")
4209                         .setCreationTimestampMillis(1000)
4210                         .setPropertyString("title", "Note title")
4211                         .setPropertyString("body", "Note body").build();
4212         checkIsBatchResultSuccess(mDb1.putAsync(
4213                 new PutDocumentsRequest.Builder()
4214                         .addGenericDocuments(email, note).build()));
4215 
4216         // Query with type property paths {"NonExistentType", []}, {"Email", ["body", "to"]}
4217         SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
4218                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
4219                 .addProjection("NonExistentType", Collections.emptyList())
4220                 .addProjection(AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("body", "to"))
4221                 .build());
4222         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
4223 
4224         // The email document should have been returned with only the "body" and "to" properties.
4225         // The note document should have been returned with all of its properties.
4226         AppSearchEmail expectedEmail =
4227                 new AppSearchEmail.Builder("namespace", "id1")
4228                         .setCreationTimestampMillis(1000)
4229                         .setTo("to1@example.com", "to2@example.com")
4230                         .setBody("This is the body of the testPut email")
4231                         .build();
4232         GenericDocument expectedNote =
4233                 new GenericDocument.Builder<>("namespace", "id2", "Note")
4234                         .setCreationTimestampMillis(1000)
4235                         .setPropertyString("title", "Note title")
4236                         .setPropertyString("body", "Note body").build();
4237         assertThat(documents).containsExactly(expectedNote, expectedEmail);
4238     }
4239 
4240     @Test
testQuery_wildcardProjection()4241     public void testQuery_wildcardProjection() throws Exception {
4242         // Schema registration
4243         mDb1.setSchemaAsync(
4244                 new SetSchemaRequest.Builder()
4245                         .addSchemas(AppSearchEmail.SCHEMA)
4246                         .addSchemas(new AppSearchSchema.Builder("Note")
4247                                 .addProperty(new StringPropertyConfig.Builder("title")
4248                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
4249                                         .setIndexingType(
4250                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
4251                                         .setTokenizerType(
4252                                                 StringPropertyConfig.TOKENIZER_TYPE_PLAIN).build())
4253                                 .addProperty(new StringPropertyConfig.Builder("body")
4254                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
4255                                         .setIndexingType(
4256                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
4257                                         .setTokenizerType(
4258                                                 StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
4259                                         .build())
4260                                 .build())
4261                         .build()).get();
4262 
4263         // Index two documents
4264         AppSearchEmail email =
4265                 new AppSearchEmail.Builder("namespace", "id1")
4266                         .setCreationTimestampMillis(1000)
4267                         .setFrom("from@example.com")
4268                         .setTo("to1@example.com", "to2@example.com")
4269                         .setSubject("testPut example")
4270                         .setBody("This is the body of the testPut email")
4271                         .build();
4272         GenericDocument note =
4273                 new GenericDocument.Builder<>("namespace", "id2", "Note")
4274                         .setCreationTimestampMillis(1000)
4275                         .setPropertyString("title", "Note title")
4276                         .setPropertyString("body", "Note body").build();
4277         checkIsBatchResultSuccess(mDb1.putAsync(
4278                 new PutDocumentsRequest.Builder()
4279                         .addGenericDocuments(email, note).build()));
4280 
4281         // Query with type property paths {"*", ["body", "to"]}
4282         SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
4283                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
4284                 .addProjection(
4285                         SearchSpec.SCHEMA_TYPE_WILDCARD, ImmutableList.of("body", "to"))
4286                 .build());
4287         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
4288 
4289         // The email document should have been returned with only the "body" and "to"
4290         // properties. The note document should have been returned with only the "body" property.
4291         AppSearchEmail expectedEmail =
4292                 new AppSearchEmail.Builder("namespace", "id1")
4293                         .setCreationTimestampMillis(1000)
4294                         .setTo("to1@example.com", "to2@example.com")
4295                         .setBody("This is the body of the testPut email")
4296                         .build();
4297         GenericDocument expectedNote =
4298                 new GenericDocument.Builder<>("namespace", "id2", "Note")
4299                         .setCreationTimestampMillis(1000)
4300                         .setPropertyString("body", "Note body").build();
4301         assertThat(documents).containsExactly(expectedNote, expectedEmail);
4302     }
4303 
4304     @Test
testQuery_wildcardProjectionEmpty()4305     public void testQuery_wildcardProjectionEmpty() throws Exception {
4306         // Schema registration
4307         mDb1.setSchemaAsync(
4308                 new SetSchemaRequest.Builder()
4309                         .addSchemas(AppSearchEmail.SCHEMA)
4310                         .addSchemas(new AppSearchSchema.Builder("Note")
4311                                 .addProperty(new StringPropertyConfig.Builder("title")
4312                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
4313                                         .setIndexingType(
4314                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
4315                                         .setTokenizerType(
4316                                                 StringPropertyConfig.TOKENIZER_TYPE_PLAIN).build())
4317                                 .addProperty(new StringPropertyConfig.Builder("body")
4318                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
4319                                         .setIndexingType(
4320                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
4321                                         .setTokenizerType(
4322                                                 StringPropertyConfig.TOKENIZER_TYPE_PLAIN).build())
4323                                 .build()).build()).get();
4324 
4325         // Index two documents
4326         AppSearchEmail email =
4327                 new AppSearchEmail.Builder("namespace", "id1")
4328                         .setCreationTimestampMillis(1000)
4329                         .setFrom("from@example.com")
4330                         .setTo("to1@example.com", "to2@example.com")
4331                         .setSubject("testPut example")
4332                         .setBody("This is the body of the testPut email")
4333                         .build();
4334         GenericDocument note =
4335                 new GenericDocument.Builder<>("namespace", "id2", "Note")
4336                         .setCreationTimestampMillis(1000)
4337                         .setPropertyString("title", "Note title")
4338                         .setPropertyString("body", "Note body").build();
4339         checkIsBatchResultSuccess(mDb1.putAsync(
4340                 new PutDocumentsRequest.Builder()
4341                         .addGenericDocuments(email, note).build()));
4342 
4343         // Query with type property paths {"*", []}
4344         SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
4345                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
4346                 .addProjection(SearchSpec.SCHEMA_TYPE_WILDCARD, Collections.emptyList())
4347                 .build());
4348         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
4349 
4350         // The email and note documents should have been returned without any properties.
4351         AppSearchEmail expectedEmail =
4352                 new AppSearchEmail.Builder("namespace", "id1")
4353                         .setCreationTimestampMillis(1000)
4354                         .build();
4355         GenericDocument expectedNote =
4356                 new GenericDocument.Builder<>("namespace", "id2", "Note")
4357                         .setCreationTimestampMillis(1000).build();
4358         assertThat(documents).containsExactly(expectedNote, expectedEmail);
4359     }
4360 
4361     @Test
testQuery_wildcardProjectionNonExistentType()4362     public void testQuery_wildcardProjectionNonExistentType() throws Exception {
4363         // Schema registration
4364         mDb1.setSchemaAsync(
4365                 new SetSchemaRequest.Builder()
4366                         .addSchemas(AppSearchEmail.SCHEMA)
4367                         .addSchemas(new AppSearchSchema.Builder("Note")
4368                                 .addProperty(new StringPropertyConfig.Builder("title")
4369                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
4370                                         .setIndexingType(
4371                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
4372                                         .setTokenizerType(
4373                                                 StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
4374                                         .build())
4375                                 .addProperty(new StringPropertyConfig.Builder("body")
4376                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
4377                                         .setIndexingType(
4378                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
4379                                         .setTokenizerType(
4380                                                 StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
4381                                         .build())
4382                                 .build())
4383                         .build()).get();
4384 
4385         // Index two documents
4386         AppSearchEmail email =
4387                 new AppSearchEmail.Builder("namespace", "id1")
4388                         .setCreationTimestampMillis(1000)
4389                         .setFrom("from@example.com")
4390                         .setTo("to1@example.com", "to2@example.com")
4391                         .setSubject("testPut example")
4392                         .setBody("This is the body of the testPut email")
4393                         .build();
4394         GenericDocument note =
4395                 new GenericDocument.Builder<>("namespace", "id2", "Note")
4396                         .setCreationTimestampMillis(1000)
4397                         .setPropertyString("title", "Note title")
4398                         .setPropertyString("body", "Note body").build();
4399         checkIsBatchResultSuccess(mDb1.putAsync(
4400                 new PutDocumentsRequest.Builder()
4401                         .addGenericDocuments(email, note).build()));
4402 
4403         // Query with type property paths {"NonExistentType", []}, {"*", ["body", "to"]}
4404         SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
4405                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
4406                 .addProjection("NonExistentType", Collections.emptyList())
4407                 .addProjection(
4408                         SearchSpec.SCHEMA_TYPE_WILDCARD, ImmutableList.of("body", "to"))
4409                 .build());
4410         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
4411 
4412         // The email document should have been returned with only the "body" and "to"
4413         // properties. The note document should have been returned with only the "body" property.
4414         AppSearchEmail expectedEmail =
4415                 new AppSearchEmail.Builder("namespace", "id1")
4416                         .setCreationTimestampMillis(1000)
4417                         .setTo("to1@example.com", "to2@example.com")
4418                         .setBody("This is the body of the testPut email")
4419                         .build();
4420         GenericDocument expectedNote =
4421                 new GenericDocument.Builder<>("namespace", "id2", "Note")
4422                         .setCreationTimestampMillis(1000)
4423                         .setPropertyString("body", "Note body").build();
4424         assertThat(documents).containsExactly(expectedNote, expectedEmail);
4425     }
4426 
4427     @Test
testQuery_wildcardProjectionWithExistentType()4428     public void testQuery_wildcardProjectionWithExistentType() throws Exception {
4429         // Schema registration
4430         mDb1.setSchemaAsync(
4431                 new SetSchemaRequest.Builder()
4432                         .addSchemas(AppSearchEmail.SCHEMA)
4433                         .build()).get();
4434 
4435         // Index two documents
4436         AppSearchEmail email1 =
4437                 new AppSearchEmail.Builder("namespace", "id1")
4438                         .setCreationTimestampMillis(1000)
4439                         .setFrom("from@example.com")
4440                         .setTo("to1@example.com", "to2@example.com")
4441                         .setSubject("testPut example")
4442                         .setBody("This is the body of the testPut email")
4443                         .build();
4444         AppSearchEmail email2 =
4445                 new AppSearchEmail.Builder("namespace", "id2")
4446                         .setCreationTimestampMillis(1000)
4447                         .setFrom("from@example.com")
4448                         .setTo("to1@example.com", "to2@example.com")
4449                         .setSubject("testPut example")
4450                         .setBody("This is the body of the testPut email")
4451                         .build();
4452         checkIsBatchResultSuccess(mDb1.putAsync(
4453                 new PutDocumentsRequest.Builder()
4454                         .addGenericDocuments(email1, email2).build()));
4455 
4456         // Get with type property paths {"Email", ["subject", "to"]}
4457         // The SCHEMA_TYPE projection takes preference over PROJECTION_SCHEMA_TYPE_WILDCARD.
4458         GetByDocumentIdRequest request = new GetByDocumentIdRequest.Builder("namespace")
4459                 .addIds("id1", "id2")
4460                 .addProjection(
4461                         GetByDocumentIdRequest.PROJECTION_SCHEMA_TYPE_WILDCARD,
4462                         ImmutableList.of("subject", "to"))
4463                 .addProjection(AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("from"))
4464 
4465                 .build();
4466         List<GenericDocument> outDocuments = doGet(mDb1, request);
4467 
4468         // The two email documents should have been returned with "from" properties.
4469         AppSearchEmail expected1 =
4470                 new AppSearchEmail.Builder("namespace", "id2")
4471                         .setCreationTimestampMillis(1000)
4472                         .setFrom("from@example.com")
4473                         .build();
4474         AppSearchEmail expected2 =
4475                 new AppSearchEmail.Builder("namespace", "id1")
4476                         .setCreationTimestampMillis(1000)
4477                         .setFrom("from@example.com")
4478                         .build();
4479         assertThat(outDocuments).containsExactly(expected1, expected2);
4480     }
4481 
4482     @Test
testQuery_projectionWithMultiplePages()4483     public void testQuery_projectionWithMultiplePages() throws Exception {
4484         // Schema registration
4485         mDb1.setSchemaAsync(
4486                 new SetSchemaRequest.Builder()
4487                     .addSchemas(AppSearchEmail.SCHEMA)
4488                     .build())
4489                 .get();
4490         PutDocumentsRequest.Builder putDocumentsRequestBuilder = new PutDocumentsRequest.Builder();
4491 
4492         // Index 10 documents.
4493         for (int i = 0; i < 10; i++) {
4494             AppSearchEmail email =
4495                     new AppSearchEmail.Builder("namespace", "id" + i)
4496                         .setFrom("from@example.com")
4497                         .setTo("to1@example.com", "to2@example.com")
4498                         .setSubject("testPut example")
4499                         .setBody("This is the body of the testPut email " + i)
4500                         .build();
4501             putDocumentsRequestBuilder.addGenericDocuments(email);
4502         }
4503         checkIsBatchResultSuccess(mDb1.putAsync(putDocumentsRequestBuilder.build()));
4504 
4505         // Query with type property paths {"Email", ["body", "to"]}
4506         // Set number of results per page to 4.
4507         SearchResults searchResults =
4508                 mDb1.search(
4509                     "body",
4510                     new SearchSpec.Builder()
4511                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
4512                         .setResultCountPerPage(4)
4513                         .addProjection(
4514                             AppSearchEmail.SCHEMA_TYPE,
4515                             ImmutableList.of("subject", "body"))
4516                         .build());
4517 
4518         // Manually populate documents instead of calling convertSearchResultsToDocuments.
4519         // This is so that the page count can be verified.
4520         List<GenericDocument> documents = new ArrayList<>();
4521         List<SearchResult> results;
4522         int pageCount = 0;
4523 
4524         // Keep loading pages until empty.
4525         do {
4526             results = searchResults.getNextPageAsync().get();
4527             ++pageCount;
4528             for (SearchResult result : results) {
4529                 documents.add(result.getGenericDocument());
4530             }
4531         } while (results.size() > 0);
4532 
4533         assertThat(pageCount).isEqualTo(4); // 3 (upper(10/4)) + 1 (final empty page)
4534         assertThat(documents).hasSize(10);
4535 
4536         for (GenericDocument document : documents) {
4537             // Assert that the document has the projected properties.
4538             assertThat(document.getPropertyString("subject")).isEqualTo("testPut example");
4539             assertThat(document.getPropertyString("body")).contains("This is the body");
4540 
4541             // Assert that a non-projected property is null.
4542             assertThat(document.getPropertyString("to")).isNull();
4543         }
4544     }
4545 
4546     @Test
testQuery_projectionWithNestedDocumentsAndMultiplePages()4547     public void testQuery_projectionWithNestedDocumentsAndMultiplePages() throws Exception {
4548         // Schema registration
4549         mDb1.setSchemaAsync(
4550                 new SetSchemaRequest.Builder()
4551                     .addSchemas(AppSearchEmail.SCHEMA)
4552                     .addSchemas(
4553                         new AppSearchSchema.Builder("yesNestedIndex")
4554                             .addProperty(
4555                                 new AppSearchSchema.DocumentPropertyConfig.Builder(
4556                                     "prop", AppSearchEmail.SCHEMA_TYPE)
4557                                     .setShouldIndexNestedProperties(true)
4558                                     .build())
4559                             .build())
4560                     .build())
4561                 .get();
4562 
4563         // Index 13 documents.
4564         PutDocumentsRequest.Builder putDocumentsRequestBuilder = new PutDocumentsRequest.Builder();
4565         for (int i = 0; i < 13; i++) {
4566             AppSearchEmail email =
4567                     new AppSearchEmail.Builder("namespace", "id" + i)
4568                         .setCreationTimestampMillis(1000)
4569                         .setFrom("from@example.com")
4570                         .setTo("to1@example.com", "to2@example.com")
4571                         .setSubject("testPut example " + i)
4572                         .setBody("This is the body of the testPut email " + i)
4573                         .build();
4574 
4575             GenericDocument nestedDocument =
4576                     new GenericDocument.Builder<>("namespace", "id" + i, "yesNestedIndex")
4577                         .setPropertyDocument("prop", email)
4578                         .build();
4579 
4580             putDocumentsRequestBuilder.addGenericDocuments(nestedDocument);
4581         }
4582         checkIsBatchResultSuccess(mDb1.putAsync(putDocumentsRequestBuilder.build()));
4583 
4584         // Query with projection on nested properties "prop.subject" and "prop.body".
4585         // Set number of results per page to 5.
4586         SearchResults searchResults =
4587                 mDb1.search(
4588                     "body",
4589                     new SearchSpec.Builder()
4590                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
4591                         .setResultCountPerPage(5)
4592                         .addProjection(
4593                             "yesNestedIndex",
4594                             ImmutableList.of("prop.subject", "prop.body"))
4595                         .build());
4596 
4597         // Manually populate documents instead of calling convertSearchResultsToDocuments.
4598         // This is so that the page count can be verified.
4599         List<GenericDocument> documents = new ArrayList<>();
4600         List<SearchResult> results;
4601         int pageCount = 0;
4602 
4603         // Keep loading pages until empty.
4604         do {
4605             results = searchResults.getNextPageAsync().get();
4606             ++pageCount;
4607             for (SearchResult result : results) {
4608                 documents.add(result.getGenericDocument());
4609             }
4610         } while (results.size() > 0);
4611 
4612         assertThat(pageCount).isEqualTo(4); // 3 (upper(13/5)) + 1 (final empty page)
4613         assertThat(documents).hasSize(13);
4614 
4615         for (GenericDocument document : documents) {
4616             // Assert that the document has the projected nested properties.
4617             assertThat(document.getPropertyString("prop.subject")).contains("testPut example");
4618             assertThat(document.getPropertyString("prop.body")).contains("This is the body");
4619 
4620             // Assert that a non-projected nested property is null.
4621             assertThat(document.getPropertyString("prop.to")).isNull();
4622         }
4623     }
4624 
4625     @Test
testQuery_projectionWithMultipleNestedDocumentsAndMultiplePages()4626     public void testQuery_projectionWithMultipleNestedDocumentsAndMultiplePages() throws Exception {
4627         // Schema registration
4628         mDb1.setSchemaAsync(
4629                 new SetSchemaRequest.Builder()
4630                     .addSchemas(AppSearchEmail.SCHEMA)
4631                     .addSchemas(
4632                         new AppSearchSchema.Builder("yesOuterNestedIndex")
4633                             .addProperty(
4634                                 new AppSearchSchema.DocumentPropertyConfig.Builder(
4635                                     "innerNestedProp",
4636                                     "yesInnerNestedIndex")
4637                                     .setShouldIndexNestedProperties(true)
4638                                     .build())
4639                             .build())
4640                     .addSchemas(
4641                         new AppSearchSchema.Builder("yesInnerNestedIndex")
4642                             .addProperty(
4643                                 new AppSearchSchema.DocumentPropertyConfig.Builder(
4644                                     "outerNestedProp",
4645                                     AppSearchEmail.SCHEMA_TYPE)
4646                                     .setShouldIndexNestedProperties(true)
4647                                     .build())
4648                             .build())
4649                     .build())
4650                 .get();
4651 
4652         // Index 28 documents
4653         PutDocumentsRequest.Builder putDocumentsRequestBuilder = new PutDocumentsRequest.Builder();
4654         for (int i = 0; i < 28; i++) {
4655             AppSearchEmail email =
4656                     new AppSearchEmail.Builder("namespace", "id" + i)
4657                         .setCreationTimestampMillis(1000)
4658                         .setFrom("from@example.com")
4659                         .setTo("to1@example.com", "to2@example.com")
4660                         .setSubject("testPut example " + i)
4661                         .setBody("This is the body of the testPut email " + i)
4662                         .build();
4663 
4664             GenericDocument innerNestedDocument =
4665                     new GenericDocument.Builder<>("namespace", "id" + i, "yesInnerNestedIndex")
4666                         .setPropertyDocument("outerNestedProp", email)
4667                         .build();
4668 
4669             GenericDocument outerNestedDocument =
4670                     new GenericDocument.Builder<>("namespace", "id" + i, "yesOuterNestedIndex")
4671                         .setPropertyDocument("innerNestedProp", innerNestedDocument)
4672                         .build();
4673 
4674             putDocumentsRequestBuilder.addGenericDocuments(outerNestedDocument);
4675         }
4676         checkIsBatchResultSuccess(mDb1.putAsync(putDocumentsRequestBuilder.build()));
4677 
4678         // Query with projection on nested properties
4679         // "innerNestedProp.outerNestedProp.subject" and "innerNestedProp.outerNestedProp.body".
4680         // Set number of results per page to 7.
4681         SearchResults searchResults =
4682                 mDb1.search(
4683                     "body",
4684                     new SearchSpec.Builder()
4685                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
4686                         .setResultCountPerPage(7)
4687                         .addProjection(
4688                             "yesOuterNestedIndex",
4689                             ImmutableList.of(
4690                                 "innerNestedProp.outerNestedProp.subject",
4691                                 "innerNestedProp.outerNestedProp.body"))
4692                         .build());
4693 
4694         // Manually populate documents instead of calling convertSearchResultsToDocuments.
4695         // This is so that the page count can be verified.
4696         List<GenericDocument> documents = new ArrayList<>();
4697         List<SearchResult> results;
4698         int pageCount = 0;
4699 
4700         // Keep loading pages until empty.
4701         do {
4702             results = searchResults.getNextPageAsync().get();
4703             ++pageCount;
4704             for (SearchResult result : results) {
4705                 documents.add(result.getGenericDocument());
4706             }
4707         } while (results.size() > 0);
4708 
4709         assertThat(pageCount).isEqualTo(5); // 4 (upper(28/7)) + 1 (final empty page)
4710         assertThat(documents).hasSize(28);
4711 
4712         for (GenericDocument document : documents) {
4713             // Assert that the document has the projected nested properties.
4714             assertThat(document.getPropertyString("innerNestedProp.outerNestedProp.subject"))
4715                     .contains("testPut example");
4716             assertThat(document.getPropertyString("innerNestedProp.outerNestedProp.body"))
4717                     .contains("This is the body");
4718 
4719             // Assert that a non-projected nested property is null.
4720             assertThat(document.getPropertyString("innerNestedProp.outerNestedProp.to")).isNull();
4721         }
4722     }
4723 
4724     @Test
testQuery_matchInfoProjection()4725     public void testQuery_matchInfoProjection() throws Exception {
4726         // Schema registration
4727         AppSearchSchema genericSchema = new AppSearchSchema.Builder("Generic")
4728                 .addProperty(new StringPropertyConfig.Builder("subject")
4729                 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
4730                 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
4731                 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
4732                 .build())
4733                 .build();
4734         mDb1.setSchemaAsync(
4735             new SetSchemaRequest.Builder().addSchemas(genericSchema).build()).get();
4736 
4737         // Index a document
4738         GenericDocument document =
4739                 new GenericDocument.Builder<>("namespace", "id", "Generic")
4740                     .setPropertyString("subject", "A commonly used fake word is foo. "
4741                             + "Another nonsense word that’s used a lot is bar")
4742                     .build();
4743         checkIsBatchResultSuccess(mDb1.putAsync(
4744             new PutDocumentsRequest.Builder().addGenericDocuments(document).build()));
4745 
4746         // Query with type property paths {"Generic", ["subject"]}
4747         SearchResults searchResults = mDb1.search("fo",
4748             new SearchSpec.Builder()
4749                 .addFilterSchemas("Generic")
4750                 .setSnippetCount(1)
4751                 .setSnippetCountPerProperty(1)
4752                 .setMaxSnippetSize(10)
4753                 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
4754                 .addProjection("Generic", ImmutableList.of("subject"))
4755                 .build());
4756         List<SearchResult> results = searchResults.getNextPageAsync().get();
4757         assertThat(results).hasSize(1);
4758 
4759         List<SearchResult.MatchInfo> matchInfos = results.get(0).getMatchInfos();
4760 
4761         assertThat(matchInfos).isNotNull();
4762         assertThat(matchInfos).hasSize(1);
4763         SearchResult.MatchInfo matchInfo = matchInfos.get(0);
4764         assertThat(matchInfo.getPropertyPath()).isEqualTo("subject");
4765         assertThat(matchInfo.getFullText()).isEqualTo("A commonly used fake word is foo. "
4766                 + "Another nonsense word that’s used a lot is bar");
4767         assertThat(matchInfo.getExactMatchRange()).isEqualTo(
4768             new SearchResult.MatchRange(/*start=*/29,  /*end=*/32));
4769         assertThat(matchInfo.getExactMatch().toString()).isEqualTo("foo");
4770         assertThat(matchInfo.getSnippetRange()).isEqualTo(
4771             new SearchResult.MatchRange(/*start=*/26,  /*end=*/33));
4772         assertThat(matchInfo.getSnippet().toString()).isEqualTo("is foo.");
4773         assertThat(matchInfo.getPropertyPath()).isEqualTo("subject");
4774 
4775         if (!mDb1.getFeatures().isFeatureSupported(
4776                 Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH)) {
4777             assertThrows(UnsupportedOperationException.class, matchInfo::getSubmatchRange);
4778             assertThrows(UnsupportedOperationException.class, matchInfo::getSubmatch);
4779         } else {
4780             assertThat(matchInfo.getSubmatchRange()).isEqualTo(
4781                 new SearchResult.MatchRange(/*start=*/29,  /*end=*/31));
4782             assertThat(matchInfo.getSubmatch().toString()).isEqualTo("fo");
4783         }
4784     }
4785 
4786     @Test
testQuery_matchInfoProjectionEmpty()4787     public void testQuery_matchInfoProjectionEmpty() throws Exception {
4788         // Schema registration
4789         AppSearchSchema genericSchema = new AppSearchSchema.Builder("Generic")
4790                 .addProperty(new StringPropertyConfig.Builder("subject")
4791                 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
4792                 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
4793                 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
4794                 .build())
4795                 .build();
4796         mDb1.setSchemaAsync(
4797             new SetSchemaRequest.Builder().addSchemas(genericSchema).build()).get();
4798 
4799         // Index a document
4800         GenericDocument document =
4801                 new GenericDocument.Builder<>("namespace", "id", "Generic")
4802                     .setPropertyString("subject", "A commonly used fake word is foo. "
4803                             + "Another nonsense word that’s used a lot is bar")
4804                     .build();
4805         checkIsBatchResultSuccess(mDb1.putAsync(
4806             new PutDocumentsRequest.Builder().addGenericDocuments(document).build()));
4807 
4808         // Query with type property paths {"Generic", []}
4809         SearchResults searchResults = mDb1.search("fo",
4810             new SearchSpec.Builder()
4811                 .addFilterSchemas("Generic")
4812                 .setSnippetCount(1)
4813                 .setSnippetCountPerProperty(1)
4814                 .setMaxSnippetSize(10)
4815                 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
4816                 .addProjection("Generic", Collections.emptyList())
4817                 .build());
4818         List<SearchResult> results = searchResults.getNextPageAsync().get();
4819         assertThat(results).hasSize(1);
4820 
4821         List<SearchResult.MatchInfo> matchInfos = results.get(0).getMatchInfos();
4822         assertThat(matchInfos).isEmpty();
4823     }
4824 
4825     @Test
4826     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SEARCH_SPEC_FILTER_DOCUMENT_IDS)
testQuery_documentIdFilter()4827     public void testQuery_documentIdFilter() throws Exception {
4828         assumeTrue(mDb1.getFeatures().isFeatureSupported(
4829                 Features.SEARCH_SPEC_ADD_FILTER_DOCUMENT_IDS));
4830 
4831         // Schema registration
4832         mDb1.setSchemaAsync(
4833                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
4834 
4835         // Index 3 documents
4836         AppSearchEmail email1 =
4837                 new AppSearchEmail.Builder("namespace", "id1")
4838                         .setFrom("from@example.com")
4839                         .setTo("to1@example.com", "to2@example.com")
4840                         .setSubject("testPut example")
4841                         .setBody("This is the body of the testPut email")
4842                         .build();
4843         AppSearchEmail email2 =
4844                 new AppSearchEmail.Builder("namespace", "id2")
4845                         .setFrom("from@example.com")
4846                         .setTo("to1@example.com", "to2@example.com")
4847                         .setSubject("testPut example")
4848                         .setBody("This is the body of the testPut email")
4849                         .build();
4850         AppSearchEmail email3 =
4851                 new AppSearchEmail.Builder("namespace", "id3")
4852                         .setFrom("from@example.com")
4853                         .setTo("to1@example.com", "to2@example.com")
4854                         .setSubject("testPut example")
4855                         .setBody("This is the body of the testPut email")
4856                         .build();
4857         checkIsBatchResultSuccess(mDb1.putAsync(
4858                 new PutDocumentsRequest.Builder()
4859                         .addGenericDocuments(email1, email2, email3).build()));
4860 
4861         // Query for all ids by an empty document id filter.
4862         SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
4863                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
4864                 .build());
4865         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
4866         assertThat(documents).hasSize(3);
4867         assertThat(documents).containsExactly(email1, email2, email3);
4868 
4869         // Query for all ids by explicitly specifying them.
4870         searchResults = mDb1.search("body", new SearchSpec.Builder()
4871                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
4872                 .addFilterDocumentIds(ImmutableSet.of("id1", "id2", "id3"))
4873                 .build());
4874         documents = convertSearchResultsToDocuments(searchResults);
4875         assertThat(documents).hasSize(3);
4876         assertThat(documents).containsExactly(email1, email2, email3);
4877 
4878         // Query only for id1
4879         searchResults = mDb1.search("body",
4880                 new SearchSpec.Builder()
4881                         .addFilterDocumentIds(ImmutableSet.of("id1"))
4882                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
4883                         .build());
4884         documents = convertSearchResultsToDocuments(searchResults);
4885         assertThat(documents).hasSize(1);
4886         assertThat(documents).containsExactly(email1);
4887 
4888         // Query only for id1 and id3
4889         searchResults = mDb1.search("body",
4890                 new SearchSpec.Builder()
4891                         .addFilterDocumentIds(ImmutableSet.of("id1"))
4892                         .addFilterDocumentIds(ImmutableSet.of("id3"))
4893                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
4894                         .build());
4895         documents = convertSearchResultsToDocuments(searchResults);
4896         assertThat(documents).hasSize(2);
4897         assertThat(documents).containsExactly(email1, email3);
4898     }
4899 
4900     @Test
4901     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SEARCH_SPEC_FILTER_DOCUMENT_IDS)
testQuery_documentIdFilter_withNamespaceFilter()4902     public void testQuery_documentIdFilter_withNamespaceFilter() throws Exception {
4903         assumeTrue(mDb1.getFeatures().isFeatureSupported(
4904                 Features.SEARCH_SPEC_ADD_FILTER_DOCUMENT_IDS));
4905 
4906         // Schema registration
4907         mDb1.setSchemaAsync(
4908                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
4909 
4910         // Index 3 documents with "id1" in 2 different namespaces
4911         AppSearchEmail email1 =
4912                 new AppSearchEmail.Builder("namespace1", "id1")
4913                         .setFrom("from@example.com")
4914                         .setTo("to1@example.com", "to2@example.com")
4915                         .setSubject("testPut example")
4916                         .setBody("This is the body of the testPut email")
4917                         .build();
4918         AppSearchEmail email2 =
4919                 new AppSearchEmail.Builder("namespace1", "id2")
4920                         .setFrom("from@example.com")
4921                         .setTo("to1@example.com", "to2@example.com")
4922                         .setSubject("testPut example")
4923                         .setBody("This is the body of the testPut email")
4924                         .build();
4925         AppSearchEmail email3 =
4926                 new AppSearchEmail.Builder("namespace2", "id1")
4927                         .setFrom("from@example.com")
4928                         .setTo("to1@example.com", "to2@example.com")
4929                         .setSubject("testPut example")
4930                         .setBody("This is the body of the testPut email")
4931                         .build();
4932         checkIsBatchResultSuccess(mDb1.putAsync(
4933                 new PutDocumentsRequest.Builder()
4934                         .addGenericDocuments(email1, email2, email3).build()));
4935 
4936         // Query for id1
4937         SearchResults searchResults = mDb1.search("body",
4938                 new SearchSpec.Builder()
4939                         .addFilterDocumentIds(ImmutableSet.of("id1"))
4940                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
4941                         .build());
4942         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
4943         assertThat(documents).hasSize(2);
4944         assertThat(documents).containsExactly(email1, email3);
4945 
4946         // Query only for id1 in namespace1
4947         searchResults = mDb1.search("body",
4948                 new SearchSpec.Builder()
4949                         .addFilterDocumentIds(ImmutableSet.of("id1"))
4950                         .addFilterNamespaces("namespace1")
4951                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
4952                         .build());
4953         documents = convertSearchResultsToDocuments(searchResults);
4954         assertThat(documents).hasSize(1);
4955         assertThat(documents).containsExactly(email1);
4956     }
4957 
4958     @Test
4959     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SEARCH_SPEC_FILTER_DOCUMENT_IDS)
testQuery_documentIdFilter_nonExistentId()4960     public void testQuery_documentIdFilter_nonExistentId() throws Exception {
4961         assumeTrue(mDb1.getFeatures().isFeatureSupported(
4962                 Features.SEARCH_SPEC_ADD_FILTER_DOCUMENT_IDS));
4963 
4964         // Schema registration
4965         mDb1.setSchemaAsync(
4966                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
4967 
4968         // Index 3 documents
4969         AppSearchEmail email1 =
4970                 new AppSearchEmail.Builder("namespace", "id1")
4971                         .setFrom("from@example.com")
4972                         .setTo("to1@example.com", "to2@example.com")
4973                         .setSubject("testPut example")
4974                         .setBody("This is the body of the testPut email")
4975                         .build();
4976         AppSearchEmail email2 =
4977                 new AppSearchEmail.Builder("namespace", "id2")
4978                         .setFrom("from@example.com")
4979                         .setTo("to1@example.com", "to2@example.com")
4980                         .setSubject("testPut example")
4981                         .setBody("This is the body of the testPut email")
4982                         .build();
4983         AppSearchEmail email3 =
4984                 new AppSearchEmail.Builder("namespace", "id3")
4985                         .setFrom("from@example.com")
4986                         .setTo("to1@example.com", "to2@example.com")
4987                         .setSubject("testPut example")
4988                         .setBody("This is the body of the testPut email")
4989                         .build();
4990         checkIsBatchResultSuccess(mDb1.putAsync(
4991                 new PutDocumentsRequest.Builder()
4992                         .addGenericDocuments(email1, email2, email3).build()));
4993 
4994         // Query for a non-existent id, which should return nothing.
4995         SearchResults searchResults = mDb1.search("body",
4996                 new SearchSpec.Builder()
4997                         .addFilterDocumentIds(ImmutableSet.of("nonExistId"))
4998                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
4999                         .build());
5000         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
5001         assertThat(documents).isEmpty();
5002     }
5003 
5004     @Test
5005     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SEARCH_SPEC_FILTER_DOCUMENT_IDS)
testQuery_documentIdFilter_notSupported()5006     public void testQuery_documentIdFilter_notSupported() throws Exception {
5007         assumeFalse(mDb1.getFeatures().isFeatureSupported(
5008                 Features.SEARCH_SPEC_ADD_FILTER_DOCUMENT_IDS));
5009 
5010         UnsupportedOperationException exception = assertThrows(
5011                 UnsupportedOperationException.class,
5012                 () -> mDb1.search("body",
5013                         new SearchSpec.Builder()
5014                                 .addFilterDocumentIds(ImmutableSet.of("id1"))
5015                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
5016                                 .build()));
5017         assertThat(exception).hasMessageThat().contains(
5018                 Features.SEARCH_SPEC_ADD_FILTER_DOCUMENT_IDS
5019                         + " is not available on this AppSearch implementation.");
5020     }
5021 
5022     @Test
testSearchSpec_setSourceTag_notSupported()5023     public void testSearchSpec_setSourceTag_notSupported() {
5024         assumeFalse(mDb1.getFeatures().isFeatureSupported(
5025                 Features.SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG));
5026         // UnsupportedOperationException will be thrown with these queries so no need to
5027         // define a schema and index document.
5028         SearchSpec.Builder builder = new SearchSpec.Builder();
5029         SearchSpec searchSpec = builder.setSearchSourceLogTag("tag").build();
5030 
5031         UnsupportedOperationException exception = assertThrows(
5032                 UnsupportedOperationException.class,
5033                 () -> mDb1.search("\"Hello, world!\"", searchSpec));
5034         assertThat(exception).hasMessageThat().contains(
5035                 Features.SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG
5036                         + " is not available on this AppSearch implementation.");
5037     }
5038 
5039     @Test
testQuery_twoInstances()5040     public void testQuery_twoInstances() throws Exception {
5041         // Schema registration
5042         mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
5043                 .addSchemas(AppSearchEmail.SCHEMA).build()).get();
5044         mDb2.setSchemaAsync(new SetSchemaRequest.Builder()
5045                 .addSchemas(AppSearchEmail.SCHEMA).build()).get();
5046 
5047         // Index a document to instance 1.
5048         AppSearchEmail inEmail1 =
5049                 new AppSearchEmail.Builder("namespace", "id1")
5050                         .setFrom("from@example.com")
5051                         .setTo("to1@example.com", "to2@example.com")
5052                         .setSubject("testPut example")
5053                         .setBody("This is the body of the testPut email")
5054                         .build();
5055         checkIsBatchResultSuccess(mDb1.putAsync(
5056                 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build()));
5057 
5058         // Index a document to instance 2.
5059         AppSearchEmail inEmail2 =
5060                 new AppSearchEmail.Builder("namespace", "id2")
5061                         .setFrom("from@example.com")
5062                         .setTo("to1@example.com", "to2@example.com")
5063                         .setSubject("testPut example")
5064                         .setBody("This is the body of the testPut email")
5065                         .build();
5066         checkIsBatchResultSuccess(mDb2.putAsync(
5067                 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail2).build()));
5068 
5069         // Query for instance 1.
5070         SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
5071                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
5072                 .build());
5073         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
5074         assertThat(documents).hasSize(1);
5075         assertThat(documents).containsExactly(inEmail1);
5076 
5077         // Query for instance 2.
5078         searchResults = mDb2.search("body", new SearchSpec.Builder()
5079                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
5080                 .build());
5081         documents = convertSearchResultsToDocuments(searchResults);
5082         assertThat(documents).hasSize(1);
5083         assertThat(documents).containsExactly(inEmail2);
5084     }
5085 
5086     @Test
testQuery_typePropertyFilters()5087     public void testQuery_typePropertyFilters() throws Exception {
5088         assumeTrue(mDb1.getFeatures().isFeatureSupported(
5089                 Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
5090         // Schema registration
5091         mDb1.setSchemaAsync(
5092                 new SetSchemaRequest.Builder()
5093                         .addSchemas(AppSearchEmail.SCHEMA)
5094                         .build()).get();
5095 
5096         // Index two documents
5097         AppSearchEmail email1 =
5098                 new AppSearchEmail.Builder("namespace", "id1")
5099                         .setCreationTimestampMillis(1000)
5100                         .setFrom("from@example.com")
5101                         .setTo("to1@example.com", "to2@example.com")
5102                         .setSubject("testPut example")
5103                         .setBody("This is the body of the testPut email")
5104                         .build();
5105         AppSearchEmail email2 =
5106                 new AppSearchEmail.Builder("namespace", "id2")
5107                         .setCreationTimestampMillis(1000)
5108                         .setFrom("from@example.com")
5109                         .setTo("to1@example.com", "to2@example.com")
5110                         .setSubject("testPut example subject with some body")
5111                         .setBody("This is the body of the testPut email")
5112                         .build();
5113         checkIsBatchResultSuccess(mDb1.putAsync(
5114                 new PutDocumentsRequest.Builder()
5115                         .addGenericDocuments(email1, email2).build()));
5116 
5117         // Query with type property filters {"Email", ["subject", "to"]}
5118         SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
5119                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
5120                 .addFilterProperties(AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("subject", "to"))
5121                 .build());
5122         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
5123         // Only email2 should be returned because email1 doesn't have the term "body" in subject
5124         // or to fields
5125         assertThat(documents).containsExactly(email2);
5126     }
5127 
5128     @Test
testQuery_typePropertyFiltersWithDifferentSchemaTypes()5129     public void testQuery_typePropertyFiltersWithDifferentSchemaTypes() throws Exception {
5130         assumeTrue(mDb1.getFeatures().isFeatureSupported(
5131                 Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
5132         // Schema registration
5133         mDb1.setSchemaAsync(
5134                 new SetSchemaRequest.Builder()
5135                         .addSchemas(AppSearchEmail.SCHEMA)
5136                         .addSchemas(new AppSearchSchema.Builder("Note")
5137                                 .addProperty(new StringPropertyConfig.Builder("title")
5138                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
5139                                         .setIndexingType(
5140                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
5141                                         .setTokenizerType(
5142                                                 StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
5143                                         .build())
5144                                 .addProperty(new StringPropertyConfig.Builder("body")
5145                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
5146                                         .setIndexingType(
5147                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
5148                                         .setTokenizerType(
5149                                                 StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
5150                                         .build())
5151                                 .build())
5152                         .build()).get();
5153 
5154         // Index two documents
5155         AppSearchEmail email =
5156                 new AppSearchEmail.Builder("namespace", "id1")
5157                         .setCreationTimestampMillis(1000)
5158                         .setFrom("from@example.com")
5159                         .setTo("to1@example.com", "to2@example.com")
5160                         .setSubject("testPut example")
5161                         .setBody("This is the body of the testPut email")
5162                         .build();
5163         GenericDocument note =
5164                 new GenericDocument.Builder<>("namespace", "id2", "Note")
5165                         .setCreationTimestampMillis(1000)
5166                         .setPropertyString("title", "Note title")
5167                         .setPropertyString("body", "Note body").build();
5168         checkIsBatchResultSuccess(mDb1.putAsync(
5169                 new PutDocumentsRequest.Builder()
5170                         .addGenericDocuments(email, note).build()));
5171 
5172         // Query with type property paths {"Email": ["subject", "to"], "Note": ["body"]}. Note
5173         // schema has body in its property filter but Email schema doesn't.
5174         SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
5175                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
5176                 .addFilterProperties(AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("subject", "to"))
5177                 .addFilterProperties("Note", ImmutableList.of("body"))
5178                 .build());
5179         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
5180         // Only the note document should be returned because the email property filter doesn't
5181         // allow searching in the body.
5182         assertThat(documents).containsExactly(note);
5183     }
5184 
5185     @Test
testQuery_typePropertyFiltersWithWildcard()5186     public void testQuery_typePropertyFiltersWithWildcard() throws Exception {
5187         assumeTrue(mDb1.getFeatures().isFeatureSupported(
5188                 Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
5189         // Schema registration
5190         mDb1.setSchemaAsync(
5191                 new SetSchemaRequest.Builder()
5192                         .addSchemas(AppSearchEmail.SCHEMA)
5193                         .addSchemas(new AppSearchSchema.Builder("Note")
5194                                 .addProperty(new StringPropertyConfig.Builder("title")
5195                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
5196                                         .setIndexingType(
5197                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
5198                                         .setTokenizerType(
5199                                                 StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
5200                                         .build())
5201                                 .addProperty(new StringPropertyConfig.Builder("body")
5202                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
5203                                         .setIndexingType(
5204                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
5205                                         .setTokenizerType(
5206                                                 StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
5207                                         .build())
5208                                 .build())
5209                         .build()).get();
5210 
5211         // Index two documents
5212         AppSearchEmail email =
5213                 new AppSearchEmail.Builder("namespace", "id1")
5214                         .setCreationTimestampMillis(1000)
5215                         .setFrom("from@example.com")
5216                         .setTo("to1@example.com", "to2@example.com")
5217                         .setSubject("testPut example subject with some body")
5218                         .setBody("This is the body of the testPut email")
5219                         .build();
5220         GenericDocument note =
5221                 new GenericDocument.Builder<>("namespace", "id2", "Note")
5222                         .setCreationTimestampMillis(1000)
5223                         .setPropertyString("title", "Note title")
5224                         .setPropertyString("body", "Note body").build();
5225         checkIsBatchResultSuccess(mDb1.putAsync(
5226                 new PutDocumentsRequest.Builder()
5227                         .addGenericDocuments(email, note).build()));
5228 
5229         // Query with type property paths {"*": ["subject", "title"]}
5230         SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
5231                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
5232                 .addFilterProperties(SearchSpec.SCHEMA_TYPE_WILDCARD,
5233                         ImmutableList.of("subject", "title"))
5234                 .build());
5235         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
5236         // The wildcard property filter will apply to both the Email and Note schema. The email
5237         // document should be returned since it has the term "body" in its subject property. The
5238         // note document should not be returned since it doesn't have the term "body" in the title
5239         // property (subject property is not applicable for Note schema)
5240         assertThat(documents).containsExactly(email);
5241     }
5242 
5243     @Test
testQuery_typePropertyFiltersWithWildcardAndExplicitSchema()5244     public void testQuery_typePropertyFiltersWithWildcardAndExplicitSchema() throws Exception {
5245         assumeTrue(mDb1.getFeatures().isFeatureSupported(
5246                 Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
5247         // Schema registration
5248         mDb1.setSchemaAsync(
5249                 new SetSchemaRequest.Builder()
5250                         .addSchemas(AppSearchEmail.SCHEMA)
5251                         .addSchemas(new AppSearchSchema.Builder("Note")
5252                                 .addProperty(new StringPropertyConfig.Builder("title")
5253                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
5254                                         .setIndexingType(
5255                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
5256                                         .setTokenizerType(
5257                                                 StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
5258                                         .build())
5259                                 .addProperty(new StringPropertyConfig.Builder("body")
5260                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
5261                                         .setIndexingType(
5262                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
5263                                         .setTokenizerType(
5264                                                 StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
5265                                         .build())
5266                                 .build())
5267                         .build()).get();
5268 
5269         // Index two documents
5270         AppSearchEmail email =
5271                 new AppSearchEmail.Builder("namespace", "id1")
5272                         .setCreationTimestampMillis(1000)
5273                         .setFrom("from@example.com")
5274                         .setTo("to1@example.com", "to2@example.com")
5275                         .setSubject("testPut example subject with some body")
5276                         .setBody("This is the body of the testPut email")
5277                         .build();
5278         GenericDocument note =
5279                 new GenericDocument.Builder<>("namespace", "id2", "Note")
5280                         .setCreationTimestampMillis(1000)
5281                         .setPropertyString("title", "Note title")
5282                         .setPropertyString("body", "Note body").build();
5283         checkIsBatchResultSuccess(mDb1.putAsync(
5284                 new PutDocumentsRequest.Builder()
5285                         .addGenericDocuments(email, note).build()));
5286 
5287         // Query with type property paths {"*": ["subject", "title"], "Note": ["body"]}
5288         SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
5289                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
5290                 .addFilterProperties(SearchSpec.SCHEMA_TYPE_WILDCARD,
5291                         ImmutableList.of("subject", "title"))
5292                 .addFilterProperties("Note", ImmutableList.of("body"))
5293                 .build());
5294         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
5295         // The wildcard property filter will only apply to the Email schema since Note schema has
5296         // its own explicit property filter specified. The email document should be returned since
5297         // it has the term "body" in its subject property. The note document should also be returned
5298         // since it has the term "body" in the body property.
5299         assertThat(documents).containsExactly(email, note);
5300     }
5301 
5302     @Test
testQuery_typePropertyFiltersNonExistentType()5303     public void testQuery_typePropertyFiltersNonExistentType() throws Exception {
5304         assumeTrue(mDb1.getFeatures().isFeatureSupported(
5305                 Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
5306         // Schema registration
5307         mDb1.setSchemaAsync(
5308                 new SetSchemaRequest.Builder()
5309                         .addSchemas(AppSearchEmail.SCHEMA)
5310                         .addSchemas(new AppSearchSchema.Builder("Note")
5311                                 .addProperty(new StringPropertyConfig.Builder("title")
5312                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
5313                                         .setIndexingType(
5314                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
5315                                         .setTokenizerType(
5316                                                 StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
5317                                         .build())
5318                                 .addProperty(new StringPropertyConfig.Builder("body")
5319                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
5320                                         .setIndexingType(
5321                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
5322                                         .setTokenizerType(
5323                                                 StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
5324                                         .build())
5325                                 .build())
5326                         .build()).get();
5327 
5328         // Index two documents
5329         AppSearchEmail email =
5330                 new AppSearchEmail.Builder("namespace", "id1")
5331                         .setCreationTimestampMillis(1000)
5332                         .setFrom("from@example.com")
5333                         .setTo("to1@example.com", "to2@example.com")
5334                         .setSubject("testPut example subject with some body")
5335                         .setBody("This is the body of the testPut email")
5336                         .build();
5337         GenericDocument note =
5338                 new GenericDocument.Builder<>("namespace", "id2", "Note")
5339                         .setCreationTimestampMillis(1000)
5340                         .setPropertyString("title", "Note title")
5341                         .setPropertyString("body", "Note body").build();
5342         checkIsBatchResultSuccess(mDb1.putAsync(
5343                 new PutDocumentsRequest.Builder()
5344                         .addGenericDocuments(email, note).build()));
5345 
5346         // Query with type property paths {"NonExistentType": ["to", "title"]}
5347         SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
5348                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
5349                 .addFilterProperties("NonExistentType", ImmutableList.of("to", "title"))
5350                 .build());
5351         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
5352         // The supplied property filters don't apply to either schema types. Both the documents
5353         // should be returned since the term "body" is present in at least one of their properties.
5354         assertThat(documents).containsExactly(email, note);
5355     }
5356 
5357     @Test
testQuery_typePropertyFiltersEmpty()5358     public void testQuery_typePropertyFiltersEmpty() throws Exception {
5359         assumeTrue(mDb1.getFeatures().isFeatureSupported(
5360                 Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
5361         // Schema registration
5362         mDb1.setSchemaAsync(
5363                 new SetSchemaRequest.Builder()
5364                         .addSchemas(AppSearchEmail.SCHEMA)
5365                         .addSchemas(new AppSearchSchema.Builder("Note")
5366                                 .addProperty(new StringPropertyConfig.Builder("title")
5367                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
5368                                         .setIndexingType(
5369                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
5370                                         .setTokenizerType(
5371                                                 StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
5372                                         .build())
5373                                 .addProperty(new StringPropertyConfig.Builder("body")
5374                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
5375                                         .setIndexingType(
5376                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
5377                                         .setTokenizerType(
5378                                                 StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
5379                                         .build())
5380                                 .build())
5381                         .build()).get();
5382 
5383         // Index two documents
5384         AppSearchEmail email =
5385                 new AppSearchEmail.Builder("namespace", "id1")
5386                         .setCreationTimestampMillis(1000)
5387                         .setFrom("from@example.com")
5388                         .setTo("to1@example.com", "to2@example.com")
5389                         .setSubject("testPut example")
5390                         .setBody("This is the body of the testPut email")
5391                         .build();
5392         GenericDocument note =
5393                 new GenericDocument.Builder<>("namespace", "id2", "Note")
5394                         .setCreationTimestampMillis(1000)
5395                         .setPropertyString("title", "Note title")
5396                         .setPropertyString("body", "Note body").build();
5397         checkIsBatchResultSuccess(mDb1.putAsync(
5398                 new PutDocumentsRequest.Builder()
5399                         .addGenericDocuments(email, note).build()));
5400 
5401         // Query with type property paths {"email": []}
5402         SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
5403                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
5404                 .addFilterProperties(AppSearchEmail.SCHEMA_TYPE, Collections.emptyList())
5405                 .build());
5406         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
5407         // The email document should not be returned since the property filter doesn't allow
5408         // searching any property.
5409         assertThat(documents).containsExactly(note);
5410     }
5411 
5412     @Test
testSnippet()5413     public void testSnippet() throws Exception {
5414         // Schema registration
5415         AppSearchSchema genericSchema = new AppSearchSchema.Builder("Generic")
5416                 .addProperty(new StringPropertyConfig.Builder("subject")
5417                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
5418                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
5419                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
5420                         .build()
5421                 ).build();
5422         mDb1.setSchemaAsync(
5423                 new SetSchemaRequest.Builder().addSchemas(genericSchema).build()).get();
5424 
5425         // Index a document
5426         GenericDocument document =
5427                 new GenericDocument.Builder<>("namespace", "id", "Generic")
5428                         .setPropertyString("subject", "A commonly used fake word is foo. "
5429                                 + "Another nonsense word that’s used a lot is bar")
5430                         .build();
5431         checkIsBatchResultSuccess(mDb1.putAsync(
5432                 new PutDocumentsRequest.Builder().addGenericDocuments(document).build()));
5433 
5434         // Query for the document
5435         SearchResults searchResults = mDb1.search("fo",
5436                 new SearchSpec.Builder()
5437                         .addFilterSchemas("Generic")
5438                         .setSnippetCount(1)
5439                         .setSnippetCountPerProperty(1)
5440                         .setMaxSnippetSize(10)
5441                         .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
5442                         .build());
5443         List<SearchResult> results = searchResults.getNextPageAsync().get();
5444         assertThat(results).hasSize(1);
5445 
5446         List<SearchResult.MatchInfo> matchInfos = results.get(0).getMatchInfos();
5447         assertThat(matchInfos).isNotNull();
5448         assertThat(matchInfos).hasSize(1);
5449         SearchResult.MatchInfo matchInfo = matchInfos.get(0);
5450         assertThat(matchInfo.getFullText()).isEqualTo("A commonly used fake word is foo. "
5451                 + "Another nonsense word that’s used a lot is bar");
5452         assertThat(matchInfo.getExactMatchRange()).isEqualTo(
5453                 new SearchResult.MatchRange(/*start=*/29,  /*end=*/32));
5454         assertThat(matchInfo.getExactMatch().toString()).isEqualTo("foo");
5455         assertThat(matchInfo.getSnippetRange()).isEqualTo(
5456                 new SearchResult.MatchRange(/*start=*/26,  /*end=*/33));
5457         assertThat(matchInfo.getSnippet().toString()).isEqualTo("is foo.");
5458 
5459         if (!mDb1.getFeatures().isFeatureSupported(
5460                 Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH)) {
5461             assertThrows(UnsupportedOperationException.class, matchInfo::getSubmatchRange);
5462             assertThrows(UnsupportedOperationException.class, matchInfo::getSubmatch);
5463         } else {
5464             assertThat(matchInfo.getSubmatchRange()).isEqualTo(
5465                     new SearchResult.MatchRange(/*start=*/29,  /*end=*/31));
5466             assertThat(matchInfo.getSubmatch().toString()).isEqualTo("fo");
5467         }
5468     }
5469 
5470     @Test
5471     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_EMBEDDING_MATCH_INFO)
testSnippet_usingTextMatchInfo()5472     public void testSnippet_usingTextMatchInfo() throws Exception {
5473         // Schema registration
5474         AppSearchSchema genericSchema = new AppSearchSchema.Builder("Generic")
5475                 .addProperty(new StringPropertyConfig.Builder("subject")
5476                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
5477                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
5478                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
5479                         .build()
5480                 ).build();
5481         mDb1.setSchemaAsync(
5482                 new SetSchemaRequest.Builder().addSchemas(genericSchema).build()).get();
5483 
5484         // Index a document
5485         GenericDocument document =
5486                 new GenericDocument.Builder<>("namespace", "id", "Generic")
5487                         .setPropertyString("subject", "A commonly used fake word is foo. "
5488                                 + "Another nonsense word that’s used a lot is bar")
5489                         .build();
5490         checkIsBatchResultSuccess(mDb1.putAsync(
5491                 new PutDocumentsRequest.Builder().addGenericDocuments(document).build()));
5492 
5493         // Query for the document
5494         SearchResults searchResults = mDb1.search("fo",
5495                 new SearchSpec.Builder()
5496                         .addFilterSchemas("Generic")
5497                         .setSnippetCount(1)
5498                         .setSnippetCountPerProperty(1)
5499                         .setMaxSnippetSize(10)
5500                         .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
5501                         .build());
5502         List<SearchResult> results = searchResults.getNextPageAsync().get();
5503         assertThat(results).hasSize(1);
5504 
5505         List<SearchResult.MatchInfo> matchInfos = results.get(0).getMatchInfos();
5506         assertThat(matchInfos).isNotNull();
5507         assertThat(matchInfos).hasSize(1);
5508         SearchResult.MatchInfo matchInfo = matchInfos.get(0);
5509         assertThat(matchInfo.getTextMatch()).isNotNull();
5510         assertThat(matchInfo.getTextMatch().getFullText()).isEqualTo(
5511                 "A commonly used fake word is foo. "
5512                         + "Another nonsense word that’s used a lot is bar");
5513         assertThat(matchInfo.getTextMatch().getExactMatchRange()).isEqualTo(
5514                 new SearchResult.MatchRange(/*start=*/29,  /*end=*/32));
5515         assertThat(matchInfo.getTextMatch().getExactMatch().toString()).isEqualTo("foo");
5516         assertThat(matchInfo.getTextMatch().getSnippetRange()).isEqualTo(
5517                 new SearchResult.MatchRange(/*start=*/26,  /*end=*/33));
5518         assertThat(matchInfo.getTextMatch().getSnippet().toString()).isEqualTo("is foo.");
5519         assertThat(matchInfo.getTextMatch().getSubmatchRange()).isEqualTo(
5520                 new SearchResult.MatchRange(/*start=*/29,  /*end=*/31));
5521         assertThat(matchInfo.getTextMatch().getSubmatch().toString()).isEqualTo("fo");
5522     }
5523 
5524     @Test
testSetSnippetCount()5525     public void testSetSnippetCount() throws Exception {
5526         // Schema registration
5527         AppSearchSchema genericSchema = new AppSearchSchema.Builder("Generic")
5528                 .addProperty(new StringPropertyConfig.Builder("subject")
5529                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
5530                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
5531                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
5532                         .build()
5533                 ).build();
5534         mDb1.setSchemaAsync(
5535                 new SetSchemaRequest.Builder().addSchemas(genericSchema).build()).get();
5536 
5537         // Index documents
5538         checkIsBatchResultSuccess(mDb1.putAsync(new PutDocumentsRequest.Builder()
5539                 .addGenericDocuments(
5540                         new GenericDocument.Builder<>("namespace", "id1", "Generic")
5541                                 .setPropertyString(
5542                                         "subject",
5543                                         "I like cats", "I like dogs", "I like birds", "I like fish")
5544                                 .setScore(10)
5545                                 .build(),
5546                         new GenericDocument.Builder<>("namespace", "id2", "Generic")
5547                                 .setPropertyString(
5548                                         "subject",
5549                                         "I like red",
5550                                         "I like green",
5551                                         "I like blue",
5552                                         "I like yellow")
5553                                 .setScore(20)
5554                                 .build(),
5555                         new GenericDocument.Builder<>("namespace", "id3", "Generic")
5556                                 .setPropertyString(
5557                                         "subject",
5558                                         "I like cupcakes",
5559                                         "I like donuts",
5560                                         "I like eclairs",
5561                                         "I like froyo")
5562                                 .setScore(5)
5563                                 .build())
5564                 .build()));
5565 
5566         // Query for the document
5567         SearchResults searchResults = mDb1.search(
5568                 "like",
5569                 new SearchSpec.Builder()
5570                         .addFilterSchemas("Generic")
5571                         .setSnippetCount(2)
5572                         .setSnippetCountPerProperty(3)
5573                         .setMaxSnippetSize(11)
5574                         .setRankingStrategy(SearchSpec.RANKING_STRATEGY_DOCUMENT_SCORE)
5575                         .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
5576                         .build());
5577 
5578         // Check result 1
5579         List<SearchResult> results = searchResults.getNextPageAsync().get();
5580         assertThat(results).hasSize(3);
5581 
5582         assertThat(results.get(0).getGenericDocument().getId()).isEqualTo("id2");
5583         List<SearchResult.MatchInfo> matchInfos = results.get(0).getMatchInfos();
5584         assertThat(matchInfos).hasSize(3);
5585         assertThat(matchInfos.get(0).getSnippet()).isEqualTo("I like red");
5586         assertThat(matchInfos.get(1).getSnippet()).isEqualTo("I like");
5587         assertThat(matchInfos.get(2).getSnippet()).isEqualTo("I like blue");
5588 
5589         // Check result 2
5590         assertThat(results.get(1).getGenericDocument().getId()).isEqualTo("id1");
5591         matchInfos = results.get(1).getMatchInfos();
5592         assertThat(matchInfos).hasSize(3);
5593         assertThat(matchInfos.get(0).getSnippet()).isEqualTo("I like cats");
5594         assertThat(matchInfos.get(1).getSnippet()).isEqualTo("I like dogs");
5595         assertThat(matchInfos.get(2).getSnippet()).isEqualTo("I like");
5596 
5597         // Check result 2
5598         assertThat(results.get(2).getGenericDocument().getId()).isEqualTo("id3");
5599         matchInfos = results.get(2).getMatchInfos();
5600         assertThat(matchInfos).isEmpty();
5601     }
5602 
5603     @Test
5604     @RequiresFlagsEnabled({Flags.FLAG_ENABLE_EMBEDDING_MATCH_INFO,
5605             Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG})
testEmbeddingSnippet()5606     public void testEmbeddingSnippet() throws Exception {
5607         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_EMBEDDING_MATCH_INFO));
5608         assumeTrue(
5609                 mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG));
5610         // Schema registration
5611         AppSearchSchema schema = new AppSearchSchema.Builder("Email")
5612                 .addProperty(new StringPropertyConfig.Builder("body")
5613                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
5614                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
5615                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
5616                         .build())
5617                 .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding1")
5618                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
5619                         .setIndexingType(
5620                                 AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
5621                         .build())
5622                 .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding2")
5623                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
5624                         .setIndexingType(
5625                                 AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
5626                         .build())
5627                 .build();
5628         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
5629 
5630         // Index documents
5631         EmbeddingVector embedding1Vector =
5632                 new EmbeddingVector(
5633                         new float[]{0.1f, 0.2f, 0.3f, 0.4f, 0.5f},
5634                         "my_model_v1");
5635         EmbeddingVector embedding2Vector0 = new EmbeddingVector(new float[]{0.6f, 0.7f, 0.8f},
5636                 "my_model_v2");
5637         EmbeddingVector embedding2Vector1 = new EmbeddingVector(
5638                 new float[]{-0.1f, -0.2f, -0.3f, 0.4f, 0.5f},
5639                 "my_model_v1");
5640         EmbeddingVector embedding2Vector2 = new EmbeddingVector(
5641                 new float[]{-0.1f, -0.2f, -0.3f, 0.4f, 0.7f},
5642                 "my_model_v1");
5643         GenericDocument doc0 =
5644                 new GenericDocument.Builder<>("namespace", "id0", "Email")
5645                         .setCreationTimestampMillis(1000)
5646                         .setPropertyEmbedding("embedding1", embedding1Vector)
5647                         .setPropertyEmbedding("embedding2", embedding2Vector0, embedding2Vector1,
5648                                 embedding2Vector2)
5649                         .build();
5650         checkIsBatchResultSuccess(mDb1.putAsync(
5651                 new PutDocumentsRequest.Builder().addGenericDocuments(doc0).build()));
5652 
5653 
5654         // Add an embedding search with dot product semantic scores:
5655         // - document 0: -0.5 (embedding1), 0.3 (embedding2[1]), 0.1 (embedding2[2])
5656         EmbeddingVector searchEmbedding = new EmbeddingVector(
5657                 new float[]{1, -1, -1, 1, -1}, "my_model_v1");
5658 
5659         // Match documents that have embeddings with a similarity closer to 0 that is
5660         // greater than -1.
5661         //
5662         // The matched embeddings for each doc are:
5663         // - document 0: -0.5 (embedding1), 0.3 (embedding2)
5664         // The scoring expression for each doc will be evaluated as:
5665         // - document 0: sum({-0.5, 0.3, 0.1}) + sum({}) = -0.1
5666         SearchSpec searchSpec = new SearchSpec.Builder()
5667                 .setDefaultEmbeddingSearchMetricType(
5668                         SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT)
5669                 .addEmbeddingParameters(searchEmbedding)
5670                 .setRankingStrategy(
5671                         "sum(this.matchedSemanticScores(getEmbeddingParameter(0)))")
5672                 .setListFilterQueryLanguageEnabled(true)
5673                 .setSnippetCount(1)
5674                 .setSnippetCountPerProperty(2)
5675                 .setRetrieveEmbeddingMatchInfos(true)
5676                 .build();
5677 
5678         // Verify SearchResults
5679         SearchResults searchResults = mDb1.search(
5680                 "semanticSearch(getEmbeddingParameter(0), -1, 1)", searchSpec);
5681         List<SearchResult> results = retrieveAllSearchResults(searchResults);
5682         assertThat(results).hasSize(1);
5683         assertThat(results.get(0).getGenericDocument()).isEqualTo(doc0);
5684         assertThat(results.get(0).getRankingSignal()).isWithin(0.00001).of(-0.1);
5685 
5686         // Verify MatchInfo
5687         List<SearchResult.MatchInfo> matchInfos = results.get(0).getMatchInfos();
5688         assertThat(matchInfos).isNotNull();
5689         assertThat(matchInfos).hasSize(3);
5690         // embedding 1
5691         SearchResult.MatchInfo matchInfo0 = matchInfos.get(0);
5692         assertThat(matchInfo0.getPropertyPath()).isEqualTo("embedding1");
5693         assertThat(matchInfo0.getTextMatch()).isNull();
5694         assertThat(matchInfo0.getEmbeddingMatch()).isNotNull();
5695         assertThat(matchInfo0.getEmbeddingMatch().getSemanticScore()).isWithin(0.00001).of(-0.5);
5696         assertThat(matchInfo0.getEmbeddingMatch().getQueryEmbeddingVectorIndex()).isEqualTo(0);
5697         assertThat(matchInfo0.getEmbeddingMatch().getEmbeddingSearchMetricType()).isEqualTo(
5698                 SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT);
5699         // Verify that the property path returns the right embedding vector
5700         EmbeddingVector actualVector = doc0.getPropertyEmbedding(matchInfo0.getPropertyPath());
5701         assertThat(actualVector).isEqualTo(embedding1Vector);
5702 
5703         // embedding 2 vector 1
5704         SearchResult.MatchInfo matchInfo1 = matchInfos.get(1);
5705         assertThat(matchInfo1.getPropertyPath()).isEqualTo("embedding2[1]");
5706         assertThat(matchInfo1.getTextMatch()).isNull();
5707         assertThat(matchInfo1.getEmbeddingMatch()).isNotNull();
5708         assertThat(matchInfo1.getEmbeddingMatch().getSemanticScore()).isWithin(0.00001).of(0.3);
5709         assertThat(matchInfo1.getEmbeddingMatch().getQueryEmbeddingVectorIndex()).isEqualTo(0);
5710         assertThat(matchInfo1.getEmbeddingMatch().getEmbeddingSearchMetricType()).isEqualTo(
5711                 SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT);
5712         // Verify that the property path returns the right embedding vector
5713         actualVector = doc0.getPropertyEmbedding(matchInfo1.getPropertyPath());
5714         assertThat(actualVector).isEqualTo(embedding2Vector1);
5715 
5716         // embedding 2 vector 2
5717         SearchResult.MatchInfo matchInfo2 = matchInfos.get(2);
5718         assertThat(matchInfo2.getPropertyPath()).isEqualTo("embedding2[2]");
5719         assertThat(matchInfo2.getTextMatch()).isNull();
5720         assertThat(matchInfo2.getEmbeddingMatch()).isNotNull();
5721         assertThat(matchInfo2.getEmbeddingMatch().getSemanticScore()).isWithin(0.00001).of(0.1);
5722         assertThat(matchInfo2.getEmbeddingMatch().getQueryEmbeddingVectorIndex()).isEqualTo(0);
5723         assertThat(matchInfo2.getEmbeddingMatch().getEmbeddingSearchMetricType()).isEqualTo(
5724                 SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT);
5725         // Verify that the property path returns the right embedding vector
5726         actualVector = doc0.getPropertyEmbedding(matchInfo2.getPropertyPath());
5727         assertThat(actualVector).isEqualTo(embedding2Vector2);
5728     }
5729 
5730     @Test
5731     @RequiresFlagsEnabled({Flags.FLAG_ENABLE_EMBEDDING_MATCH_INFO,
5732             Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG})
testHybridSnippet()5733     public void testHybridSnippet() throws Exception {
5734         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_EMBEDDING_MATCH_INFO));
5735         assumeTrue(
5736                 mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG));
5737         // Schema registration
5738         AppSearchSchema schema = new AppSearchSchema.Builder("Email")
5739                 .addProperty(new StringPropertyConfig.Builder("body")
5740                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
5741                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
5742                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
5743                         .build())
5744                 .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding1")
5745                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
5746                         .setIndexingType(
5747                                 AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
5748                         .build())
5749                 .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding2")
5750                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
5751                         .setIndexingType(
5752                                 AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
5753                         .build())
5754                 .build();
5755         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
5756 
5757         // Index documents
5758         EmbeddingVector embedding1Vector =
5759                 new EmbeddingVector(
5760                         new float[]{0.1f, 0.2f, 0.3f, 0.4f, 0.5f},
5761                         "my_model_v1");
5762         EmbeddingVector embedding2Vector0 = new EmbeddingVector(new float[]{0.6f, 0.7f, 0.8f},
5763                 "my_model_v2");
5764         EmbeddingVector embedding2Vector1 = new EmbeddingVector(
5765                 new float[]{-0.1f, -0.2f, -0.3f, 0.4f, 0.5f},
5766                 "my_model_v1");
5767         EmbeddingVector embedding2Vector2 = new EmbeddingVector(
5768                 new float[]{-0.1f, -0.2f, -0.3f, 0.4f, 0.7f},
5769                 "my_model_v1");
5770         GenericDocument doc0 =
5771                 new GenericDocument.Builder<>("namespace", "id0", "Email")
5772                         .setPropertyString("body", "A commonly used fake word is foo. "
5773                                 + "Another nonsense word that’s used a lot is bar")
5774                         .setCreationTimestampMillis(1000)
5775                         .setPropertyEmbedding("embedding1", embedding1Vector)
5776                         .setPropertyEmbedding("embedding2", embedding2Vector0, embedding2Vector1,
5777                                 embedding2Vector2)
5778                         .build();
5779         checkIsBatchResultSuccess(mDb1.putAsync(
5780                 new PutDocumentsRequest.Builder().addGenericDocuments(doc0).build()));
5781 
5782         // Add an embedding search with dot product semantic scores:
5783         // - document 0: -0.5 (embedding1), 0.3 (embedding2[1]), 0.1 (embedding2[2])
5784         EmbeddingVector searchEmbedding = new EmbeddingVector(
5785                 new float[]{1, -1, -1, 1, -1}, "my_model_v1");
5786 
5787         // Match documents that have embeddings with a similarity closer to 0 that is
5788         // greater than -1.
5789         //
5790         // The matched embeddings for each doc are:
5791         // - document 0: -0.5 (embedding1), 0.3 (embedding2)
5792         // The scoring expression for each doc will be evaluated as:
5793         // - document 0: sum({-0.5, 0.3, 0.1}) + sum({}) = -0.1
5794         SearchSpec searchSpec = new SearchSpec.Builder()
5795                 .setDefaultEmbeddingSearchMetricType(
5796                         SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT)
5797                 .addEmbeddingParameters(searchEmbedding)
5798                 .setRankingStrategy(
5799                         "sum(this.matchedSemanticScores(getEmbeddingParameter(0)))")
5800                 .setListFilterQueryLanguageEnabled(true)
5801                 .setSnippetCount(1)
5802                 .setSnippetCountPerProperty(2)
5803                 .setMaxSnippetSize(11)
5804                 .setRetrieveEmbeddingMatchInfos(true)
5805                 .build();
5806 
5807         // Verify SearchResults
5808         SearchResults searchResults = mDb1.search(
5809                 "fo OR semanticSearch(getEmbeddingParameter(0), -1, 1)", searchSpec);
5810         List<SearchResult> results = retrieveAllSearchResults(searchResults);
5811         assertThat(results).hasSize(1);
5812         assertThat(results.get(0).getGenericDocument()).isEqualTo(doc0);
5813 
5814         // Verify MatchInfo
5815         List<SearchResult.MatchInfo> matchInfos = results.get(0).getMatchInfos();
5816         assertThat(matchInfos).isNotNull();
5817         assertThat(matchInfos).hasSize(4);
5818 
5819         // body - this is first as snippets are returned by sorted property paths order
5820         SearchResult.MatchInfo matchInfo0 = matchInfos.get(0);
5821         assertThat(matchInfo0.getPropertyPath()).isEqualTo("body");
5822         assertThat(matchInfo0.getTextMatch()).isNotNull();
5823         assertThat(matchInfo0.getEmbeddingMatch()).isNull();
5824         assertThat(matchInfo0.getTextMatch().getFullText()).isEqualTo(
5825                 "A commonly used fake word is foo. Another nonsense word that’s used a lot is bar");
5826         assertThat(matchInfo0.getTextMatch().getExactMatchRange()).isEqualTo(
5827                 new SearchResult.MatchRange(/*start=*/29,  /*end=*/32));
5828         assertThat(matchInfo0.getTextMatch().getExactMatch().toString()).isEqualTo("foo");
5829         assertThat(matchInfo0.getTextMatch().getSnippetRange()).isEqualTo(
5830                 new SearchResult.MatchRange(/*start=*/26,  /*end=*/33));
5831         assertThat(matchInfo0.getTextMatch().getSnippet().toString()).isEqualTo("is foo.");
5832         assertThat(matchInfo0.getTextMatch().getSubmatchRange()).isEqualTo(
5833                 new SearchResult.MatchRange(/*start=*/29,  /*end=*/31));
5834         assertThat(matchInfo0.getTextMatch().getSubmatch().toString()).isEqualTo("fo");
5835 
5836         // embedding 1
5837         SearchResult.MatchInfo matchInfo1 = matchInfos.get(1);
5838         assertThat(matchInfo1.getPropertyPath()).isEqualTo("embedding1");
5839         assertThat(matchInfo1.getTextMatch()).isNull();
5840         assertThat(matchInfo1.getEmbeddingMatch()).isNotNull();
5841         assertThat(matchInfo1.getEmbeddingMatch().getSemanticScore()).isWithin(0.00001).of(-0.5);
5842         assertThat(matchInfo1.getEmbeddingMatch().getQueryEmbeddingVectorIndex()).isEqualTo(0);
5843         assertThat(matchInfo1.getEmbeddingMatch().getEmbeddingSearchMetricType()).isEqualTo(
5844                 SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT);
5845         // Verify that the property path returns the right embedding vector
5846         EmbeddingVector actualVector = doc0.getPropertyEmbedding(matchInfo1.getPropertyPath());
5847         assertThat(actualVector).isEqualTo(embedding1Vector);
5848 
5849         // embedding 2 vector 1
5850         SearchResult.MatchInfo matchInfo2 = matchInfos.get(2);
5851         assertThat(matchInfo2.getPropertyPath()).isEqualTo("embedding2[1]");
5852         assertThat(matchInfo2.getTextMatch()).isNull();
5853         assertThat(matchInfo2.getEmbeddingMatch()).isNotNull();
5854         assertThat(matchInfo2.getEmbeddingMatch().getSemanticScore()).isWithin(0.00001).of(0.3);
5855         assertThat(matchInfo2.getEmbeddingMatch().getQueryEmbeddingVectorIndex()).isEqualTo(0);
5856         assertThat(matchInfo2.getEmbeddingMatch().getEmbeddingSearchMetricType()).isEqualTo(
5857                 SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT);
5858         // Verify that the property path returns the right embedding vector
5859         actualVector = doc0.getPropertyEmbedding(matchInfo2.getPropertyPath());
5860         assertThat(actualVector).isEqualTo(embedding2Vector1);
5861 
5862         // embedding 2 vector 2
5863         SearchResult.MatchInfo matchInfo3 = matchInfos.get(3);
5864         assertThat(matchInfo3.getPropertyPath()).isEqualTo("embedding2[2]");
5865         assertThat(matchInfo3.getTextMatch()).isNull();
5866         assertThat(matchInfo3.getEmbeddingMatch()).isNotNull();
5867         assertThat(matchInfo3.getEmbeddingMatch().getSemanticScore()).isWithin(0.00001).of(0.1);
5868         assertThat(matchInfo3.getEmbeddingMatch().getQueryEmbeddingVectorIndex()).isEqualTo(0);
5869         assertThat(matchInfo3.getEmbeddingMatch().getEmbeddingSearchMetricType()).isEqualTo(
5870                 SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT);
5871         // Verify that the property path returns the right embedding vector
5872         actualVector = doc0.getPropertyEmbedding(matchInfo3.getPropertyPath());
5873         assertThat(actualVector).isEqualTo(embedding2Vector2);
5874     }
5875 
5876     @Test
5877     @RequiresFlagsEnabled({Flags.FLAG_ENABLE_EMBEDDING_MATCH_INFO,
5878             Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG})
testEmbeddingSnippet_withSnippetCountPerPropertyLimit()5879     public void testEmbeddingSnippet_withSnippetCountPerPropertyLimit() throws Exception {
5880         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_EMBEDDING_MATCH_INFO));
5881         assumeTrue(
5882                 mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG));
5883         // Schema registration
5884         AppSearchSchema schema = new AppSearchSchema.Builder("Email")
5885                 .addProperty(new StringPropertyConfig.Builder("body")
5886                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
5887                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
5888                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
5889                         .build())
5890                 .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding1")
5891                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
5892                         .setIndexingType(
5893                                 AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
5894                         .build())
5895                 .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding2")
5896                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
5897                         .setIndexingType(
5898                                 AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
5899                         .build())
5900                 .build();
5901         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
5902 
5903         EmbeddingVector embedding1Vector =
5904                 new EmbeddingVector(
5905                         new float[]{0.1f, 0.2f, 0.3f, 0.4f, 0.5f},
5906                         "my_model_v1");
5907         EmbeddingVector embedding2Vector0 = new EmbeddingVector(new float[]{0.6f, 0.7f, 0.8f},
5908                 "my_model_v2");
5909         EmbeddingVector embedding2Vector1 = new EmbeddingVector(
5910                 new float[]{-0.1f, -0.2f, -0.3f, 0.4f, 0.5f},
5911                 "my_model_v1");
5912         EmbeddingVector embedding2Vector2 = new EmbeddingVector(
5913                 new float[]{-0.1f, -0.2f, -0.3f, 0.4f, 0.7f},
5914                 "my_model_v1");
5915 
5916         // Index documents
5917         GenericDocument doc0 =
5918                 new GenericDocument.Builder<>("namespace", "id0", "Email")
5919                         .setCreationTimestampMillis(1000)
5920                         .setPropertyEmbedding("embedding1", embedding1Vector)
5921                         .setPropertyEmbedding("embedding2", embedding2Vector0, embedding2Vector1,
5922                                 embedding2Vector2)
5923                         .build();
5924         checkIsBatchResultSuccess(mDb1.putAsync(
5925                 new PutDocumentsRequest.Builder().addGenericDocuments(doc0).build()));
5926 
5927         // Add an embedding search with dot product semantic scores:
5928         // - document 0: -0.5 (embedding1), 0.3 (embedding2[1]), 0.1(embedding2[2])
5929         EmbeddingVector searchEmbedding = new EmbeddingVector(
5930                 new float[]{1, -1, -1, 1, -1}, "my_model_v1");
5931 
5932         // Match documents that have embeddings with a similarity closer to 0 that is
5933         // greater than -1.
5934         //
5935         // The matched embeddings for each doc are:
5936         // - document 0: -0.5 (embedding1), 0.3 (embedding2[1]), 0.1 (embedding2[2])
5937         // The scoring expression for each doc will be evaluated as:
5938         // - document 0: sum({-0.5, 0.3, 0.1}) + sum({}) = 0.1
5939         //
5940         // Create a searchSpec where snippets get cut off due to the setSnippetCountPerProperty
5941         // limit
5942         SearchSpec searchSpec = new SearchSpec.Builder()
5943                 .setDefaultEmbeddingSearchMetricType(
5944                         SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT)
5945                 .addEmbeddingParameters(searchEmbedding)
5946                 .setRankingStrategy(
5947                         "sum(this.matchedSemanticScores(getEmbeddingParameter(0)))")
5948                 .setListFilterQueryLanguageEnabled(true)
5949                 .setSnippetCount(1)
5950                 .setSnippetCountPerProperty(1)
5951                 .setRetrieveEmbeddingMatchInfos(true)
5952                 .build();
5953 
5954         // Verify SearchResults
5955         SearchResults searchResults = mDb1.search(
5956                 "semanticSearch(getEmbeddingParameter(0), -1, 1)", searchSpec);
5957         List<SearchResult> results = retrieveAllSearchResults(searchResults);
5958         assertThat(results).hasSize(1);
5959         assertThat(results.get(0).getGenericDocument()).isEqualTo(doc0);
5960 
5961         // Verify MatchInfo
5962         List<SearchResult.MatchInfo> matchInfos = results.get(0).getMatchInfos();
5963         assertThat(matchInfos).isNotNull();
5964         // Will only fetch one matchInfo per property
5965         assertThat(matchInfos).hasSize(2);
5966 
5967         // embedding 1
5968         SearchResult.MatchInfo matchInfo0 = matchInfos.get(0);
5969         assertThat(matchInfo0.getPropertyPath()).isEqualTo("embedding1");
5970         assertThat(matchInfo0.getTextMatch()).isNull();
5971         assertThat(matchInfo0.getEmbeddingMatch()).isNotNull();
5972         assertThat(matchInfo0.getEmbeddingMatch().getSemanticScore()).isWithin(0.00001).of(-0.5);
5973         assertThat(matchInfo0.getEmbeddingMatch().getQueryEmbeddingVectorIndex()).isEqualTo(0);
5974         assertThat(matchInfo0.getEmbeddingMatch().getEmbeddingSearchMetricType()).isEqualTo(
5975                 SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT);
5976         // Verify that the property path returns the right embedding vector
5977         EmbeddingVector actualVector = doc0.getPropertyEmbedding(matchInfo0.getPropertyPath());
5978         assertThat(actualVector).isEqualTo(embedding1Vector);
5979 
5980         // embedding 2 vector 1
5981         SearchResult.MatchInfo matchInfo1 = matchInfos.get(1);
5982         assertThat(matchInfo1.getPropertyPath()).isEqualTo("embedding2[1]");
5983         assertThat(matchInfo1.getTextMatch()).isNull();
5984         assertThat(matchInfo1.getEmbeddingMatch()).isNotNull();
5985         assertThat(matchInfo1.getEmbeddingMatch().getSemanticScore()).isWithin(0.00001).of(0.3);
5986         assertThat(matchInfo1.getEmbeddingMatch().getQueryEmbeddingVectorIndex()).isEqualTo(0);
5987         assertThat(matchInfo1.getEmbeddingMatch().getEmbeddingSearchMetricType()).isEqualTo(
5988                 SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT);
5989         // Verify that the property path returns the right embedding vector
5990         actualVector = doc0.getPropertyEmbedding(matchInfo1.getPropertyPath());
5991         assertThat(actualVector).isEqualTo(embedding2Vector1);
5992     }
5993 
5994     @Test
testCJKSnippet()5995     public void testCJKSnippet() throws Exception {
5996         // Schema registration
5997         AppSearchSchema genericSchema = new AppSearchSchema.Builder("Generic")
5998                 .addProperty(new StringPropertyConfig.Builder("subject")
5999                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
6000                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
6001                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
6002                         .build()
6003                 ).build();
6004         mDb1.setSchemaAsync(
6005                 new SetSchemaRequest.Builder().addSchemas(genericSchema).build()).get();
6006 
6007         String japanese =
6008                 "差し出されたのが今日ランドセルでした普通の子であれば満面の笑みで俺を言うでしょうしかし私は赤いランド"
6009                         + "セルを見て笑うことができませんでしたどうしたのと心配そうな仕事ガラスながら渋い顔する私書いたこと言"
6010                         + "うんじゃないのカードとなる声を聞きたい私は目から涙をこぼしながらおじいちゃんの近くにかけおり頭をポ"
6011                         + "ンポンと叩きピンクが良かったんだもん";
6012         // Index a document
6013         GenericDocument document =
6014                 new GenericDocument.Builder<>("namespace", "id", "Generic")
6015                         .setPropertyString("subject", japanese)
6016                         .build();
6017         checkIsBatchResultSuccess(mDb1.putAsync(
6018                 new PutDocumentsRequest.Builder().addGenericDocuments(document).build()));
6019 
6020         // Query for the document
6021         SearchResults searchResults = mDb1.search("は",
6022                 new SearchSpec.Builder()
6023                         .addFilterSchemas("Generic")
6024                         .setSnippetCount(1)
6025                         .setSnippetCountPerProperty(1)
6026                         .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
6027                         .build());
6028         List<SearchResult> results = searchResults.getNextPageAsync().get();
6029         assertThat(results).hasSize(1);
6030 
6031         List<SearchResult.MatchInfo> matchInfos = results.get(0).getMatchInfos();
6032         assertThat(matchInfos).isNotNull();
6033         assertThat(matchInfos).hasSize(1);
6034         SearchResult.MatchInfo matchInfo = matchInfos.get(0);
6035         assertThat(matchInfo.getFullText()).isEqualTo(japanese);
6036         assertThat(matchInfo.getExactMatchRange()).isEqualTo(
6037                 new SearchResult.MatchRange(/*start=*/44,  /*end=*/45));
6038         assertThat(matchInfo.getExactMatch()).isEqualTo("は");
6039 
6040         if (!mDb1.getFeatures().isFeatureSupported(
6041                 Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH)) {
6042             assertThrows(UnsupportedOperationException.class, matchInfo::getSubmatchRange);
6043             assertThrows(UnsupportedOperationException.class, matchInfo::getSubmatch);
6044         } else {
6045             assertThat(matchInfo.getSubmatchRange()).isEqualTo(
6046                     new SearchResult.MatchRange(/*start=*/44,  /*end=*/45));
6047             assertThat(matchInfo.getSubmatch()).isEqualTo("は");
6048         }
6049     }
6050 
6051     @Test
testRemove()6052     public void testRemove() throws Exception {
6053         // Schema registration
6054         mDb1.setSchemaAsync(
6055                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
6056 
6057         // Index documents
6058         AppSearchEmail email1 =
6059                 new AppSearchEmail.Builder("namespace", "id1")
6060                         .setFrom("from@example.com")
6061                         .setTo("to1@example.com", "to2@example.com")
6062                         .setSubject("testPut example")
6063                         .setBody("This is the body of the testPut email")
6064                         .build();
6065         AppSearchEmail email2 =
6066                 new AppSearchEmail.Builder("namespace", "id2")
6067                         .setFrom("from@example.com")
6068                         .setTo("to1@example.com", "to2@example.com")
6069                         .setSubject("testPut example 2")
6070                         .setBody("This is the body of the testPut second email")
6071                         .build();
6072         checkIsBatchResultSuccess(mDb1.putAsync(
6073                 new PutDocumentsRequest.Builder().addGenericDocuments(email1, email2).build()));
6074 
6075         // Check the presence of the documents
6076         assertThat(doGet(mDb1, "namespace", "id1")).hasSize(1);
6077         assertThat(doGet(mDb1, "namespace", "id2")).hasSize(1);
6078 
6079         // Delete the document
6080         checkIsBatchResultSuccess(mDb1.removeAsync(
6081                 new RemoveByDocumentIdRequest.Builder("namespace").addIds(
6082                         "id1").build()));
6083 
6084         // Make sure it's really gone
6085         AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentIdAsync(
6086                         new GetByDocumentIdRequest.Builder("namespace").addIds("id1",
6087                                 "id2").build())
6088                 .get();
6089         assertThat(getResult.isSuccess()).isFalse();
6090         assertThat(getResult.getFailures().get("id1").getResultCode())
6091                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
6092         assertThat(getResult.getSuccesses().get("id2")).isEqualTo(email2);
6093 
6094         // Test if we delete a nonexistent id.
6095         AppSearchBatchResult<String, Void> deleteResult = mDb1.removeAsync(
6096                 new RemoveByDocumentIdRequest.Builder("namespace").addIds(
6097                         "id1").build()).get();
6098 
6099         assertThat(deleteResult.getFailures().get("id1").getResultCode()).isEqualTo(
6100                 AppSearchResult.RESULT_NOT_FOUND);
6101     }
6102 
6103     @Test
testRemove_multipleIds()6104     public void testRemove_multipleIds() throws Exception {
6105         // Schema registration
6106         mDb1.setSchemaAsync(
6107                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
6108 
6109         // Index documents
6110         AppSearchEmail email1 =
6111                 new AppSearchEmail.Builder("namespace", "id1")
6112                         .setFrom("from@example.com")
6113                         .setTo("to1@example.com", "to2@example.com")
6114                         .setSubject("testPut example")
6115                         .setBody("This is the body of the testPut email")
6116                         .build();
6117         AppSearchEmail email2 =
6118                 new AppSearchEmail.Builder("namespace", "id2")
6119                         .setFrom("from@example.com")
6120                         .setTo("to1@example.com", "to2@example.com")
6121                         .setSubject("testPut example 2")
6122                         .setBody("This is the body of the testPut second email")
6123                         .build();
6124         checkIsBatchResultSuccess(mDb1.putAsync(
6125                 new PutDocumentsRequest.Builder().addGenericDocuments(email1, email2).build()));
6126 
6127         // Check the presence of the documents
6128         assertThat(doGet(mDb1, "namespace", "id1")).hasSize(1);
6129         assertThat(doGet(mDb1, "namespace", "id2")).hasSize(1);
6130 
6131         // Delete the document
6132         checkIsBatchResultSuccess(mDb1.removeAsync(
6133                 new RemoveByDocumentIdRequest.Builder("namespace").addIds("id1", "id2").build()));
6134 
6135         // Make sure it's really gone
6136         AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentIdAsync(
6137                         new GetByDocumentIdRequest.Builder("namespace").addIds("id1",
6138                                 "id2").build())
6139                 .get();
6140         assertThat(getResult.isSuccess()).isFalse();
6141         assertThat(getResult.getFailures().get("id1").getResultCode())
6142                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
6143         assertThat(getResult.getFailures().get("id2").getResultCode())
6144                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
6145     }
6146 
6147     @Test
testRemoveByQuery()6148     public void testRemoveByQuery() throws Exception {
6149         // Schema registration
6150         mDb1.setSchemaAsync(
6151                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
6152 
6153         // Index documents
6154         AppSearchEmail email1 =
6155                 new AppSearchEmail.Builder("namespace", "id1")
6156                         .setFrom("from@example.com")
6157                         .setTo("to1@example.com", "to2@example.com")
6158                         .setSubject("foo")
6159                         .setBody("This is the body of the testPut email")
6160                         .build();
6161         AppSearchEmail email2 =
6162                 new AppSearchEmail.Builder("namespace", "id2")
6163                         .setFrom("from@example.com")
6164                         .setTo("to1@example.com", "to2@example.com")
6165                         .setSubject("bar")
6166                         .setBody("This is the body of the testPut second email")
6167                         .build();
6168         checkIsBatchResultSuccess(mDb1.putAsync(
6169                 new PutDocumentsRequest.Builder().addGenericDocuments(email1, email2).build()));
6170 
6171         // Check the presence of the documents
6172         assertThat(doGet(mDb1, "namespace", "id1")).hasSize(1);
6173         assertThat(doGet(mDb1, "namespace", "id2")).hasSize(1);
6174 
6175         // Delete the email 1 by query "foo"
6176         mDb1.removeAsync("foo",
6177                 new SearchSpec.Builder().setTermMatch(SearchSpec.TERM_MATCH_PREFIX).build()).get();
6178         AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentIdAsync(
6179                         new GetByDocumentIdRequest.Builder("namespace")
6180                                 .addIds("id1", "id2").build())
6181                 .get();
6182         assertThat(getResult.isSuccess()).isFalse();
6183         assertThat(getResult.getFailures().get("id1").getResultCode())
6184                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
6185         assertThat(getResult.getSuccesses().get("id2")).isEqualTo(email2);
6186 
6187         // Delete the email 2 by query "bar"
6188         mDb1.removeAsync("bar",
6189                 new SearchSpec.Builder().setTermMatch(SearchSpec.TERM_MATCH_PREFIX).build()).get();
6190         getResult = mDb1.getByDocumentIdAsync(
6191                         new GetByDocumentIdRequest.Builder("namespace").addIds("id2").build())
6192                 .get();
6193         assertThat(getResult.isSuccess()).isFalse();
6194         assertThat(getResult.getFailures().get("id2").getResultCode())
6195                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
6196     }
6197 
6198 
6199     @Test
testRemoveByQuery_nonExistNamespace()6200     public void testRemoveByQuery_nonExistNamespace() throws Exception {
6201         // Schema registration
6202         mDb1.setSchemaAsync(
6203                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
6204 
6205         // Index documents
6206         AppSearchEmail email1 =
6207                 new AppSearchEmail.Builder("namespace1", "id1")
6208                         .setFrom("from@example.com")
6209                         .setTo("to1@example.com", "to2@example.com")
6210                         .setSubject("foo")
6211                         .setBody("This is the body of the testPut email")
6212                         .build();
6213         AppSearchEmail email2 =
6214                 new AppSearchEmail.Builder("namespace2", "id2")
6215                         .setFrom("from@example.com")
6216                         .setTo("to1@example.com", "to2@example.com")
6217                         .setSubject("bar")
6218                         .setBody("This is the body of the testPut second email")
6219                         .build();
6220         checkIsBatchResultSuccess(mDb1.putAsync(
6221                 new PutDocumentsRequest.Builder().addGenericDocuments(email1, email2).build()));
6222 
6223         // Check the presence of the documents
6224         assertThat(doGet(mDb1, "namespace1", "id1")).hasSize(1);
6225         assertThat(doGet(mDb1, "namespace2", "id2")).hasSize(1);
6226 
6227         // Delete the email by nonExist namespace.
6228         mDb1.removeAsync("",
6229                 new SearchSpec.Builder().setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
6230                         .addFilterNamespaces("nonExistNamespace").build()).get();
6231         // None of these emails will be deleted.
6232         assertThat(doGet(mDb1, "namespace1", "id1")).hasSize(1);
6233         assertThat(doGet(mDb1, "namespace2", "id2")).hasSize(1);
6234     }
6235 
6236     @Test
testRemoveByQuery_packageFilter()6237     public void testRemoveByQuery_packageFilter() throws Exception {
6238         // Schema registration
6239         mDb1.setSchemaAsync(
6240                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
6241 
6242         // Index documents
6243         AppSearchEmail email =
6244                 new AppSearchEmail.Builder("namespace", "id1")
6245                         .setFrom("from@example.com")
6246                         .setTo("to1@example.com", "to2@example.com")
6247                         .setSubject("foo")
6248                         .setBody("This is the body of the testPut email")
6249                         .build();
6250         checkIsBatchResultSuccess(mDb1.putAsync(
6251                 new PutDocumentsRequest.Builder().addGenericDocuments(email).build()));
6252 
6253         // Check the presence of the documents
6254         assertThat(doGet(mDb1, "namespace", "id1")).hasSize(1);
6255 
6256         // Try to delete email with query "foo", but restricted to a different package name.
6257         // Won't work and email will still exist.
6258         mDb1.removeAsync("foo",
6259                 new SearchSpec.Builder().setTermMatch(
6260                         SearchSpec.TERM_MATCH_PREFIX).addFilterPackageNames(
6261                         "some.other.package").build()).get();
6262         assertThat(doGet(mDb1, "namespace", "id1")).hasSize(1);
6263 
6264         // Delete the email by query "foo", restricted to the correct package this time.
6265         mDb1.removeAsync("foo", new SearchSpec.Builder().setTermMatch(
6266                 SearchSpec.TERM_MATCH_PREFIX).addFilterPackageNames(
6267                 ApplicationProvider.getApplicationContext().getPackageName()).build()).get();
6268         AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentIdAsync(
6269             new GetByDocumentIdRequest.Builder("namespace").addIds("id1", "id2").build())
6270                 .get();
6271         assertThat(getResult.isSuccess()).isFalse();
6272         assertThat(getResult.getFailures().get("id1").getResultCode())
6273                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
6274     }
6275 
6276     @Test
testRemove_twoInstances()6277     public void testRemove_twoInstances() throws Exception {
6278         // Schema registration
6279         mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
6280                 .addSchemas(AppSearchEmail.SCHEMA).build()).get();
6281 
6282         // Index documents
6283         AppSearchEmail email1 =
6284                 new AppSearchEmail.Builder("namespace", "id1")
6285                         .setFrom("from@example.com")
6286                         .setTo("to1@example.com", "to2@example.com")
6287                         .setSubject("testPut example")
6288                         .setBody("This is the body of the testPut email")
6289                         .build();
6290         checkIsBatchResultSuccess(mDb1.putAsync(
6291                 new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
6292 
6293         // Check the presence of the documents
6294         assertThat(doGet(mDb1, "namespace", "id1")).hasSize(1);
6295 
6296         // Can't delete in the other instance.
6297         AppSearchBatchResult<String, Void> deleteResult = mDb2.removeAsync(
6298                 new RemoveByDocumentIdRequest.Builder("namespace").addIds("id1").build()).get();
6299         assertThat(deleteResult.getFailures().get("id1").getResultCode()).isEqualTo(
6300                 AppSearchResult.RESULT_NOT_FOUND);
6301         assertThat(doGet(mDb1, "namespace", "id1")).hasSize(1);
6302 
6303         // Delete the document
6304         checkIsBatchResultSuccess(mDb1.removeAsync(
6305                 new RemoveByDocumentIdRequest.Builder("namespace").addIds("id1").build()));
6306 
6307         // Make sure it's really gone
6308         AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentIdAsync(
6309                 new GetByDocumentIdRequest.Builder("namespace").addIds("id1").build()).get();
6310         assertThat(getResult.isSuccess()).isFalse();
6311         assertThat(getResult.getFailures().get("id1").getResultCode())
6312                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
6313 
6314         // Test if we delete a nonexistent id.
6315         deleteResult = mDb1.removeAsync(
6316                 new RemoveByDocumentIdRequest.Builder("namespace").addIds("id1").build()).get();
6317         assertThat(deleteResult.getFailures().get("id1").getResultCode()).isEqualTo(
6318                 AppSearchResult.RESULT_NOT_FOUND);
6319     }
6320 
6321     @Test
testRemoveByTypes()6322     public void testRemoveByTypes() throws Exception {
6323         // Schema registration
6324         AppSearchSchema genericSchema = new AppSearchSchema.Builder("Generic").build();
6325         mDb1.setSchemaAsync(
6326                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).addSchemas(
6327                         genericSchema).build()).get();
6328 
6329         // Index documents
6330         AppSearchEmail email1 =
6331                 new AppSearchEmail.Builder("namespace", "id1")
6332                         .setFrom("from@example.com")
6333                         .setTo("to1@example.com", "to2@example.com")
6334                         .setSubject("testPut example")
6335                         .setBody("This is the body of the testPut email")
6336                         .build();
6337         AppSearchEmail email2 =
6338                 new AppSearchEmail.Builder("namespace", "id2")
6339                         .setFrom("from@example.com")
6340                         .setTo("to1@example.com", "to2@example.com")
6341                         .setSubject("testPut example 2")
6342                         .setBody("This is the body of the testPut second email")
6343                         .build();
6344         GenericDocument document1 =
6345                 new GenericDocument.Builder<>("namespace", "id3", "Generic").build();
6346         checkIsBatchResultSuccess(mDb1.putAsync(
6347                 new PutDocumentsRequest.Builder().addGenericDocuments(email1, email2, document1)
6348                         .build()));
6349 
6350         // Check the presence of the documents
6351         assertThat(doGet(mDb1, "namespace", "id1", "id2", "id3")).hasSize(3);
6352 
6353         // Delete the email type
6354         mDb1.removeAsync("", new SearchSpec.Builder()
6355                         .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
6356                         .addFilterSchemas(AppSearchEmail.SCHEMA_TYPE)
6357                         .build())
6358                 .get();
6359 
6360         // Make sure it's really gone
6361         AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentIdAsync(
6362                 new GetByDocumentIdRequest.Builder("namespace").addIds("id1", "id2", "id3").build())
6363                 .get();
6364         assertThat(getResult.isSuccess()).isFalse();
6365         assertThat(getResult.getFailures().get("id1").getResultCode())
6366                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
6367         assertThat(getResult.getFailures().get("id2").getResultCode())
6368                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
6369         assertThat(getResult.getSuccesses().get("id3")).isEqualTo(document1);
6370     }
6371 
6372     @Test
testRemoveByTypes_twoInstances()6373     public void testRemoveByTypes_twoInstances() throws Exception {
6374         // Schema registration
6375         mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
6376                 .addSchemas(AppSearchEmail.SCHEMA).build()).get();
6377         mDb2.setSchemaAsync(new SetSchemaRequest.Builder()
6378                 .addSchemas(AppSearchEmail.SCHEMA).build()).get();
6379 
6380         // Index documents
6381         AppSearchEmail email1 =
6382                 new AppSearchEmail.Builder("namespace", "id1")
6383                         .setFrom("from@example.com")
6384                         .setTo("to1@example.com", "to2@example.com")
6385                         .setSubject("testPut example")
6386                         .setBody("This is the body of the testPut email")
6387                         .build();
6388         AppSearchEmail email2 =
6389                 new AppSearchEmail.Builder("namespace", "id2")
6390                         .setFrom("from@example.com")
6391                         .setTo("to1@example.com", "to2@example.com")
6392                         .setSubject("testPut example 2")
6393                         .setBody("This is the body of the testPut second email")
6394                         .build();
6395         checkIsBatchResultSuccess(mDb1.putAsync(
6396                 new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
6397         checkIsBatchResultSuccess(mDb2.putAsync(
6398                 new PutDocumentsRequest.Builder().addGenericDocuments(email2).build()));
6399 
6400         // Check the presence of the documents
6401         assertThat(doGet(mDb1, "namespace", "id1")).hasSize(1);
6402         assertThat(doGet(mDb2, "namespace", "id2")).hasSize(1);
6403 
6404         // Delete the email type in instance 1
6405         mDb1.removeAsync("", new SearchSpec.Builder()
6406                         .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
6407                         .addFilterSchemas(AppSearchEmail.SCHEMA_TYPE)
6408                         .build())
6409                 .get();
6410 
6411         // Make sure it's really gone in instance 1
6412         AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentIdAsync(
6413                 new GetByDocumentIdRequest.Builder("namespace").addIds("id1").build()).get();
6414         assertThat(getResult.isSuccess()).isFalse();
6415         assertThat(getResult.getFailures().get("id1").getResultCode())
6416                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
6417 
6418         // Make sure it's still in instance 2.
6419         getResult = mDb2.getByDocumentIdAsync(
6420                 new GetByDocumentIdRequest.Builder("namespace").addIds("id2").build()).get();
6421         assertThat(getResult.isSuccess()).isTrue();
6422         assertThat(getResult.getSuccesses().get("id2")).isEqualTo(email2);
6423     }
6424 
6425     @Test
testRemoveByNamespace()6426     public void testRemoveByNamespace() throws Exception {
6427         // Schema registration
6428         AppSearchSchema genericSchema = new AppSearchSchema.Builder("Generic")
6429                 .addProperty(new StringPropertyConfig.Builder("foo")
6430                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
6431                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
6432                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
6433                         .build()
6434                 ).build();
6435         mDb1.setSchemaAsync(
6436                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).addSchemas(
6437                         genericSchema).build()).get();
6438 
6439         // Index documents
6440         AppSearchEmail email1 =
6441                 new AppSearchEmail.Builder("email", "id1")
6442                         .setFrom("from@example.com")
6443                         .setTo("to1@example.com", "to2@example.com")
6444                         .setSubject("testPut example")
6445                         .setBody("This is the body of the testPut email")
6446                         .build();
6447         AppSearchEmail email2 =
6448                 new AppSearchEmail.Builder("email", "id2")
6449                         .setFrom("from@example.com")
6450                         .setTo("to1@example.com", "to2@example.com")
6451                         .setSubject("testPut example 2")
6452                         .setBody("This is the body of the testPut second email")
6453                         .build();
6454         GenericDocument document1 =
6455                 new GenericDocument.Builder<>("document", "id3", "Generic")
6456                         .setPropertyString("foo", "bar").build();
6457         checkIsBatchResultSuccess(mDb1.putAsync(
6458                 new PutDocumentsRequest.Builder().addGenericDocuments(email1, email2, document1)
6459                         .build()));
6460 
6461         // Check the presence of the documents
6462         assertThat(doGet(mDb1, /*namespace=*/"email", "id1", "id2")).hasSize(2);
6463         assertThat(doGet(mDb1, /*namespace=*/"document", "id3")).hasSize(1);
6464 
6465         // Delete the email namespace
6466         mDb1.removeAsync("", new SearchSpec.Builder()
6467                         .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
6468                         .addFilterNamespaces("email")
6469                         .build())
6470                 .get();
6471 
6472         // Make sure it's really gone
6473         AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentIdAsync(
6474                 new GetByDocumentIdRequest.Builder("email")
6475                         .addIds("id1", "id2").build()).get();
6476         assertThat(getResult.isSuccess()).isFalse();
6477         assertThat(getResult.getFailures().get("id1").getResultCode())
6478                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
6479         assertThat(getResult.getFailures().get("id2").getResultCode())
6480                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
6481         getResult = mDb1.getByDocumentIdAsync(
6482                 new GetByDocumentIdRequest.Builder("document")
6483                         .addIds("id3").build()).get();
6484         assertThat(getResult.isSuccess()).isTrue();
6485         assertThat(getResult.getSuccesses().get("id3")).isEqualTo(document1);
6486     }
6487 
6488     @Test
testRemoveByNamespaces_twoInstances()6489     public void testRemoveByNamespaces_twoInstances() throws Exception {
6490         // Schema registration
6491         mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
6492                 .addSchemas(AppSearchEmail.SCHEMA).build()).get();
6493         mDb2.setSchemaAsync(new SetSchemaRequest.Builder()
6494                 .addSchemas(AppSearchEmail.SCHEMA).build()).get();
6495 
6496         // Index documents
6497         AppSearchEmail email1 =
6498                 new AppSearchEmail.Builder("email", "id1")
6499                         .setFrom("from@example.com")
6500                         .setTo("to1@example.com", "to2@example.com")
6501                         .setSubject("testPut example")
6502                         .setBody("This is the body of the testPut email")
6503                         .build();
6504         AppSearchEmail email2 =
6505                 new AppSearchEmail.Builder("email", "id2")
6506                         .setFrom("from@example.com")
6507                         .setTo("to1@example.com", "to2@example.com")
6508                         .setSubject("testPut example 2")
6509                         .setBody("This is the body of the testPut second email")
6510                         .build();
6511         checkIsBatchResultSuccess(mDb1.putAsync(
6512                 new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
6513         checkIsBatchResultSuccess(mDb2.putAsync(
6514                 new PutDocumentsRequest.Builder().addGenericDocuments(email2).build()));
6515 
6516         // Check the presence of the documents
6517         assertThat(doGet(mDb1, /*namespace=*/"email", "id1")).hasSize(1);
6518         assertThat(doGet(mDb2, /*namespace=*/"email", "id2")).hasSize(1);
6519 
6520         // Delete the email namespace in instance 1
6521         mDb1.removeAsync("", new SearchSpec.Builder()
6522                         .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
6523                         .addFilterNamespaces("email")
6524                         .build())
6525                 .get();
6526 
6527         // Make sure it's really gone in instance 1
6528         AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentIdAsync(
6529                 new GetByDocumentIdRequest.Builder("email")
6530                         .addIds("id1").build()).get();
6531         assertThat(getResult.isSuccess()).isFalse();
6532         assertThat(getResult.getFailures().get("id1").getResultCode())
6533                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
6534 
6535         // Make sure it's still in instance 2.
6536         getResult = mDb2.getByDocumentIdAsync(
6537                 new GetByDocumentIdRequest.Builder("email")
6538                         .addIds("id2").build()).get();
6539         assertThat(getResult.isSuccess()).isTrue();
6540         assertThat(getResult.getSuccesses().get("id2")).isEqualTo(email2);
6541     }
6542 
6543     @Test
testRemoveAll_twoInstances()6544     public void testRemoveAll_twoInstances() throws Exception {
6545         // Schema registration
6546         mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
6547                 .addSchemas(AppSearchEmail.SCHEMA).build()).get();
6548         mDb2.setSchemaAsync(new SetSchemaRequest.Builder()
6549                 .addSchemas(AppSearchEmail.SCHEMA).build()).get();
6550 
6551         // Index documents
6552         AppSearchEmail email1 =
6553                 new AppSearchEmail.Builder("namespace", "id1")
6554                         .setFrom("from@example.com")
6555                         .setTo("to1@example.com", "to2@example.com")
6556                         .setSubject("testPut example")
6557                         .setBody("This is the body of the testPut email")
6558                         .build();
6559         AppSearchEmail email2 =
6560                 new AppSearchEmail.Builder("namespace", "id2")
6561                         .setFrom("from@example.com")
6562                         .setTo("to1@example.com", "to2@example.com")
6563                         .setSubject("testPut example 2")
6564                         .setBody("This is the body of the testPut second email")
6565                         .build();
6566         checkIsBatchResultSuccess(mDb1.putAsync(
6567                 new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
6568         checkIsBatchResultSuccess(mDb2.putAsync(
6569                 new PutDocumentsRequest.Builder().addGenericDocuments(email2).build()));
6570 
6571         // Check the presence of the documents
6572         assertThat(doGet(mDb1, "namespace", "id1")).hasSize(1);
6573         assertThat(doGet(mDb2, "namespace", "id2")).hasSize(1);
6574 
6575         // Delete the all document in instance 1
6576         mDb1.removeAsync("", new SearchSpec.Builder()
6577                         .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
6578                         .build())
6579                 .get();
6580 
6581         // Make sure it's really gone in instance 1
6582         AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentIdAsync(
6583                 new GetByDocumentIdRequest.Builder("namespace").addIds("id1").build()).get();
6584         assertThat(getResult.isSuccess()).isFalse();
6585         assertThat(getResult.getFailures().get("id1").getResultCode())
6586                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
6587 
6588         // Make sure it's still in instance 2.
6589         getResult = mDb2.getByDocumentIdAsync(
6590                 new GetByDocumentIdRequest.Builder("namespace").addIds("id2").build()).get();
6591         assertThat(getResult.isSuccess()).isTrue();
6592         assertThat(getResult.getSuccesses().get("id2")).isEqualTo(email2);
6593     }
6594 
6595     @Test
testRemoveAll_termMatchType()6596     public void testRemoveAll_termMatchType() throws Exception {
6597         // Schema registration
6598         mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
6599                 .addSchemas(AppSearchEmail.SCHEMA).build()).get();
6600         mDb2.setSchemaAsync(new SetSchemaRequest.Builder()
6601                 .addSchemas(AppSearchEmail.SCHEMA).build()).get();
6602 
6603         // Index documents
6604         AppSearchEmail email1 =
6605                 new AppSearchEmail.Builder("namespace", "id1")
6606                         .setFrom("from@example.com")
6607                         .setTo("to1@example.com", "to2@example.com")
6608                         .setSubject("testPut example")
6609                         .setBody("This is the body of the testPut email")
6610                         .build();
6611         AppSearchEmail email2 =
6612                 new AppSearchEmail.Builder("namespace", "id2")
6613                         .setFrom("from@example.com")
6614                         .setTo("to1@example.com", "to2@example.com")
6615                         .setSubject("testPut example 2")
6616                         .setBody("This is the body of the testPut second email")
6617                         .build();
6618         AppSearchEmail email3 =
6619                 new AppSearchEmail.Builder("namespace", "id3")
6620                         .setFrom("from@example.com")
6621                         .setTo("to1@example.com", "to2@example.com")
6622                         .setSubject("testPut example 3")
6623                         .setBody("This is the body of the testPut second email")
6624                         .build();
6625         AppSearchEmail email4 =
6626                 new AppSearchEmail.Builder("namespace", "id4")
6627                         .setFrom("from@example.com")
6628                         .setTo("to1@example.com", "to2@example.com")
6629                         .setSubject("testPut example 4")
6630                         .setBody("This is the body of the testPut second email")
6631                         .build();
6632         checkIsBatchResultSuccess(mDb1.putAsync(
6633                 new PutDocumentsRequest.Builder().addGenericDocuments(email1, email2).build()));
6634         checkIsBatchResultSuccess(mDb2.putAsync(
6635                 new PutDocumentsRequest.Builder().addGenericDocuments(email3, email4).build()));
6636 
6637         // Check the presence of the documents
6638         SearchResults searchResults = mDb1.search("", new SearchSpec.Builder()
6639                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
6640                 .build());
6641         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
6642         assertThat(documents).hasSize(2);
6643         searchResults = mDb2.search("", new SearchSpec.Builder()
6644                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
6645                 .build());
6646         documents = convertSearchResultsToDocuments(searchResults);
6647         assertThat(documents).hasSize(2);
6648 
6649         // Delete the all document in instance 1 with TERM_MATCH_PREFIX
6650         mDb1.removeAsync("", new SearchSpec.Builder()
6651                         .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
6652                         .build())
6653                 .get();
6654         searchResults = mDb1.search("", new SearchSpec.Builder()
6655                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
6656                 .build());
6657         documents = convertSearchResultsToDocuments(searchResults);
6658         assertThat(documents).isEmpty();
6659 
6660         // Delete the all document in instance 2 with TERM_MATCH_EXACT_ONLY
6661         mDb2.removeAsync("", new SearchSpec.Builder()
6662                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
6663                         .build())
6664                 .get();
6665         searchResults = mDb2.search("", new SearchSpec.Builder()
6666                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
6667                 .build());
6668         documents = convertSearchResultsToDocuments(searchResults);
6669         assertThat(documents).isEmpty();
6670     }
6671 
6672     @Test
testRemoveAllAfterEmpty()6673     public void testRemoveAllAfterEmpty() throws Exception {
6674         // Schema registration
6675         mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
6676                 .addSchemas(AppSearchEmail.SCHEMA).build()).get();
6677 
6678         // Index documents
6679         AppSearchEmail email1 =
6680                 new AppSearchEmail.Builder("namespace", "id1")
6681                         .setFrom("from@example.com")
6682                         .setTo("to1@example.com", "to2@example.com")
6683                         .setSubject("testPut example")
6684                         .setBody("This is the body of the testPut email")
6685                         .build();
6686         checkIsBatchResultSuccess(mDb1.putAsync(
6687                 new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
6688 
6689         // Check the presence of the documents
6690         assertThat(doGet(mDb1, "namespace", "id1")).hasSize(1);
6691 
6692         // Remove the document
6693         checkIsBatchResultSuccess(
6694                 mDb1.removeAsync(new RemoveByDocumentIdRequest.Builder("namespace").addIds(
6695                         "id1").build()));
6696 
6697         // Make sure it's really gone
6698         AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentIdAsync(
6699                 new GetByDocumentIdRequest.Builder("namespace").addIds("id1").build()).get();
6700         assertThat(getResult.isSuccess()).isFalse();
6701         assertThat(getResult.getFailures().get("id1").getResultCode())
6702                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
6703 
6704         // Delete the all documents
6705         mDb1.removeAsync("", new SearchSpec.Builder()
6706                 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX).build()).get();
6707 
6708         // Make sure it's still gone
6709         getResult = mDb1.getByDocumentIdAsync(
6710                 new GetByDocumentIdRequest.Builder("namespace").addIds("id1").build()).get();
6711         assertThat(getResult.isSuccess()).isFalse();
6712         assertThat(getResult.getFailures().get("id1").getResultCode())
6713                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
6714     }
6715 
6716     @Test
testRemoveQueryWithJoinSpecThrowsException()6717     public void testRemoveQueryWithJoinSpecThrowsException() {
6718         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
6719 
6720         IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
6721                 () -> mDb2.removeAsync("", new SearchSpec.Builder()
6722                         .setJoinSpec(new JoinSpec.Builder("entityId").build())
6723                         .build()));
6724         assertThat(e.getMessage()).isEqualTo("JoinSpec not allowed in removeByQuery, "
6725                 + "but JoinSpec was provided.");
6726     }
6727 
6728     @Test
testCloseAndReopen()6729     public void testCloseAndReopen() throws Exception {
6730         // Schema registration
6731         mDb1.setSchemaAsync(
6732                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
6733 
6734         // Index a document
6735         AppSearchEmail inEmail =
6736                 new AppSearchEmail.Builder("namespace", "id1")
6737                         .setFrom("from@example.com")
6738                         .setTo("to1@example.com", "to2@example.com")
6739                         .setSubject("testPut example")
6740                         .setBody("This is the body of the testPut email")
6741                         .build();
6742         checkIsBatchResultSuccess(mDb1.putAsync(
6743                 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
6744 
6745         // close and re-open the appSearchSession
6746         mDb1.close();
6747         mDb1 = createSearchSessionAsync(DB_NAME_1).get();
6748 
6749         // Query for the document
6750         SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
6751                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
6752                 .build());
6753         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
6754         assertThat(documents).containsExactly(inEmail);
6755     }
6756 
6757     @Test
testCallAfterClose()6758     public void testCallAfterClose() throws Exception {
6759 
6760         // Create a same-thread database by inject an executor which could help us maintain the
6761         // execution order of those async tasks.
6762         Context context = ApplicationProvider.getApplicationContext();
6763         AppSearchSession sameThreadDb = createSearchSessionAsync(
6764                 "sameThreadDb", MoreExecutors.newDirectExecutorService()).get();
6765 
6766         try {
6767             // Schema registration -- just mutate something
6768             sameThreadDb.setSchemaAsync(
6769                     new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
6770 
6771             // Close the database. No further call will be allowed.
6772             sameThreadDb.close();
6773 
6774             // Try to query the closed database
6775             // We are using the same-thread db here to make sure it has been closed.
6776             IllegalStateException e = assertThrows(IllegalStateException.class, () ->
6777                     sameThreadDb.search("query", new SearchSpec.Builder()
6778                             .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
6779                             .build()));
6780             assertThat(e).hasMessageThat().contains("SearchSession has already been closed");
6781         } finally {
6782             // To clean the data that has been added in the test, need to re-open the session and
6783             // set an empty schema.
6784             AppSearchSession reopen = createSearchSessionAsync(
6785                     "sameThreadDb", MoreExecutors.newDirectExecutorService()).get();
6786             reopen.setSchemaAsync(new SetSchemaRequest.Builder()
6787                     .setForceOverride(true).build()).get();
6788         }
6789     }
6790 
6791     @Test
testReportUsage()6792     public void testReportUsage() throws Exception {
6793         mDb1.setSchemaAsync(
6794                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
6795 
6796         // Index two documents.
6797         AppSearchEmail email1 =
6798                 new AppSearchEmail.Builder("namespace", "id1").build();
6799         AppSearchEmail email2 =
6800                 new AppSearchEmail.Builder("namespace", "id2").build();
6801         checkIsBatchResultSuccess(mDb1.putAsync(
6802                 new PutDocumentsRequest.Builder().addGenericDocuments(email1, email2).build()));
6803 
6804         // Email 1 has more usages, but email 2 has more recent usages.
6805         mDb1.reportUsageAsync(new ReportUsageRequest.Builder("namespace", "id1")
6806                 .setUsageTimestampMillis(1000).build()).get();
6807         mDb1.reportUsageAsync(new ReportUsageRequest.Builder("namespace", "id1")
6808                 .setUsageTimestampMillis(2000).build()).get();
6809         mDb1.reportUsageAsync(new ReportUsageRequest.Builder("namespace", "id1")
6810                 .setUsageTimestampMillis(3000).build()).get();
6811         mDb1.reportUsageAsync(new ReportUsageRequest.Builder("namespace", "id2")
6812                 .setUsageTimestampMillis(10000).build()).get();
6813         mDb1.reportUsageAsync(new ReportUsageRequest.Builder("namespace", "id2")
6814                 .setUsageTimestampMillis(20000).build()).get();
6815 
6816         // Query by number of usages
6817         List<SearchResult> results = retrieveAllSearchResults(
6818                 mDb1.search("", new SearchSpec.Builder()
6819                         .setRankingStrategy(SearchSpec.RANKING_STRATEGY_USAGE_COUNT)
6820                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
6821                         .build()));
6822         // Email 1 has three usages and email 2 has two usages.
6823         assertThat(results).hasSize(2);
6824         assertThat(results.get(0).getGenericDocument()).isEqualTo(email1);
6825         assertThat(results.get(1).getGenericDocument()).isEqualTo(email2);
6826         assertThat(results.get(0).getRankingSignal()).isEqualTo(3);
6827         assertThat(results.get(1).getRankingSignal()).isEqualTo(2);
6828 
6829         // Query by most recent usage.
6830         results = retrieveAllSearchResults(
6831                 mDb1.search("", new SearchSpec.Builder()
6832                         .setRankingStrategy(SearchSpec.RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP)
6833                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
6834                         .build()));
6835         assertThat(results).hasSize(2);
6836         assertThat(results.get(0).getGenericDocument()).isEqualTo(email2);
6837         assertThat(results.get(1).getGenericDocument()).isEqualTo(email1);
6838         assertThat(results.get(0).getRankingSignal()).isEqualTo(20000);
6839         assertThat(results.get(1).getRankingSignal()).isEqualTo(3000);
6840     }
6841 
6842     @Test
testReportUsage_invalidNamespace()6843     public void testReportUsage_invalidNamespace() throws Exception {
6844         mDb1.setSchemaAsync(
6845                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
6846         AppSearchEmail email1 = new AppSearchEmail.Builder("namespace", "id1").build();
6847         checkIsBatchResultSuccess(mDb1.putAsync(
6848                 new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
6849 
6850         // Use the correct namespace; it works
6851         mDb1.reportUsageAsync(new ReportUsageRequest.Builder("namespace", "id1").build()).get();
6852 
6853         // Use an incorrect namespace; it fails
6854         ReportUsageRequest reportUsageRequest =
6855                 new ReportUsageRequest.Builder("namespace2", "id1").build();
6856         ExecutionException executionException = assertThrows(ExecutionException.class,
6857                 () -> mDb1.reportUsageAsync(reportUsageRequest).get());
6858         assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
6859         AppSearchException cause = (AppSearchException) executionException.getCause();
6860         assertThat(cause.getResultCode()).isEqualTo(RESULT_NOT_FOUND);
6861     }
6862 
6863     @Test
testGetStorageInfo()6864     public void testGetStorageInfo() throws Exception {
6865         StorageInfo storageInfo = mDb1.getStorageInfoAsync().get();
6866         assertThat(storageInfo.getSizeBytes()).isEqualTo(0);
6867 
6868         mDb1.setSchemaAsync(
6869                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
6870 
6871         // Still no storage space attributed with just a schema
6872         storageInfo = mDb1.getStorageInfoAsync().get();
6873         assertThat(storageInfo.getSizeBytes()).isEqualTo(0);
6874 
6875         // Index two documents.
6876         AppSearchEmail email1 = new AppSearchEmail.Builder("namespace1", "id1").build();
6877         AppSearchEmail email2 = new AppSearchEmail.Builder("namespace1", "id2").build();
6878         AppSearchEmail email3 = new AppSearchEmail.Builder("namespace2", "id1").build();
6879         checkIsBatchResultSuccess(mDb1.putAsync(
6880                 new PutDocumentsRequest.Builder().addGenericDocuments(email1, email2,
6881                         email3).build()));
6882 
6883         // Non-zero size now
6884         storageInfo = mDb1.getStorageInfoAsync().get();
6885         assertThat(storageInfo.getSizeBytes()).isGreaterThan(0);
6886         assertThat(storageInfo.getAliveDocumentsCount()).isEqualTo(3);
6887         assertThat(storageInfo.getAliveNamespacesCount()).isEqualTo(2);
6888     }
6889 
6890     @Test
testFlush()6891     public void testFlush() throws Exception {
6892         // Schema registration
6893         mDb1.setSchemaAsync(
6894                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
6895 
6896         // Index a document
6897         AppSearchEmail email = new AppSearchEmail.Builder("namespace", "id1")
6898                 .setFrom("from@example.com")
6899                 .setTo("to1@example.com", "to2@example.com")
6900                 .setSubject("testPut example")
6901                 .setBody("This is the body of the testPut email")
6902                 .build();
6903 
6904         AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb1.putAsync(
6905                 new PutDocumentsRequest.Builder().addGenericDocuments(email).build()));
6906         assertThat(result.getSuccesses()).containsExactly("id1", null);
6907         assertThat(result.getFailures()).isEmpty();
6908 
6909         // The future returned from requestFlush will be set as a void or an Exception on error.
6910         mDb1.requestFlushAsync().get();
6911     }
6912 
6913     @Test
testQuery_ResultGroupingLimits()6914     public void testQuery_ResultGroupingLimits() throws Exception {
6915         // Schema registration
6916         mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
6917                 .addSchemas(AppSearchEmail.SCHEMA).build()).get();
6918 
6919         // Index four documents.
6920         AppSearchEmail inEmail1 =
6921                 new AppSearchEmail.Builder("namespace1", "id1")
6922                         .setFrom("from@example.com")
6923                         .setTo("to1@example.com", "to2@example.com")
6924                         .setSubject("testPut example")
6925                         .setBody("This is the body of the testPut email")
6926                         .build();
6927         checkIsBatchResultSuccess(mDb1.putAsync(
6928                 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build()));
6929         AppSearchEmail inEmail2 =
6930                 new AppSearchEmail.Builder("namespace1", "id2")
6931                         .setFrom("from@example.com")
6932                         .setTo("to1@example.com", "to2@example.com")
6933                         .setSubject("testPut example")
6934                         .setBody("This is the body of the testPut email")
6935                         .build();
6936         checkIsBatchResultSuccess(mDb1.putAsync(
6937                 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail2).build()));
6938         AppSearchEmail inEmail3 =
6939                 new AppSearchEmail.Builder("namespace2", "id3")
6940                         .setFrom("from@example.com")
6941                         .setTo("to1@example.com", "to2@example.com")
6942                         .setSubject("testPut example")
6943                         .setBody("This is the body of the testPut email")
6944                         .build();
6945         checkIsBatchResultSuccess(mDb1.putAsync(
6946                 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail3).build()));
6947         AppSearchEmail inEmail4 =
6948                 new AppSearchEmail.Builder("namespace2", "id4")
6949                         .setFrom("from@example.com")
6950                         .setTo("to1@example.com", "to2@example.com")
6951                         .setSubject("testPut example")
6952                         .setBody("This is the body of the testPut email")
6953                         .build();
6954         checkIsBatchResultSuccess(mDb1.putAsync(
6955                 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail4).build()));
6956 
6957         // Query with per package result grouping. Only the last document 'email4' should be
6958         // returned.
6959         SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
6960                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
6961                 .setResultGrouping(SearchSpec.GROUPING_TYPE_PER_PACKAGE, /*limit=*/ 1)
6962                 .build());
6963         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
6964         assertThat(documents).containsExactly(inEmail4);
6965 
6966         // Query with per namespace result grouping. Only the last document in each namespace should
6967         // be returned ('email4' and 'email2').
6968         searchResults = mDb1.search("body", new SearchSpec.Builder()
6969                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
6970                 .setResultGrouping(SearchSpec.GROUPING_TYPE_PER_NAMESPACE, /*limit=*/ 1)
6971                 .build());
6972         documents = convertSearchResultsToDocuments(searchResults);
6973         assertThat(documents).containsExactly(inEmail4, inEmail2);
6974 
6975         // Query with per package and per namespace result grouping. Only the last document in each
6976         // namespace should be returned ('email4' and 'email2').
6977         searchResults = mDb1.search("body", new SearchSpec.Builder()
6978                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
6979                 .setResultGrouping(SearchSpec.GROUPING_TYPE_PER_NAMESPACE
6980                         | SearchSpec.GROUPING_TYPE_PER_PACKAGE, /*limit=*/ 1)
6981                 .build());
6982         documents = convertSearchResultsToDocuments(searchResults);
6983         assertThat(documents).containsExactly(inEmail4, inEmail2);
6984     }
6985 
6986     @Test
testQuery_ResultGroupingLimits_SchemaGroupingSupported()6987     public void testQuery_ResultGroupingLimits_SchemaGroupingSupported() throws Exception {
6988         assumeTrue(
6989                 mDb1.getFeatures()
6990                 .isFeatureSupported(Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA));
6991         // Schema registration
6992         AppSearchSchema genericSchema =
6993                 new AppSearchSchema.Builder("Generic")
6994                 .addProperty(
6995                     new StringPropertyConfig.Builder("foo")
6996                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
6997                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
6998                         .setIndexingType(
6999                             StringPropertyConfig.INDEXING_TYPE_PREFIXES)
7000                         .build())
7001                 .build();
7002         mDb1.setSchemaAsync(
7003                 new SetSchemaRequest.Builder()
7004                     .addSchemas(AppSearchEmail.SCHEMA)
7005                     .addSchemas(genericSchema)
7006                     .build())
7007                 .get();
7008 
7009         // Index four documents.
7010         AppSearchEmail inEmail1 =
7011                 new AppSearchEmail.Builder("namespace1", "id1")
7012                 .setFrom("from@example.com")
7013                 .setTo("to1@example.com", "to2@example.com")
7014                 .setSubject("testPut example")
7015                 .setBody("This is the body of the testPut email")
7016                 .build();
7017         checkIsBatchResultSuccess(
7018                 mDb1.putAsync(
7019                     new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build()));
7020         AppSearchEmail inEmail2 =
7021                 new AppSearchEmail.Builder("namespace1", "id2")
7022                 .setFrom("from@example.com")
7023                 .setTo("to1@example.com", "to2@example.com")
7024                 .setSubject("testPut example")
7025                 .setBody("This is the body of the testPut email")
7026                 .build();
7027         checkIsBatchResultSuccess(
7028                 mDb1.putAsync(
7029                     new PutDocumentsRequest.Builder().addGenericDocuments(inEmail2).build()));
7030         AppSearchEmail inEmail3 =
7031                 new AppSearchEmail.Builder("namespace2", "id3")
7032                 .setFrom("from@example.com")
7033                 .setTo("to1@example.com", "to2@example.com")
7034                 .setSubject("testPut example")
7035                 .setBody("This is the body of the testPut email")
7036                 .build();
7037         checkIsBatchResultSuccess(
7038                 mDb1.putAsync(
7039                     new PutDocumentsRequest.Builder().addGenericDocuments(inEmail3).build()));
7040         AppSearchEmail inEmail4 =
7041                 new AppSearchEmail.Builder("namespace2", "id4")
7042                 .setFrom("from@example.com")
7043                 .setTo("to1@example.com", "to2@example.com")
7044                 .setSubject("testPut example")
7045                 .setBody("This is the body of the testPut email")
7046                 .build();
7047         checkIsBatchResultSuccess(
7048                 mDb1.putAsync(
7049                     new PutDocumentsRequest.Builder().addGenericDocuments(inEmail4).build()));
7050         AppSearchEmail inEmail5 =
7051                 new AppSearchEmail.Builder("namespace2", "id5")
7052                 .setFrom("from@example.com")
7053                 .setTo("to1@example.com", "to2@example.com")
7054                 .setSubject("testPut example")
7055                 .setBody("This is the body of the testPut email")
7056                 .build();
7057         checkIsBatchResultSuccess(
7058                 mDb1.putAsync(
7059                     new PutDocumentsRequest.Builder().addGenericDocuments(inEmail5).build()));
7060         GenericDocument inDoc1 =
7061                 new GenericDocument.Builder<>("namespace3", "id6", "Generic")
7062                 .setPropertyString("foo", "body")
7063                 .build();
7064         checkIsBatchResultSuccess(
7065                 mDb1.putAsync(
7066                     new PutDocumentsRequest.Builder().addGenericDocuments(inDoc1).build()));
7067         GenericDocument inDoc2 =
7068                 new GenericDocument.Builder<>("namespace3", "id7", "Generic")
7069                 .setPropertyString("foo", "body")
7070                 .build();
7071         checkIsBatchResultSuccess(
7072                 mDb1.putAsync(
7073                     new PutDocumentsRequest.Builder().addGenericDocuments(inDoc2).build()));
7074         GenericDocument inDoc3 =
7075                 new GenericDocument.Builder<>("namespace4", "id8", "Generic")
7076                 .setPropertyString("foo", "body")
7077                 .build();
7078         checkIsBatchResultSuccess(
7079                 mDb1.putAsync(
7080                     new PutDocumentsRequest.Builder().addGenericDocuments(inDoc3).build()));
7081 
7082         // Query with per package result grouping. Only the last document 'doc3' should be
7083         // returned.
7084         SearchResults searchResults =
7085                 mDb1.search(
7086                 "body",
7087                     new SearchSpec.Builder()
7088                     .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
7089                     .setResultGrouping(
7090                         SearchSpec.GROUPING_TYPE_PER_PACKAGE, /* limit= */ 1)
7091                     .build());
7092         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
7093         assertThat(documents).containsExactly(inDoc3);
7094 
7095         // Query with per namespace result grouping. Only the last document in each namespace should
7096         // be returned ('doc3', 'doc2', 'email5' and 'email2').
7097         searchResults =
7098             mDb1.search(
7099                 "body",
7100                 new SearchSpec.Builder()
7101                     .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
7102                     .setResultGrouping(
7103                         SearchSpec.GROUPING_TYPE_PER_NAMESPACE,
7104                         /* limit= */ 1)
7105                     .build());
7106         documents = convertSearchResultsToDocuments(searchResults);
7107         assertThat(documents).containsExactly(inDoc3, inDoc2, inEmail5, inEmail2);
7108 
7109         // Query with per namespace result grouping. Two of the last documents in each namespace
7110         // should be returned ('doc3', 'doc2', 'doc1', 'email5', 'email4', 'email2', 'email1')
7111         searchResults =
7112             mDb1.search(
7113                 "body",
7114                 new SearchSpec.Builder()
7115                     .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
7116                     .setResultGrouping(
7117                         SearchSpec.GROUPING_TYPE_PER_NAMESPACE,
7118                         /* limit= */ 2)
7119                     .build());
7120         documents = convertSearchResultsToDocuments(searchResults);
7121         assertThat(documents)
7122                 .containsExactly(inDoc3, inDoc2, inDoc1, inEmail5, inEmail4, inEmail2, inEmail1);
7123 
7124         // Query with per schema result grouping. Only the last document of each schema type should
7125         // be returned ('doc3', 'email5')
7126         searchResults =
7127             mDb1.search(
7128                 "body",
7129                 new SearchSpec.Builder()
7130                     .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
7131                     .setResultGrouping(
7132                         SearchSpec.GROUPING_TYPE_PER_SCHEMA, /* limit= */ 1)
7133                     .build());
7134         documents = convertSearchResultsToDocuments(searchResults);
7135         assertThat(documents).containsExactly(inDoc3, inEmail5);
7136 
7137         // Query with per schema result grouping. Only the last two documents of each schema type
7138         // should be returned ('doc3', 'doc2', 'email5', 'email4')
7139         searchResults =
7140             mDb1.search(
7141                 "body",
7142                 new SearchSpec.Builder()
7143                     .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
7144                     .setResultGrouping(
7145                         SearchSpec.GROUPING_TYPE_PER_SCHEMA, /* limit= */ 2)
7146                     .build());
7147         documents = convertSearchResultsToDocuments(searchResults);
7148         assertThat(documents).containsExactly(inDoc3, inDoc2, inEmail5, inEmail4);
7149 
7150         // Query with per package and per namespace result grouping. Only the last document in each
7151         // namespace should be returned ('doc3', 'doc2', 'email5' and 'email2').
7152         searchResults =
7153             mDb1.search(
7154                 "body",
7155                 new SearchSpec.Builder()
7156                     .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
7157                     .setResultGrouping(
7158                         SearchSpec.GROUPING_TYPE_PER_NAMESPACE
7159                             | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
7160                         /* limit= */ 1)
7161                     .build());
7162         documents = convertSearchResultsToDocuments(searchResults);
7163         assertThat(documents).containsExactly(inDoc3, inDoc2, inEmail5, inEmail2);
7164 
7165         // Query with per package and per namespace result grouping. Only the last two documents
7166         // in each namespace should be returned ('doc3', 'doc2', 'doc1', 'email5', 'email4',
7167         // 'email2', 'email1')
7168         searchResults =
7169             mDb1.search(
7170                 "body",
7171                 new SearchSpec.Builder()
7172                     .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
7173                     .setResultGrouping(
7174                         SearchSpec.GROUPING_TYPE_PER_NAMESPACE
7175                             | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
7176                         /* limit= */ 2)
7177                     .build());
7178         documents = convertSearchResultsToDocuments(searchResults);
7179         assertThat(documents)
7180                 .containsExactly(inDoc3, inDoc2, inDoc1, inEmail5, inEmail4, inEmail2, inEmail1);
7181 
7182         // Query with per package and per schema type result grouping. Only the last document in
7183         // each schema type should be returned. ('doc3', 'email5')
7184         searchResults =
7185             mDb1.search(
7186                 "body",
7187                 new SearchSpec.Builder()
7188                     .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
7189                     .setResultGrouping(
7190                         SearchSpec.GROUPING_TYPE_PER_SCHEMA
7191                             | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
7192                         /* limit= */ 1)
7193                     .build());
7194         documents = convertSearchResultsToDocuments(searchResults);
7195         assertThat(documents).containsExactly(inDoc3, inEmail5);
7196 
7197         // Query with per package and per schema type result grouping. Only the last two document in
7198         // each schema type should be returned. ('doc3', 'doc2', 'email5', 'email4')
7199         searchResults =
7200             mDb1.search(
7201                 "body",
7202                 new SearchSpec.Builder()
7203                     .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
7204                     .setResultGrouping(
7205                         SearchSpec.GROUPING_TYPE_PER_SCHEMA
7206                             | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
7207                         /* limit= */ 2)
7208                     .build());
7209         documents = convertSearchResultsToDocuments(searchResults);
7210         assertThat(documents).containsExactly(inDoc3, inDoc2, inEmail5, inEmail4);
7211 
7212         // Query with per namespace and per schema type result grouping. Only the last document in
7213         // each namespace should be returned. ('doc3', 'doc2', 'email5' and 'email2').
7214         searchResults =
7215             mDb1.search(
7216                 "body",
7217                 new SearchSpec.Builder()
7218                     .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
7219                     .setResultGrouping(
7220                         SearchSpec.GROUPING_TYPE_PER_NAMESPACE
7221                             | SearchSpec.GROUPING_TYPE_PER_SCHEMA,
7222                         /* limit= */ 1)
7223                     .build());
7224         documents = convertSearchResultsToDocuments(searchResults);
7225         assertThat(documents).containsExactly(inDoc3, inDoc2, inEmail5, inEmail2);
7226 
7227         // Query with per namespace and per schema type result grouping. Only the last two documents
7228         // in each namespace should be returned. ('doc3', 'doc2', 'doc1', 'email5', 'email4',
7229         // 'email2', 'email1')
7230         searchResults =
7231             mDb1.search(
7232                 "body",
7233                 new SearchSpec.Builder()
7234                     .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
7235                     .setResultGrouping(
7236                         SearchSpec.GROUPING_TYPE_PER_NAMESPACE
7237                             | SearchSpec.GROUPING_TYPE_PER_SCHEMA,
7238                         /* limit= */ 2)
7239                     .build());
7240         documents = convertSearchResultsToDocuments(searchResults);
7241         assertThat(documents)
7242                 .containsExactly(inDoc3, inDoc2, inDoc1, inEmail5, inEmail4, inEmail2, inEmail1);
7243 
7244         // Query with per namespace, per package and per schema type result grouping. Only the last
7245         // document in each namespace should be returned. ('doc3', 'doc2', 'email5' and 'email2')
7246         searchResults =
7247             mDb1.search(
7248                 "body",
7249                 new SearchSpec.Builder()
7250                     .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
7251                     .setResultGrouping(
7252                         SearchSpec.GROUPING_TYPE_PER_NAMESPACE
7253                             | SearchSpec.GROUPING_TYPE_PER_SCHEMA
7254                             | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
7255                         /* limit= */ 1)
7256                     .build());
7257         documents = convertSearchResultsToDocuments(searchResults);
7258         assertThat(documents).containsExactly(inDoc3, inDoc2, inEmail5, inEmail2);
7259 
7260         // Query with per namespace, per package and per schema type result grouping. Only the last
7261         // two documents in each namespace should be returned.('doc3', 'doc2', 'doc1', 'email5',
7262         // 'email4', 'email2', 'email1')
7263         searchResults =
7264             mDb1.search(
7265                 "body",
7266                 new SearchSpec.Builder()
7267                     .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
7268                     .setResultGrouping(
7269                         SearchSpec.GROUPING_TYPE_PER_NAMESPACE
7270                             | SearchSpec.GROUPING_TYPE_PER_SCHEMA
7271                             | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
7272                         /* limit= */ 2)
7273                     .build());
7274         documents = convertSearchResultsToDocuments(searchResults);
7275         assertThat(documents)
7276                 .containsExactly(inDoc3, inDoc2, inDoc1, inEmail5, inEmail4, inEmail2, inEmail1);
7277     }
7278 
7279     @Test
testQuery_ResultGroupingLimits_SchemaGroupingNotSupported()7280     public void testQuery_ResultGroupingLimits_SchemaGroupingNotSupported() throws Exception {
7281         assumeFalse(
7282                 mDb1.getFeatures()
7283                 .isFeatureSupported(Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA));
7284         // Schema registration
7285         mDb1.setSchemaAsync(
7286                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
7287                 .get();
7288 
7289         // Index four documents.
7290         AppSearchEmail inEmail1 =
7291                 new AppSearchEmail.Builder("namespace1", "id1")
7292                 .setFrom("from@example.com")
7293                 .setTo("to1@example.com", "to2@example.com")
7294                 .setSubject("testPut example")
7295                 .setBody("This is the body of the testPut email")
7296                 .build();
7297         checkIsBatchResultSuccess(
7298                 mDb1.putAsync(
7299                     new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build()));
7300         AppSearchEmail inEmail2 =
7301                 new AppSearchEmail.Builder("namespace1", "id2")
7302                 .setFrom("from@example.com")
7303                 .setTo("to1@example.com", "to2@example.com")
7304                 .setSubject("testPut example")
7305                 .setBody("This is the body of the testPut email")
7306                 .build();
7307         checkIsBatchResultSuccess(
7308                 mDb1.putAsync(
7309                     new PutDocumentsRequest.Builder().addGenericDocuments(inEmail2).build()));
7310         AppSearchEmail inEmail3 =
7311                 new AppSearchEmail.Builder("namespace2", "id3")
7312                 .setFrom("from@example.com")
7313                 .setTo("to1@example.com", "to2@example.com")
7314                 .setSubject("testPut example")
7315                 .setBody("This is the body of the testPut email")
7316                 .build();
7317         checkIsBatchResultSuccess(
7318                 mDb1.putAsync(
7319                     new PutDocumentsRequest.Builder().addGenericDocuments(inEmail3).build()));
7320         AppSearchEmail inEmail4 =
7321                 new AppSearchEmail.Builder("namespace2", "id4")
7322                 .setFrom("from@example.com")
7323                 .setTo("to1@example.com", "to2@example.com")
7324                 .setSubject("testPut example")
7325                 .setBody("This is the body of the testPut email")
7326                 .build();
7327         checkIsBatchResultSuccess(
7328                 mDb1.putAsync(
7329                     new PutDocumentsRequest.Builder().addGenericDocuments(inEmail4).build()));
7330 
7331         // SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA is not supported.
7332         // UnsupportedOperationException will be thrown.
7333         SearchSpec searchSpec1 =
7334                 new SearchSpec.Builder()
7335                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
7336                 .setResultGrouping(
7337                     SearchSpec.GROUPING_TYPE_PER_SCHEMA, /* limit= */ 1)
7338                 .build();
7339         UnsupportedOperationException exception =
7340                 assertThrows(
7341                 UnsupportedOperationException.class,
7342                     () -> mDb1.search("body", searchSpec1));
7343         assertThat(exception)
7344                 .hasMessageThat()
7345                 .contains(
7346                     Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA
7347                     + " is not available on this"
7348                     + " AppSearch implementation.");
7349 
7350         // SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA is not supported.
7351         // UnsupportedOperationException will be thrown.
7352         SearchSpec searchSpec2 =
7353                 new SearchSpec.Builder()
7354                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
7355                 .setResultGrouping(
7356                     SearchSpec.GROUPING_TYPE_PER_PACKAGE
7357                         | SearchSpec.GROUPING_TYPE_PER_SCHEMA,
7358                     /* limit= */ 1)
7359                 .build();
7360         exception =
7361             assertThrows(
7362                 UnsupportedOperationException.class,
7363                 () -> mDb1.search("body", searchSpec2));
7364         assertThat(exception)
7365                 .hasMessageThat()
7366                 .contains(
7367                     Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA
7368                     + " is not available on this"
7369                     + " AppSearch implementation.");
7370 
7371         // SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA is not supported.
7372         // UnsupportedOperationException will be thrown.
7373         SearchSpec searchSpec3 =
7374                 new SearchSpec.Builder()
7375                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
7376                 .setResultGrouping(
7377                     SearchSpec.GROUPING_TYPE_PER_NAMESPACE
7378                         | SearchSpec.GROUPING_TYPE_PER_SCHEMA,
7379                     /* limit= */ 1)
7380                 .build();
7381         exception =
7382             assertThrows(
7383                 UnsupportedOperationException.class,
7384                 () -> mDb1.search("body", searchSpec3));
7385         assertThat(exception)
7386                 .hasMessageThat()
7387                 .contains(
7388                     Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA
7389                     + " is not available on this"
7390                     + " AppSearch implementation.");
7391 
7392         // SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA is not supported.
7393         // UnsupportedOperationException will be thrown.
7394         SearchSpec searchSpec4 =
7395                 new SearchSpec.Builder()
7396                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
7397                 .setResultGrouping(
7398                     SearchSpec.GROUPING_TYPE_PER_NAMESPACE
7399                         | SearchSpec.GROUPING_TYPE_PER_SCHEMA
7400                         | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
7401                     /* limit= */ 1)
7402                 .build();
7403         exception =
7404             assertThrows(
7405                 UnsupportedOperationException.class,
7406                 () -> mDb1.search("body", searchSpec4));
7407         assertThat(exception)
7408                 .hasMessageThat()
7409                 .contains(
7410                     Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA
7411                     + " is not available on this"
7412                     + " AppSearch implementation.");
7413     }
7414 
7415     @Test
testIndexNestedDocuments()7416     public void testIndexNestedDocuments() throws Exception {
7417         // Schema registration
7418         mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
7419                         .addSchemas(AppSearchEmail.SCHEMA)
7420                         .addSchemas(new AppSearchSchema.Builder("YesNestedIndex")
7421                                 .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(
7422                                         "prop", AppSearchEmail.SCHEMA_TYPE)
7423                                         .setShouldIndexNestedProperties(true)
7424                                         .build())
7425                                 .build())
7426                         .addSchemas(new AppSearchSchema.Builder("NoNestedIndex")
7427                                 .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(
7428                                         "prop", AppSearchEmail.SCHEMA_TYPE)
7429                                         .setShouldIndexNestedProperties(false)
7430                                         .build())
7431                                 .build())
7432                         .build())
7433                 .get();
7434 
7435         // Index the documents.
7436         AppSearchEmail email = new AppSearchEmail.Builder("", "")
7437                 .setSubject("This is the body")
7438                 .build();
7439         GenericDocument yesNestedIndex =
7440                 new GenericDocument.Builder<>("namespace", "yesNestedIndex", "YesNestedIndex")
7441                         .setPropertyDocument("prop", email)
7442                         .build();
7443         GenericDocument noNestedIndex =
7444                 new GenericDocument.Builder<>("namespace", "noNestedIndex", "NoNestedIndex")
7445                         .setPropertyDocument("prop", email)
7446                         .build();
7447         checkIsBatchResultSuccess(mDb1.putAsync(new PutDocumentsRequest.Builder()
7448                 .addGenericDocuments(yesNestedIndex, noNestedIndex).build()));
7449 
7450         // Query.
7451         SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
7452                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
7453                 .setSnippetCount(10)
7454                 .setSnippetCountPerProperty(10)
7455                 .build());
7456         List<SearchResult> page = searchResults.getNextPageAsync().get();
7457         assertThat(page).hasSize(1);
7458         assertThat(page.get(0).getGenericDocument()).isEqualTo(yesNestedIndex);
7459         List<SearchResult.MatchInfo> matches = page.get(0).getMatchInfos();
7460         assertThat(matches).hasSize(1);
7461         assertThat(matches.get(0).getPropertyPath()).isEqualTo("prop.subject");
7462         assertThat(matches.get(0).getPropertyPathObject())
7463                 .isEqualTo(new PropertyPath("prop.subject"));
7464         assertThat(matches.get(0).getFullText()).isEqualTo("This is the body");
7465         assertThat(matches.get(0).getExactMatch()).isEqualTo("body");
7466     }
7467 
7468     @Test
testCJKTQuery()7469     public void testCJKTQuery() throws Exception {
7470         // Schema registration
7471         mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
7472                 .addSchemas(AppSearchEmail.SCHEMA).build()).get();
7473 
7474         // Index a document to instance 1.
7475         AppSearchEmail inEmail1 =
7476                 new AppSearchEmail.Builder("namespace", "uri1")
7477                         .setBody("他是個男孩 is a boy")
7478                         .build();
7479         checkIsBatchResultSuccess(mDb1.putAsync(
7480                 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build()));
7481 
7482         // Query for "他" (He)
7483         SearchResults searchResults = mDb1.search("他", new SearchSpec.Builder()
7484                 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
7485                 .build());
7486         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
7487         assertThat(documents).containsExactly(inEmail1);
7488 
7489         // Query for "男孩" (boy)
7490         searchResults = mDb1.search("男孩", new SearchSpec.Builder()
7491                 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
7492                 .build());
7493         documents = convertSearchResultsToDocuments(searchResults);
7494         assertThat(documents).containsExactly(inEmail1);
7495 
7496         // Query for "boy"
7497         searchResults = mDb1.search("boy", new SearchSpec.Builder()
7498                 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
7499                 .build());
7500         documents = convertSearchResultsToDocuments(searchResults);
7501         assertThat(documents).containsExactly(inEmail1);
7502     }
7503 
7504     @Test
testSetSchemaWithIncompatibleNestedSchema()7505     public void testSetSchemaWithIncompatibleNestedSchema() throws Exception {
7506         // 1. Set the original schema. This should succeed without any problems.
7507         AppSearchSchema originalNestedSchema =
7508                 new AppSearchSchema.Builder("TypeA").addProperty(new StringPropertyConfig.Builder(
7509                         "prop1").setCardinality(
7510                         PropertyConfig.CARDINALITY_OPTIONAL).build()).build();
7511         SetSchemaRequest originalRequest =
7512                 new SetSchemaRequest.Builder().addSchemas(originalNestedSchema).build();
7513         mDb1.setSchemaAsync(originalRequest).get();
7514 
7515         // 2. Set a new schema with a new type that refers to "TypeA" and an incompatible change to
7516         // "TypeA". This should fail.
7517         AppSearchSchema newNestedSchema =
7518                 new AppSearchSchema.Builder("TypeA").addProperty(new StringPropertyConfig.Builder(
7519                         "prop1").setCardinality(
7520                         PropertyConfig.CARDINALITY_REQUIRED).build()).build();
7521         AppSearchSchema newSchema =
7522                 new AppSearchSchema.Builder("TypeB").addProperty(
7523                         new AppSearchSchema.DocumentPropertyConfig.Builder("prop2",
7524                                 "TypeA").build()).build();
7525         final SetSchemaRequest newRequest =
7526                 new SetSchemaRequest.Builder().addSchemas(newNestedSchema,
7527                         newSchema).build();
7528         ExecutionException executionException = assertThrows(ExecutionException.class,
7529                 () -> mDb1.setSchemaAsync(newRequest).get());
7530         assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
7531         AppSearchException exception = (AppSearchException) executionException.getCause();
7532         assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_SCHEMA);
7533         assertThat(exception).hasMessageThat().contains("Schema is incompatible.");
7534         assertThat(exception).hasMessageThat().contains("Incompatible types: {TypeA}");
7535 
7536         // 3. Now set that same set of schemas but with forceOverride=true. This should succeed.
7537         SetSchemaRequest newRequestForced =
7538                 new SetSchemaRequest.Builder().addSchemas(newNestedSchema,
7539                         newSchema).setForceOverride(true).build();
7540         mDb1.setSchemaAsync(newRequestForced).get();
7541     }
7542 
7543     @Test
testEmojiSnippet()7544     public void testEmojiSnippet() throws Exception {
7545         // Schema registration
7546         mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
7547                 .addSchemas(AppSearchEmail.SCHEMA).build()).get();
7548 
7549         // String:     "Luca Brasi sleeps with the ������."
7550         //              ^    ^     ^      ^    ^   ^ ^  ^ ^
7551         // UTF8 idx:    0    5     11     18   23 27 3135 39
7552         // UTF16 idx:   0    5     11     18   23 27 2931 33
7553         // Breaks into segments: "Luca", "Brasi", "sleeps", "with", "the", "��", "��"
7554         // and "��".
7555         // Index a document to instance 1.
7556         String sicilianMessage = "Luca Brasi sleeps with the ������.";
7557         AppSearchEmail inEmail1 =
7558                 new AppSearchEmail.Builder("namespace", "uri1")
7559                         .setBody(sicilianMessage)
7560                         .build();
7561         checkIsBatchResultSuccess(mDb1.putAsync(
7562                 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build()));
7563 
7564         AppSearchEmail inEmail2 =
7565                 new AppSearchEmail.Builder("namespace", "uri2")
7566                         .setBody("Some other content.")
7567                         .build();
7568         checkIsBatchResultSuccess(mDb1.putAsync(
7569                 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail2).build()));
7570 
7571         // Query for "��"
7572         SearchResults searchResults = mDb1.search("��", new SearchSpec.Builder()
7573                 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
7574                 .setSnippetCount(1)
7575                 .setSnippetCountPerProperty(1)
7576                 .build());
7577         List<SearchResult> page = searchResults.getNextPageAsync().get();
7578         assertThat(page).hasSize(1);
7579         assertThat(page.get(0).getGenericDocument()).isEqualTo(inEmail1);
7580         List<SearchResult.MatchInfo> matches = page.get(0).getMatchInfos();
7581         assertThat(matches).hasSize(1);
7582         assertThat(matches.get(0).getPropertyPath()).isEqualTo("body");
7583         assertThat(matches.get(0).getFullText()).isEqualTo(sicilianMessage);
7584         assertThat(matches.get(0).getExactMatch()).isEqualTo("��");
7585         if (mDb1.getFeatures().isFeatureSupported(Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH)) {
7586             assertThat(matches.get(0).getSubmatch()).isEqualTo("��");
7587         }
7588     }
7589 
7590     @Test
testRfc822()7591     public void testRfc822() throws Exception {
7592         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.TOKENIZER_TYPE_RFC822));
7593         AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email")
7594                 .addProperty(new StringPropertyConfig.Builder("address")
7595                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
7596                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
7597                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_RFC822)
7598                         .build()
7599                 ).build();
7600         mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
7601                 .setForceOverride(true).addSchemas(emailSchema).build()).get();
7602 
7603         GenericDocument email = new GenericDocument.Builder<>("NS", "alex1", "Email")
7604                 .setPropertyString("address", "Alex Saveliev <alex.sav@google.com>")
7605                 .build();
7606         mDb1.putAsync(new PutDocumentsRequest.Builder().addGenericDocuments(email).build()).get();
7607 
7608         SearchResults sr = mDb1.search("com", new SearchSpec.Builder().build());
7609         List<SearchResult> page = sr.getNextPageAsync().get();
7610 
7611         // RFC tokenization will produce the following tokens for
7612         // "Alex Saveliev <alex.sav@google.com>" : ["Alex Saveliev <alex.sav@google.com>", "Alex",
7613         // "Saveliev", "alex.sav", "alex.sav@google.com", "alex.sav", "google", "com"]. Therefore,
7614         // a query for "com" should match the document.
7615         assertThat(page).hasSize(1);
7616         assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("alex1");
7617 
7618         // Plain tokenizer will not match this
7619         AppSearchSchema plainEmailSchema = new AppSearchSchema.Builder("Email")
7620                 .addProperty(new StringPropertyConfig.Builder("address")
7621                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
7622                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
7623                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
7624                         .build()
7625                 ).build();
7626 
7627         // Flipping the tokenizer type is a backwards compatible change. The index will be
7628         // rebuilt with the email doc being tokenized in the new way.
7629         mDb1.setSchemaAsync(
7630                 new SetSchemaRequest.Builder().addSchemas(plainEmailSchema).build()).get();
7631 
7632         sr = mDb1.search("com", new SearchSpec.Builder().build());
7633 
7634         // Plain tokenization will produce the following tokens for
7635         // "Alex Saveliev <alex.sav@google.com>" : ["Alex", "Saveliev", "<", "alex.sav",
7636         // "google.com", ">"]. So "com" will not match any of the tokens produced.
7637         assertThat(sr.getNextPageAsync().get()).hasSize(0);
7638     }
7639 
7640     @Test
testRfc822_unsupportedFeature_throwsException()7641     public void testRfc822_unsupportedFeature_throwsException() {
7642         assumeFalse(mDb1.getFeatures().isFeatureSupported(Features.TOKENIZER_TYPE_RFC822));
7643 
7644         AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email")
7645                 .addProperty(new StringPropertyConfig.Builder("address")
7646                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
7647                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
7648                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_RFC822)
7649                         .build()
7650                 ).build();
7651 
7652         Exception e = assertThrows(IllegalArgumentException.class, () ->
7653                 mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
7654                         .setForceOverride(true).addSchemas(emailSchema).build()).get());
7655         assertThat(e.getMessage()).isEqualTo("tokenizerType is out of range of [0, 1] (too high)");
7656     }
7657 
7658 
7659     @Test
testQuery_verbatimSearch()7660     public void testQuery_verbatimSearch() throws Exception {
7661         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.VERBATIM_SEARCH));
7662         AppSearchSchema verbatimSchema = new AppSearchSchema.Builder("VerbatimSchema")
7663                 .addProperty(new StringPropertyConfig.Builder("verbatimProp")
7664                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
7665                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
7666                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_VERBATIM)
7667                         .build()
7668                 ).build();
7669         mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
7670                 .setForceOverride(true).addSchemas(verbatimSchema).build()).get();
7671 
7672         GenericDocument email = new GenericDocument.Builder<>(
7673                 "namespace1", "id1", "VerbatimSchema")
7674                 .setPropertyString("verbatimProp", "Hello, world!")
7675                 .build();
7676         mDb1.putAsync(new PutDocumentsRequest.Builder().addGenericDocuments(email).build()).get();
7677 
7678         SearchResults sr = mDb1.search("\"Hello, world!\"",
7679                 new SearchSpec.Builder().setVerbatimSearchEnabled(true).build());
7680         List<SearchResult> page = sr.getNextPageAsync().get();
7681 
7682         // Verbatim tokenization would produce one token 'Hello, world!'.
7683         assertThat(page).hasSize(1);
7684         assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id1");
7685     }
7686 
7687     @Test
testQuery_verbatimSearchWithoutEnablingFeatureFails()7688     public void testQuery_verbatimSearchWithoutEnablingFeatureFails() throws Exception {
7689         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.VERBATIM_SEARCH));
7690         AppSearchSchema verbatimSchema = new AppSearchSchema.Builder("VerbatimSchema")
7691                 .addProperty(new StringPropertyConfig.Builder("verbatimProp")
7692                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
7693                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
7694                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_VERBATIM)
7695                         .build()
7696                 ).build();
7697         mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
7698                 .setForceOverride(true).addSchemas(verbatimSchema).build()).get();
7699 
7700         GenericDocument email = new GenericDocument.Builder<>(
7701                 "namespace1", "id1", "VerbatimSchema")
7702                 .setPropertyString("verbatimProp", "Hello, world!")
7703                 .build();
7704         mDb1.putAsync(new PutDocumentsRequest.Builder().addGenericDocuments(email).build()).get();
7705 
7706         // Disable VERBATIM_SEARCH in the SearchSpec.
7707         SearchResults searchResults = mDb1.search("\"Hello, world!\"",
7708                 new SearchSpec.Builder()
7709                         .setVerbatimSearchEnabled(false)
7710                         .build());
7711         ExecutionException executionException = assertThrows(ExecutionException.class,
7712                 () -> searchResults.getNextPageAsync().get());
7713         assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
7714         AppSearchException exception = (AppSearchException) executionException.getCause();
7715         assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT);
7716         assertThat(exception).hasMessageThat().contains("Attempted use of unenabled feature");
7717         assertThat(exception).hasMessageThat().contains(Features.VERBATIM_SEARCH);
7718     }
7719 
7720     @Test
testQuery_listFilterQueryWithEnablingFeatureSucceeds()7721     public void testQuery_listFilterQueryWithEnablingFeatureSucceeds() throws Exception {
7722         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_QUERY_LANGUAGE));
7723         AppSearchSchema schema = new AppSearchSchema.Builder("Schema")
7724                 .addProperty(new StringPropertyConfig.Builder("prop")
7725                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
7726                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
7727                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
7728                         .build()
7729                 ).build();
7730         mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
7731                 .setForceOverride(true).addSchemas(schema).build()).get();
7732 
7733         GenericDocument email = new GenericDocument.Builder<>(
7734                 "namespace1", "id1", "Schema")
7735                 .setPropertyString("prop", "Hello, world!")
7736                 .build();
7737         mDb1.putAsync(new PutDocumentsRequest.Builder().addGenericDocuments(email).build()).get();
7738 
7739         SearchSpec searchSpec = new SearchSpec.Builder()
7740                 .setListFilterQueryLanguageEnabled(true)
7741                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
7742                 .build();
7743         // Support for function calls `search`, `createList` was added in list filters
7744         SearchResults searchResults = mDb1.search("search(\"hello\", createList(\"prop\"))",
7745                 searchSpec);
7746         List<SearchResult> page = searchResults.getNextPageAsync().get();
7747         assertThat(page).hasSize(1);
7748         assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id1");
7749 
7750         // Support for prefix operator * was added in list filters.
7751         searchResults = mDb1.search("wor*", searchSpec);
7752         page = searchResults.getNextPageAsync().get();
7753         assertThat(page).hasSize(1);
7754         assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id1");
7755 
7756         // Combining negations with compound statements and property restricts was added in list
7757         // filters.
7758         searchResults = mDb1.search("NOT (foo OR otherProp:hello)", searchSpec);
7759         page = searchResults.getNextPageAsync().get();
7760         assertThat(page).hasSize(1);
7761         assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id1");
7762     }
7763 
7764     @Test
testQuery_PropertyDefinedWithEnablingFeatureSucceeds()7765     public void testQuery_PropertyDefinedWithEnablingFeatureSucceeds() throws Exception {
7766         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_QUERY_LANGUAGE));
7767         AppSearchSchema schema1 = new AppSearchSchema.Builder("Schema1")
7768                 .addProperty(new StringPropertyConfig.Builder("prop1")
7769                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
7770                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
7771                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
7772                         .build()
7773                 ).build();
7774         AppSearchSchema schema2 = new AppSearchSchema.Builder("Schema2")
7775                 .addProperty(new StringPropertyConfig.Builder("prop2")
7776                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
7777                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
7778                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
7779                         .build()
7780                 ).build();
7781         mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
7782                 .setForceOverride(true).addSchemas(schema1, schema2).build()).get();
7783 
7784         GenericDocument doc1 = new GenericDocument.Builder<>(
7785                 "namespace1", "id1", "Schema1")
7786                 .setPropertyString("prop1", "Hello, world!")
7787                 .build();
7788         GenericDocument doc2 = new GenericDocument.Builder<>(
7789                 "namespace1", "id2", "Schema2")
7790                 .setPropertyString("prop2", "Hello, world!")
7791                 .build();
7792         mDb1.putAsync(
7793                 new PutDocumentsRequest.Builder().addGenericDocuments(doc1, doc2).build()).get();
7794 
7795         SearchSpec searchSpec = new SearchSpec.Builder()
7796                 .setListFilterQueryLanguageEnabled(true)
7797                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
7798                 .build();
7799         // Support for function calls `search`, `createList` was added in list filters
7800         SearchResults searchResults = mDb1.search("propertyDefined(\"prop1\")",
7801                 searchSpec);
7802         List<SearchResult> page = searchResults.getNextPageAsync().get();
7803         assertThat(page).hasSize(1);
7804         assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id1");
7805 
7806         // Support for prefix operator * was added in list filters.
7807         searchResults = mDb1.search("propertyDefined(\"prop2\")", searchSpec);
7808         page = searchResults.getNextPageAsync().get();
7809         assertThat(page).hasSize(1);
7810         assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id2");
7811 
7812         // Combining negations with compound statements and property restricts was added in list
7813         // filters.
7814         searchResults = mDb1.search("NOT propertyDefined(\"prop1\")", searchSpec);
7815         page = searchResults.getNextPageAsync().get();
7816         assertThat(page).hasSize(1);
7817         assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id2");
7818     }
7819 
7820     @Test
testQuery_listFilterQueryWithoutEnablingFeatureFails()7821     public void testQuery_listFilterQueryWithoutEnablingFeatureFails() throws Exception {
7822         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_QUERY_LANGUAGE));
7823         AppSearchSchema schema = new AppSearchSchema.Builder("Schema")
7824                 .addProperty(new StringPropertyConfig.Builder("prop")
7825                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
7826                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
7827                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
7828                         .build()
7829                 ).build();
7830         mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
7831                 .setForceOverride(true).addSchemas(schema).build()).get();
7832 
7833         GenericDocument email = new GenericDocument.Builder<>(
7834                 "namespace1", "id1", "Schema")
7835                 .setPropertyString("prop", "Hello, world!")
7836                 .build();
7837         mDb1.putAsync(new PutDocumentsRequest.Builder().addGenericDocuments(email).build()).get();
7838 
7839         // Disable LIST_FILTER_QUERY_LANGUAGE in the SearchSpec.
7840         SearchSpec searchSpec = new SearchSpec.Builder()
7841                 .setListFilterQueryLanguageEnabled(false)
7842                 .build();
7843         SearchResults searchResults = mDb1.search("search(\"hello\", createList(\"prop\"))",
7844                 searchSpec);
7845         ExecutionException executionException = assertThrows(ExecutionException.class,
7846                 () -> searchResults.getNextPageAsync().get());
7847         assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
7848         AppSearchException exception = (AppSearchException) executionException.getCause();
7849         assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT);
7850         assertThat(exception).hasMessageThat().contains("Attempted use of unenabled feature");
7851         assertThat(exception).hasMessageThat().contains(Features.LIST_FILTER_QUERY_LANGUAGE);
7852 
7853         SearchResults searchResults2 = mDb1.search("wor*", searchSpec);
7854         executionException = assertThrows(ExecutionException.class,
7855                 () -> searchResults2.getNextPageAsync().get());
7856         assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
7857         exception = (AppSearchException) executionException.getCause();
7858         assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT);
7859         assertThat(exception).hasMessageThat().contains("Attempted use of unenabled feature");
7860         assertThat(exception).hasMessageThat().contains(Features.LIST_FILTER_QUERY_LANGUAGE);
7861 
7862         SearchResults searchResults3 = mDb1.search("NOT (foo OR otherProp:hello)", searchSpec);
7863         executionException = assertThrows(ExecutionException.class,
7864                 () -> searchResults3.getNextPageAsync().get());
7865         assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
7866         exception = (AppSearchException) executionException.getCause();
7867         assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT);
7868         assertThat(exception).hasMessageThat().contains("Attempted use of unenabled feature");
7869         assertThat(exception).hasMessageThat().contains(Features.LIST_FILTER_QUERY_LANGUAGE);
7870 
7871         SearchResults searchResults4 = mDb1.search("propertyDefined(\"prop\")", searchSpec);
7872         executionException = assertThrows(ExecutionException.class,
7873                 () -> searchResults4.getNextPageAsync().get());
7874         assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
7875         exception = (AppSearchException) executionException.getCause();
7876         assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT);
7877         assertThat(exception).hasMessageThat().contains("Attempted use of unenabled feature");
7878         assertThat(exception).hasMessageThat().contains(Features.LIST_FILTER_QUERY_LANGUAGE);
7879     }
7880 
7881     @Test
testQuery_listFilterQueryFeatures_notSupported()7882     public void testQuery_listFilterQueryFeatures_notSupported() throws Exception {
7883         assumeFalse(mDb1.getFeatures().isFeatureSupported(Features.NUMERIC_SEARCH));
7884         assumeFalse(mDb1.getFeatures().isFeatureSupported(Features.VERBATIM_SEARCH));
7885         assumeFalse(mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_QUERY_LANGUAGE));
7886 
7887         // UnsupportedOperationException will be thrown with these queries so no need to
7888         // define a schema and index document.
7889         SearchSpec.Builder builder = new SearchSpec.Builder();
7890         SearchSpec searchSpec1 = builder.setNumericSearchEnabled(true).build();
7891         SearchSpec searchSpec2 = builder.setVerbatimSearchEnabled(true).build();
7892         SearchSpec searchSpec3 = builder.setListFilterQueryLanguageEnabled(true).build();
7893 
7894         assertThrows(UnsupportedOperationException.class, () ->
7895                 mDb1.search("\"Hello, world!\"", searchSpec1));
7896         assertThrows(UnsupportedOperationException.class, () ->
7897                 mDb1.search("\"Hello, world!\"", searchSpec2));
7898         assertThrows(UnsupportedOperationException.class, () ->
7899                 mDb1.search("\"Hello, world!\"", searchSpec3));
7900     }
7901 
7902     @Test
testQuery_listFilterQueryHasPropertyFunction_notSupported()7903     public void testQuery_listFilterQueryHasPropertyFunction_notSupported() throws Exception {
7904         assumeFalse(
7905                 mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_HAS_PROPERTY_FUNCTION));
7906 
7907         // UnsupportedOperationException will be thrown with these queries so no need to
7908         // define a schema and index document.
7909         SearchSpec.Builder builder = new SearchSpec.Builder();
7910         SearchSpec searchSpec = builder.setListFilterHasPropertyFunctionEnabled(true).build();
7911 
7912         UnsupportedOperationException exception = assertThrows(
7913                 UnsupportedOperationException.class,
7914                 () -> mDb1.search("\"Hello, world!\"", searchSpec));
7915         assertThat(exception).hasMessageThat().contains(Features.LIST_FILTER_HAS_PROPERTY_FUNCTION
7916                 + " is not available on this AppSearch implementation.");
7917     }
7918 
7919     @Test
testQuery_hasPropertyFunction()7920     public void testQuery_hasPropertyFunction() throws Exception {
7921         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_QUERY_LANGUAGE));
7922         assumeTrue(
7923                 mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_HAS_PROPERTY_FUNCTION));
7924         AppSearchSchema schema = new AppSearchSchema.Builder("Schema")
7925                 .addProperty(new StringPropertyConfig.Builder("prop1")
7926                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
7927                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
7928                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
7929                         .build()
7930                 ).addProperty(new StringPropertyConfig.Builder("prop2")
7931                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
7932                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
7933                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
7934                         .build()
7935                 ).build();
7936         mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
7937                 .setForceOverride(true).addSchemas(schema).build()).get();
7938 
7939         GenericDocument doc1 = new GenericDocument.Builder<>(
7940                 "namespace", "id1", "Schema")
7941                 .setPropertyString("prop1", "Hello, world!")
7942                 .build();
7943         GenericDocument doc2 = new GenericDocument.Builder<>(
7944                 "namespace", "id2", "Schema")
7945                 .setPropertyString("prop2", "Hello, world!")
7946                 .build();
7947         GenericDocument doc3 = new GenericDocument.Builder<>(
7948                 "namespace", "id3", "Schema")
7949                 .setPropertyString("prop1", "Hello, world!")
7950                 .setPropertyString("prop2", "Hello, world!")
7951                 .build();
7952         mDb1.putAsync(new PutDocumentsRequest.Builder()
7953                 .addGenericDocuments(doc1, doc2, doc3).build()).get();
7954 
7955         SearchSpec searchSpec = new SearchSpec.Builder()
7956                 .setListFilterQueryLanguageEnabled(true)
7957                 .setListFilterHasPropertyFunctionEnabled(true)
7958                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
7959                 .build();
7960         SearchResults searchResults = mDb1.search("hasProperty(\"prop1\")",
7961                 searchSpec);
7962         List<SearchResult> page = searchResults.getNextPageAsync().get();
7963         assertThat(page).hasSize(2);
7964         assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id3");
7965         assertThat(page.get(1).getGenericDocument().getId()).isEqualTo("id1");
7966 
7967         searchResults = mDb1.search("hasProperty(\"prop2\")", searchSpec);
7968         page = searchResults.getNextPageAsync().get();
7969         assertThat(page).hasSize(2);
7970         assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id3");
7971         assertThat(page.get(1).getGenericDocument().getId()).isEqualTo("id2");
7972 
7973         searchResults = mDb1.search(
7974                 "hasProperty(\"prop1\") AND hasProperty(\"prop2\")",
7975                 searchSpec);
7976         page = searchResults.getNextPageAsync().get();
7977         assertThat(page).hasSize(1);
7978         assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id3");
7979     }
7980 
7981     @Test
testQuery_hasPropertyFunctionWithoutEnablingFeatureFails()7982     public void testQuery_hasPropertyFunctionWithoutEnablingFeatureFails() throws Exception {
7983         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_QUERY_LANGUAGE));
7984         assumeTrue(
7985                 mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_HAS_PROPERTY_FUNCTION));
7986         AppSearchSchema schema = new AppSearchSchema.Builder("Schema")
7987                 .addProperty(new StringPropertyConfig.Builder("prop")
7988                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
7989                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
7990                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
7991                         .build()
7992                 ).build();
7993         mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
7994                 .setForceOverride(true).addSchemas(schema).build()).get();
7995 
7996         GenericDocument doc = new GenericDocument.Builder<>(
7997                 "namespace1", "id1", "Schema")
7998                 .setPropertyString("prop", "Hello, world!")
7999                 .build();
8000         mDb1.putAsync(new PutDocumentsRequest.Builder().addGenericDocuments(doc).build()).get();
8001 
8002         // Enable LIST_FILTER_HAS_PROPERTY_FUNCTION but disable LIST_FILTER_QUERY_LANGUAGE in the
8003         // SearchSpec.
8004         SearchSpec searchSpec = new SearchSpec.Builder()
8005                 .setListFilterQueryLanguageEnabled(false)
8006                 .setListFilterHasPropertyFunctionEnabled(true)
8007                 .build();
8008         SearchResults searchResults = mDb1.search("hasProperty(\"prop\")",
8009                 searchSpec);
8010         ExecutionException executionException = assertThrows(ExecutionException.class,
8011                 () -> searchResults.getNextPageAsync().get());
8012         assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
8013         AppSearchException exception = (AppSearchException) executionException.getCause();
8014         assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT);
8015         assertThat(exception).hasMessageThat().contains("Attempted use of unenabled feature");
8016         assertThat(exception).hasMessageThat().contains(Features.LIST_FILTER_QUERY_LANGUAGE);
8017 
8018         // Disable LIST_FILTER_HAS_PROPERTY_FUNCTION in the SearchSpec.
8019         searchSpec = new SearchSpec.Builder()
8020                 .setListFilterQueryLanguageEnabled(true)
8021                 .setListFilterHasPropertyFunctionEnabled(false)
8022                 .build();
8023         SearchResults searchResults2 = mDb1.search("hasProperty(\"prop\")",
8024                 searchSpec);
8025         executionException = assertThrows(ExecutionException.class,
8026                 () -> searchResults2.getNextPageAsync().get());
8027         assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
8028         exception = (AppSearchException) executionException.getCause();
8029         assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT);
8030         assertThat(exception).hasMessageThat().contains("Attempted use of unenabled feature");
8031         assertThat(exception).hasMessageThat().contains("HAS_PROPERTY_FUNCTION");
8032     }
8033 
8034     @Test
8035     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_LIST_FILTER_MATCH_SCORE_EXPRESSION_FUNCTION)
testQuery_matchScoreExpression()8036     public void testQuery_matchScoreExpression() throws Exception {
8037         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_QUERY_LANGUAGE));
8038         assumeTrue(
8039                 mDb1.getFeatures().isFeatureSupported(
8040                         Features.LIST_FILTER_MATCH_SCORE_EXPRESSION_FUNCTION));
8041         AppSearchSchema schema = new AppSearchSchema.Builder("Schema")
8042                 .addProperty(new StringPropertyConfig.Builder("prop")
8043                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
8044                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
8045                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
8046                         .build()
8047                 ).build();
8048         mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
8049                 .setForceOverride(true).addSchemas(schema).build()).get();
8050 
8051         // Put documents with document scores 3, 4, and 5.
8052         GenericDocument doc1 = new GenericDocument.Builder<>(
8053                 "namespace", "id1", "Schema")
8054                 .setPropertyString("prop", "Hello, world!")
8055                 .setScore(3)
8056                 .build();
8057         GenericDocument doc2 = new GenericDocument.Builder<>(
8058                 "namespace", "id2", "Schema")
8059                 .setPropertyString("prop", "Hello, world!")
8060                 .setScore(4)
8061                 .build();
8062         GenericDocument doc3 = new GenericDocument.Builder<>(
8063                 "namespace", "id3", "Schema")
8064                 .setPropertyString("prop", "Hello, world!")
8065                 .setScore(5)
8066                 .build();
8067         mDb1.putAsync(new PutDocumentsRequest.Builder()
8068                 .addGenericDocuments(doc1, doc2, doc3).build()).get();
8069 
8070         // Get documents of scores in [3, 4], which should return doc1 and doc2.
8071         SearchSpec searchSpec = new SearchSpec.Builder()
8072                 .setListFilterQueryLanguageEnabled(true)
8073                 .setListFilterMatchScoreExpressionFunctionEnabled(true)
8074                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
8075                 .build();
8076         SearchResults searchResults = mDb1.search(
8077                 "matchScoreExpression(\"this.documentScore()\", 3, 4)", searchSpec);
8078         List<SearchResult> page = searchResults.getNextPageAsync().get();
8079         assertThat(page).hasSize(2);
8080         assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id2");
8081         assertThat(page.get(1).getGenericDocument().getId()).isEqualTo("id1");
8082 
8083         // Get documents of scores in [3, 5], which should return all documents.
8084         searchResults = mDb1.search(
8085                 "matchScoreExpression(\"this.documentScore()\", 3, 5)", searchSpec);
8086         page = searchResults.getNextPageAsync().get();
8087         assertThat(page).hasSize(3);
8088         assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id3");
8089         assertThat(page.get(1).getGenericDocument().getId()).isEqualTo("id2");
8090         assertThat(page.get(2).getGenericDocument().getId()).isEqualTo("id1");
8091     }
8092 
8093     @Test
8094     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_LIST_FILTER_MATCH_SCORE_EXPRESSION_FUNCTION)
testQuery_listFilterQueryMatchScoreExpressionFunction_notSupported()8095     public void testQuery_listFilterQueryMatchScoreExpressionFunction_notSupported()
8096             throws Exception {
8097         assumeFalse(
8098                 mDb1.getFeatures().isFeatureSupported(
8099                         Features.LIST_FILTER_MATCH_SCORE_EXPRESSION_FUNCTION));
8100 
8101         // UnsupportedOperationException will be thrown with these queries so no need to
8102         // define a schema and index document.
8103         SearchSpec.Builder builder = new SearchSpec.Builder();
8104         SearchSpec searchSpec = builder
8105                 .setListFilterMatchScoreExpressionFunctionEnabled(true)
8106                 .build();
8107 
8108         UnsupportedOperationException exception = assertThrows(
8109                 UnsupportedOperationException.class,
8110                 () -> mDb1.search("\"Hello, world!\"", searchSpec));
8111         assertThat(exception).hasMessageThat().contains(
8112                 Features.LIST_FILTER_MATCH_SCORE_EXPRESSION_FUNCTION
8113                         + " is not available on this AppSearch implementation.");
8114     }
8115 
8116     @Test
8117     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_LIST_FILTER_MATCH_SCORE_EXPRESSION_FUNCTION)
testQuery_matchScoreExpressionFunctionWithoutEnablingFeatureFails()8118     public void testQuery_matchScoreExpressionFunctionWithoutEnablingFeatureFails()
8119             throws Exception {
8120         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_QUERY_LANGUAGE));
8121         assumeTrue(mDb1.getFeatures().isFeatureSupported(
8122                 Features.LIST_FILTER_MATCH_SCORE_EXPRESSION_FUNCTION));
8123         AppSearchSchema schema = new AppSearchSchema.Builder("Schema")
8124                 .addProperty(new StringPropertyConfig.Builder("prop")
8125                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
8126                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
8127                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
8128                         .build()
8129                 ).build();
8130         mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
8131                 .setForceOverride(true).addSchemas(schema).build()).get();
8132 
8133         GenericDocument doc = new GenericDocument.Builder<>(
8134                 "namespace", "id", "Schema")
8135                 .setPropertyString("prop", "Hello, world!")
8136                 .build();
8137         mDb1.putAsync(new PutDocumentsRequest.Builder().addGenericDocuments(doc).build()).get();
8138 
8139         // Enable LIST_FILTER_MATCH_SCORE_EXPRESSION_FUNCTION but disable
8140         // LIST_FILTER_QUERY_LANGUAGE in the SearchSpec.
8141         SearchSpec searchSpec = new SearchSpec.Builder()
8142                 .setListFilterQueryLanguageEnabled(false)
8143                 .setListFilterMatchScoreExpressionFunctionEnabled(true)
8144                 .build();
8145         SearchResults searchResults = mDb1.search(
8146                 "matchScoreExpression(\"this.documentScore()\", 3, 4)", searchSpec);
8147         ExecutionException executionException = assertThrows(ExecutionException.class,
8148                 () -> searchResults.getNextPageAsync().get());
8149         assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
8150         AppSearchException exception = (AppSearchException) executionException.getCause();
8151         assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT);
8152         assertThat(exception).hasMessageThat().contains("Attempted use of unenabled feature");
8153         assertThat(exception).hasMessageThat().contains(Features.LIST_FILTER_QUERY_LANGUAGE);
8154 
8155         // Disable LIST_FILTER_MATCH_SCORE_EXPRESSION_FUNCTION in the SearchSpec.
8156         searchSpec = new SearchSpec.Builder()
8157                 .setListFilterQueryLanguageEnabled(true)
8158                 .setListFilterMatchScoreExpressionFunctionEnabled(false)
8159                 .build();
8160         SearchResults searchResults2 = mDb1.search(
8161                 "matchScoreExpression(\"this.documentScore()\", 3, 4)", searchSpec);
8162         executionException = assertThrows(ExecutionException.class,
8163                 () -> searchResults2.getNextPageAsync().get());
8164         assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
8165         exception = (AppSearchException) executionException.getCause();
8166         assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT);
8167         assertThat(exception).hasMessageThat().contains("Attempted use of unenabled feature");
8168         assertThat(exception).hasMessageThat().contains("MATCH_SCORE_EXPRESSION");
8169     }
8170 
8171     @Test
testQuery_propertyWeightsNotSupported()8172     public void testQuery_propertyWeightsNotSupported() throws Exception {
8173         assumeFalse(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SPEC_PROPERTY_WEIGHTS));
8174 
8175         // Schema registration
8176         mDb1.setSchemaAsync(
8177                 new SetSchemaRequest.Builder()
8178                         .addSchemas(AppSearchEmail.SCHEMA)
8179                         .build()).get();
8180 
8181         // Index two documents
8182         AppSearchEmail email1 =
8183                 new AppSearchEmail.Builder("namespace", "id1")
8184                         .setCreationTimestampMillis(1000)
8185                         .setSubject("foo")
8186                         .build();
8187         AppSearchEmail email2 =
8188                 new AppSearchEmail.Builder("namespace", "id2")
8189                         .setCreationTimestampMillis(1000)
8190                         .setBody("foo")
8191                         .build();
8192         checkIsBatchResultSuccess(mDb1.putAsync(
8193                 new PutDocumentsRequest.Builder()
8194                         .addGenericDocuments(email1, email2).build()));
8195 
8196         SearchSpec searchSpec = new SearchSpec.Builder()
8197                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
8198                 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE)
8199                 .setOrder(SearchSpec.ORDER_DESCENDING)
8200                 .setPropertyWeights(AppSearchEmail.SCHEMA_TYPE, ImmutableMap.of("subject",
8201                         2.0, "body", 0.5))
8202                 .build();
8203         UnsupportedOperationException exception =
8204                 assertThrows(UnsupportedOperationException.class,
8205                         () -> mDb1.search("Hello", searchSpec));
8206         assertThat(exception).hasMessageThat().contains("Property weights are not supported");
8207     }
8208 
8209     @Test
testQuery_propertyWeights()8210     public void testQuery_propertyWeights() throws Exception {
8211         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SPEC_PROPERTY_WEIGHTS));
8212 
8213         // Schema registration
8214         mDb1.setSchemaAsync(
8215                 new SetSchemaRequest.Builder()
8216                         .addSchemas(AppSearchEmail.SCHEMA)
8217                         .build()).get();
8218 
8219         // Index two documents
8220         AppSearchEmail email1 =
8221                 new AppSearchEmail.Builder("namespace", "id1")
8222                         .setCreationTimestampMillis(1000)
8223                         .setSubject("foo")
8224                         .build();
8225         AppSearchEmail email2 =
8226                 new AppSearchEmail.Builder("namespace", "id2")
8227                         .setCreationTimestampMillis(1000)
8228                         .setBody("foo")
8229                         .build();
8230         checkIsBatchResultSuccess(mDb1.putAsync(
8231                 new PutDocumentsRequest.Builder()
8232                         .addGenericDocuments(email1, email2).build()));
8233 
8234         // Query for "foo". It should match both emails.
8235         SearchResults searchResults = mDb1.search("foo", new SearchSpec.Builder()
8236                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
8237                 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE)
8238                 .setOrder(SearchSpec.ORDER_DESCENDING)
8239                 .setPropertyWeights(AppSearchEmail.SCHEMA_TYPE, ImmutableMap.of("subject",
8240                         2.0, "body", 0.5))
8241                 .build());
8242         List<SearchResult> results = retrieveAllSearchResults(searchResults);
8243 
8244         // email1 should be ranked higher because "foo" appears in the "subject" property which
8245         // has higher weight than the "body" property.
8246         assertThat(results).hasSize(2);
8247         assertThat(results.get(0).getRankingSignal()).isGreaterThan(0);
8248         assertThat(results.get(0).getRankingSignal()).isGreaterThan(
8249                 results.get(1).getRankingSignal());
8250         assertThat(results.get(0).getGenericDocument()).isEqualTo(email1);
8251         assertThat(results.get(1).getGenericDocument()).isEqualTo(email2);
8252 
8253         // Query for "foo" without property weights.
8254         SearchSpec searchSpecWithoutWeights = new SearchSpec.Builder()
8255                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
8256                 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE)
8257                 .setOrder(SearchSpec.ORDER_DESCENDING)
8258                 .build();
8259         SearchResults searchResultsWithoutWeights = mDb1.search("foo", searchSpecWithoutWeights);
8260         List<SearchResult> resultsWithoutWeights =
8261                 retrieveAllSearchResults(searchResultsWithoutWeights);
8262 
8263         // email1 should have the same ranking signal as email2 as each contains the term "foo"
8264         // once.
8265         assertThat(resultsWithoutWeights).hasSize(2);
8266         assertThat(resultsWithoutWeights.get(0).getRankingSignal()).isGreaterThan(0);
8267         assertThat(resultsWithoutWeights.get(0).getRankingSignal()).isEqualTo(
8268                 resultsWithoutWeights.get(1).getRankingSignal());
8269     }
8270 
8271     @Test
testQuery_propertyWeightsNestedProperties()8272     public void testQuery_propertyWeightsNestedProperties() throws Exception {
8273         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SPEC_PROPERTY_WEIGHTS));
8274 
8275         // Register a schema with a nested type
8276         AppSearchSchema schema =
8277                 new AppSearchSchema.Builder("TypeA").addProperty(
8278                         new AppSearchSchema.DocumentPropertyConfig.Builder("nestedEmail",
8279                                 AppSearchEmail.SCHEMA_TYPE).setShouldIndexNestedProperties(
8280                                 true).build()).build();
8281         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA,
8282                 schema).build()).get();
8283 
8284         // Index two documents
8285         AppSearchEmail nestedEmail1 =
8286                 new AppSearchEmail.Builder("namespace", "id1")
8287                         .setCreationTimestampMillis(1000)
8288                         .setSubject("foo")
8289                         .build();
8290         GenericDocument doc1 =
8291                 new GenericDocument.Builder<>("namespace", "id1", "TypeA").setPropertyDocument(
8292                         "nestedEmail", nestedEmail1).build();
8293         AppSearchEmail nestedEmail2 =
8294                 new AppSearchEmail.Builder("namespace", "id2")
8295                         .setCreationTimestampMillis(1000)
8296                         .setBody("foo")
8297                         .build();
8298         GenericDocument doc2 =
8299                 new GenericDocument.Builder<>("namespace", "id2", "TypeA").setPropertyDocument(
8300                         "nestedEmail", nestedEmail2).build();
8301         checkIsBatchResultSuccess(mDb1.putAsync(
8302                 new PutDocumentsRequest.Builder()
8303                         .addGenericDocuments(doc1, doc2).build()));
8304 
8305         // Query for "foo". It should match both emails.
8306         SearchResults searchResults = mDb1.search("foo", new SearchSpec.Builder()
8307                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
8308                 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE)
8309                 .setOrder(SearchSpec.ORDER_DESCENDING)
8310                 .setPropertyWeights("TypeA", ImmutableMap.of(
8311                         "nestedEmail.subject",
8312                         2.0, "nestedEmail.body", 0.5))
8313                 .build());
8314         List<SearchResult> results = retrieveAllSearchResults(searchResults);
8315 
8316         // email1 should be ranked higher because "foo" appears in the "nestedEmail.subject"
8317         // property which has higher weight than the "nestedEmail.body" property.
8318         assertThat(results).hasSize(2);
8319         assertThat(results.get(0).getRankingSignal()).isGreaterThan(0);
8320         assertThat(results.get(0).getRankingSignal()).isGreaterThan(
8321                 results.get(1).getRankingSignal());
8322         assertThat(results.get(0).getGenericDocument()).isEqualTo(doc1);
8323         assertThat(results.get(1).getGenericDocument()).isEqualTo(doc2);
8324 
8325         // Query for "foo" without property weights.
8326         SearchSpec searchSpecWithoutWeights = new SearchSpec.Builder()
8327                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
8328                 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE)
8329                 .setOrder(SearchSpec.ORDER_DESCENDING)
8330                 .build();
8331         SearchResults searchResultsWithoutWeights = mDb1.search("foo", searchSpecWithoutWeights);
8332         List<SearchResult> resultsWithoutWeights =
8333                 retrieveAllSearchResults(searchResultsWithoutWeights);
8334 
8335         // email1 should have the same ranking signal as email2 as each contains the term "foo"
8336         // once.
8337         assertThat(resultsWithoutWeights).hasSize(2);
8338         assertThat(resultsWithoutWeights.get(0).getRankingSignal()).isGreaterThan(0);
8339         assertThat(resultsWithoutWeights.get(0).getRankingSignal()).isEqualTo(
8340                 resultsWithoutWeights.get(1).getRankingSignal());
8341     }
8342 
8343     @Test
testQuery_propertyWeightsDefaults()8344     public void testQuery_propertyWeightsDefaults() throws Exception {
8345         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SPEC_PROPERTY_WEIGHTS));
8346 
8347         // Schema registration
8348         mDb1.setSchemaAsync(
8349                 new SetSchemaRequest.Builder()
8350                         .addSchemas(AppSearchEmail.SCHEMA)
8351                         .build()).get();
8352 
8353         // Index two documents
8354         AppSearchEmail email1 =
8355                 new AppSearchEmail.Builder("namespace", "id1")
8356                         .setCreationTimestampMillis(1000)
8357                         .setSubject("foo")
8358                         .build();
8359         AppSearchEmail email2 =
8360                 new AppSearchEmail.Builder("namespace", "id2")
8361                         .setCreationTimestampMillis(1000)
8362                         .setBody("foo bar")
8363                         .build();
8364         checkIsBatchResultSuccess(mDb1.putAsync(
8365                 new PutDocumentsRequest.Builder()
8366                         .addGenericDocuments(email1, email2).build()));
8367 
8368         // Query for "foo" without assigning property weights for any path.
8369         SearchResults searchResults = mDb1.search("foo", new SearchSpec.Builder()
8370                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
8371                 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE)
8372                 .setOrder(SearchSpec.ORDER_DESCENDING)
8373                 .setPropertyWeights(AppSearchEmail.SCHEMA_TYPE, ImmutableMap.of())
8374                 .build());
8375         List<SearchResult> resultsWithoutPropertyWeights = retrieveAllSearchResults(
8376                 searchResults);
8377 
8378         // Query for "foo" with assigning default property weights.
8379         searchResults = mDb1.search("foo", new SearchSpec.Builder()
8380                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
8381                 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE)
8382                 .setOrder(SearchSpec.ORDER_DESCENDING)
8383                 .setPropertyWeights(AppSearchEmail.SCHEMA_TYPE, ImmutableMap.of("subject", 1.0,
8384                         "body", 1.0))
8385                 .build());
8386         List<SearchResult> expectedResults = retrieveAllSearchResults(searchResults);
8387 
8388         assertThat(resultsWithoutPropertyWeights).hasSize(2);
8389         assertThat(expectedResults).hasSize(2);
8390 
8391         assertThat(resultsWithoutPropertyWeights.get(0).getGenericDocument()).isEqualTo(email1);
8392         assertThat(resultsWithoutPropertyWeights.get(1).getGenericDocument()).isEqualTo(email2);
8393         assertThat(expectedResults.get(0).getGenericDocument()).isEqualTo(email1);
8394         assertThat(expectedResults.get(1).getGenericDocument()).isEqualTo(email2);
8395 
8396         // The ranking signal for results with no property path and weights set should be equal
8397         // to the ranking signal for results with explicitly set default weights.
8398         assertThat(resultsWithoutPropertyWeights.get(0).getRankingSignal()).isEqualTo(
8399                 expectedResults.get(0).getRankingSignal());
8400         assertThat(resultsWithoutPropertyWeights.get(1).getRankingSignal()).isEqualTo(
8401                 expectedResults.get(1).getRankingSignal());
8402     }
8403 
8404     @Test
testQuery_propertyWeightsIgnoresInvalidPropertyPaths()8405     public void testQuery_propertyWeightsIgnoresInvalidPropertyPaths() throws Exception {
8406         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SPEC_PROPERTY_WEIGHTS));
8407 
8408         // Schema registration
8409         mDb1.setSchemaAsync(
8410                 new SetSchemaRequest.Builder()
8411                         .addSchemas(AppSearchEmail.SCHEMA)
8412                         .build()).get();
8413 
8414         // Index an email
8415         AppSearchEmail email1 =
8416                 new AppSearchEmail.Builder("namespace", "id1")
8417                         .setCreationTimestampMillis(1000)
8418                         .setSubject("baz")
8419                         .build();
8420         checkIsBatchResultSuccess(mDb1.putAsync(
8421                 new PutDocumentsRequest.Builder()
8422                         .addGenericDocuments(email1).build()));
8423 
8424         // Query for "baz" with property weight for "subject", a valid property in the schema type.
8425         SearchResults searchResults = mDb1.search("baz", new SearchSpec.Builder()
8426                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
8427                 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE)
8428                 .setOrder(SearchSpec.ORDER_DESCENDING)
8429                 .setPropertyWeights(AppSearchEmail.SCHEMA_TYPE, ImmutableMap.of("subject", 2.0))
8430                 .build());
8431         List<SearchResult> results = retrieveAllSearchResults(searchResults);
8432 
8433         // Query for "baz" with property weights, one for valid property "subject" and one for a
8434         // non-existing property "invalid".
8435         searchResults = mDb1.search("baz", new SearchSpec.Builder()
8436                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
8437                 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE)
8438                 .setOrder(SearchSpec.ORDER_DESCENDING)
8439                 .setPropertyWeights(AppSearchEmail.SCHEMA_TYPE, ImmutableMap.of("subject", 2.0,
8440                         "invalid", 3.0))
8441                 .build());
8442         List<SearchResult> resultsWithInvalidPath = retrieveAllSearchResults(searchResults);
8443 
8444         assertThat(results).hasSize(1);
8445         assertThat(resultsWithInvalidPath).hasSize(1);
8446 
8447         // We expect the ranking signal to be unchanged in the presence of an invalid property
8448         // weight.
8449         assertThat(results.get(0).getRankingSignal()).isGreaterThan(0);
8450         assertThat(resultsWithInvalidPath.get(0).getRankingSignal()).isEqualTo(
8451                 results.get(0).getRankingSignal());
8452 
8453         assertThat(results.get(0).getGenericDocument()).isEqualTo(email1);
8454         assertThat(resultsWithInvalidPath.get(0).getGenericDocument()).isEqualTo(email1);
8455     }
8456 
8457     @Test
testQueryWithJoin_typePropertyFiltersOnNestedSpec()8458     public void testQueryWithJoin_typePropertyFiltersOnNestedSpec() throws Exception {
8459         assumeTrue(mDb1.getFeatures().isFeatureSupported(
8460                 Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
8461         assumeTrue(mDb1.getFeatures()
8462                 .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
8463 
8464         // A full example of how join might be used with property filters in join spec
8465         AppSearchSchema actionSchema = new AppSearchSchema.Builder("ViewAction")
8466                 .addProperty(new StringPropertyConfig.Builder("entityId")
8467                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
8468                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
8469                         .setJoinableValueType(StringPropertyConfig
8470                                 .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
8471                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
8472                         .build()
8473                 ).addProperty(new StringPropertyConfig.Builder("note")
8474                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
8475                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
8476                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
8477                         .build()
8478                 ).addProperty(new StringPropertyConfig.Builder("viewType")
8479                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
8480                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
8481                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
8482                         .build()
8483                 ).build();
8484 
8485         // Schema registration
8486         mDb1.setSchemaAsync(
8487                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA, actionSchema)
8488                         .build()).get();
8489 
8490         // Index 2 email documents
8491         AppSearchEmail inEmail =
8492                 new AppSearchEmail.Builder("namespace", "id1")
8493                         .setFrom("from@example.com")
8494                         .setTo("to1@example.com", "to2@example.com")
8495                         .setSubject("testPut example")
8496                         .setBody("This is the body of the testPut email")
8497                         .build();
8498 
8499         AppSearchEmail inEmail2 =
8500                 new AppSearchEmail.Builder("namespace", "id2")
8501                         .setFrom("from@example.com")
8502                         .setTo("to1@example.com", "to2@example.com")
8503                         .setSubject("testPut example")
8504                         .setBody("This is the body of the testPut email")
8505                         .build();
8506 
8507         // Index 2 viewAction documents, one for email1 and the other for email2
8508         String qualifiedId1 =
8509                 DocumentIdUtil.createQualifiedId(
8510                         ApplicationProvider.getApplicationContext().getPackageName(), DB_NAME_1,
8511                         "namespace", "id1");
8512         String qualifiedId2 =
8513                 DocumentIdUtil.createQualifiedId(
8514                         ApplicationProvider.getApplicationContext().getPackageName(), DB_NAME_1,
8515                         "namespace", "id2");
8516         GenericDocument viewAction1 = new GenericDocument.Builder<>("NS", "id3", "ViewAction")
8517                 .setPropertyString("entityId", qualifiedId1)
8518                 .setPropertyString("note", "Viewed email on Monday")
8519                 .setPropertyString("viewType", "Stared").build();
8520         GenericDocument viewAction2 = new GenericDocument.Builder<>("NS", "id4", "ViewAction")
8521                 .setPropertyString("entityId", qualifiedId2)
8522                 .setPropertyString("note", "Viewed email on Tuesday")
8523                 .setPropertyString("viewType", "Viewed").build();
8524         checkIsBatchResultSuccess(mDb1.putAsync(
8525                 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail, inEmail2,
8526                                 viewAction1, viewAction2)
8527                         .build()));
8528 
8529         // The nested search spec only allows searching the viewType property for viewAction
8530         // schema type. It also specifies a property filter for Email schema.
8531         SearchSpec nestedSearchSpec =
8532                 new SearchSpec.Builder()
8533                         .addFilterProperties("ViewAction", ImmutableList.of("viewType"))
8534                         .addFilterProperties(AppSearchEmail.SCHEMA_TYPE,
8535                                 ImmutableList.of("subject"))
8536                         .build();
8537 
8538         // Search for the term "Viewed" in join spec
8539         JoinSpec js = new JoinSpec.Builder("entityId")
8540                 .setNestedSearch("Viewed", nestedSearchSpec)
8541                 .setAggregationScoringStrategy(JoinSpec.AGGREGATION_SCORING_RESULT_COUNT)
8542                 .build();
8543 
8544         SearchResults searchResults = mDb1.search("body email", new SearchSpec.Builder()
8545                 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_JOIN_AGGREGATE_SCORE)
8546                 .setJoinSpec(js)
8547                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
8548                 .build());
8549 
8550         List<SearchResult> sr = searchResults.getNextPageAsync().get();
8551 
8552         // Both email docs are returned, email2 comes first because it has higher number of
8553         // joined documents. The property filters for Email schema specified in the nested search
8554         // specs don't apply to the outer query (otherwise none of the email documents would have
8555         // been returned).
8556         assertThat(sr).hasSize(2);
8557 
8558         // Email2 has a viewAction document viewAction2 that satisfies the property filters in
8559         // the join spec, so it should be present in the joined results.
8560         assertThat(sr.get(0).getGenericDocument().getId()).isEqualTo("id2");
8561         assertThat(sr.get(0).getRankingSignal()).isEqualTo(1.0);
8562         assertThat(sr.get(0).getJoinedResults()).hasSize(1);
8563         assertThat(sr.get(0).getJoinedResults().get(0).getGenericDocument()).isEqualTo(viewAction2);
8564 
8565         // Email1 has a viewAction document viewAction1 but it doesn't satisfy the property filters
8566         // in the join spec, so it should not be present in the joined results.
8567         assertThat(sr.get(1).getGenericDocument().getId()).isEqualTo("id1");
8568         assertThat(sr.get(1).getRankingSignal()).isEqualTo(0.0);
8569         assertThat(sr.get(1).getJoinedResults()).isEmpty();
8570     }
8571 
8572     @Test
testQueryWithJoin_typePropertyFiltersOnOuterSpec()8573     public void testQueryWithJoin_typePropertyFiltersOnOuterSpec() throws Exception {
8574         assumeTrue(mDb1.getFeatures().isFeatureSupported(
8575                 Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
8576         assumeTrue(mDb1.getFeatures()
8577                 .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
8578 
8579         // A full example of how join might be used with property filters in join spec
8580         AppSearchSchema actionSchema = new AppSearchSchema.Builder("ViewAction")
8581                 .addProperty(new StringPropertyConfig.Builder("entityId")
8582                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
8583                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
8584                         .setJoinableValueType(StringPropertyConfig
8585                                 .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
8586                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
8587                         .build()
8588                 ).addProperty(new StringPropertyConfig.Builder("note")
8589                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
8590                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
8591                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
8592                         .build()
8593                 ).addProperty(new StringPropertyConfig.Builder("viewType")
8594                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
8595                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
8596                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
8597                         .build()
8598                 ).build();
8599 
8600         // Schema registration
8601         mDb1.setSchemaAsync(
8602                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA, actionSchema)
8603                         .build()).get();
8604 
8605         // Index 2 email documents
8606         AppSearchEmail inEmail =
8607                 new AppSearchEmail.Builder("namespace", "id1")
8608                         .setFrom("from@example.com")
8609                         .setTo("to1@example.com", "to2@example.com")
8610                         .setSubject("testPut example")
8611                         .setBody("This is the body of the testPut email")
8612                         .build();
8613 
8614         AppSearchEmail inEmail2 =
8615                 new AppSearchEmail.Builder("namespace", "id2")
8616                         .setFrom("from@example.com")
8617                         .setTo("to1@example.com", "to2@example.com")
8618                         .setSubject("testPut example")
8619                         .setBody("This is the body of the testPut email")
8620                         .build();
8621 
8622         // Index 2 viewAction documents, one for email1 and the other for email2
8623         String qualifiedId1 =
8624                 DocumentIdUtil.createQualifiedId(
8625                         ApplicationProvider.getApplicationContext().getPackageName(), DB_NAME_1,
8626                         "namespace", "id1");
8627         String qualifiedId2 =
8628                 DocumentIdUtil.createQualifiedId(
8629                         ApplicationProvider.getApplicationContext().getPackageName(), DB_NAME_1,
8630                         "namespace", "id2");
8631         GenericDocument viewAction1 = new GenericDocument.Builder<>("NS", "id3", "ViewAction")
8632                 .setPropertyString("entityId", qualifiedId1)
8633                 .setPropertyString("note", "Viewed email on Monday")
8634                 .setPropertyString("viewType", "Stared").build();
8635         GenericDocument viewAction2 = new GenericDocument.Builder<>("NS", "id4", "ViewAction")
8636                 .setPropertyString("entityId", qualifiedId2)
8637                 .setPropertyString("note", "Viewed email on Tuesday")
8638                 .setPropertyString("viewType", "Viewed").build();
8639         checkIsBatchResultSuccess(mDb1.putAsync(
8640                 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail, inEmail2,
8641                                 viewAction1, viewAction2)
8642                         .build()));
8643 
8644         // The nested search spec doesn't specify any property filters.
8645         SearchSpec nestedSearchSpec = new SearchSpec.Builder().build();
8646 
8647         // Search for the term "Viewed" in join spec
8648         JoinSpec js = new JoinSpec.Builder("entityId")
8649                 .setNestedSearch("Viewed", nestedSearchSpec)
8650                 .setAggregationScoringStrategy(JoinSpec.AGGREGATION_SCORING_RESULT_COUNT)
8651                 .build();
8652 
8653         // Outer search spec adds property filters for both Email and ViewAction schema
8654         SearchResults searchResults = mDb1.search("body email", new SearchSpec.Builder()
8655                 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_JOIN_AGGREGATE_SCORE)
8656                 .setJoinSpec(js)
8657                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
8658                 .addFilterProperties(AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("body"))
8659                 .addFilterProperties("ViewAction", ImmutableList.of("viewType"))
8660                 .build());
8661 
8662         List<SearchResult> sr = searchResults.getNextPageAsync().get();
8663 
8664         // Both email docs are returned as they both satisfy the property filters for Email, email2
8665         // comes first because it has higher id lexicographically.
8666         assertThat(sr).hasSize(2);
8667 
8668         // Email2 has a viewAction document viewAction2 that satisfies the property filters in
8669         // the outer spec (although those property filters are irrelevant for joined documents),
8670         // it should be present in the joined results.
8671         assertThat(sr.get(0).getGenericDocument().getId()).isEqualTo("id2");
8672         assertThat(sr.get(0).getRankingSignal()).isEqualTo(1.0);
8673         assertThat(sr.get(0).getJoinedResults()).hasSize(1);
8674         assertThat(sr.get(0).getJoinedResults().get(0).getGenericDocument()).isEqualTo(viewAction2);
8675 
8676         // Email1 has a viewAction document viewAction1 that doesn't satisfy the property filters
8677         // in the outer spec, but property filters in the outer spec should not apply on joined
8678         // documents, so viewAction1 should be present in the joined results.
8679         assertThat(sr.get(1).getGenericDocument().getId()).isEqualTo("id1");
8680         assertThat(sr.get(1).getRankingSignal()).isEqualTo(1.0);
8681         assertThat(sr.get(0).getJoinedResults()).hasSize(1);
8682         assertThat(sr.get(1).getJoinedResults().get(0).getGenericDocument()).isEqualTo(viewAction1);
8683     }
8684 
8685     @Test
testQuery_typePropertyFiltersNotSupported()8686     public void testQuery_typePropertyFiltersNotSupported() throws Exception {
8687         assumeFalse(mDb1.getFeatures().isFeatureSupported(
8688                 Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
8689         // Schema registration
8690         mDb1.setSchemaAsync(
8691                 new SetSchemaRequest.Builder()
8692                         .addSchemas(AppSearchEmail.SCHEMA)
8693                         .build()).get();
8694 
8695         // Query with type property filters {"Email", ["subject", "to"]} and verify that unsupported
8696         // exception is thrown
8697         SearchSpec searchSpec = new SearchSpec.Builder()
8698                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
8699                 .addFilterProperties(AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("subject", "to"))
8700                 .build();
8701         UnsupportedOperationException exception =
8702                 assertThrows(UnsupportedOperationException.class,
8703                         () -> mDb1.search("body", searchSpec));
8704         assertThat(exception).hasMessageThat().contains(Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES
8705                 + " is not available on this AppSearch implementation.");
8706     }
8707 
8708     @Test
8709     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SEARCH_RESULT_PARENT_TYPES)
testQuery_searchResultWrapsParentTypeMapForPolymorphism()8710     public void testQuery_searchResultWrapsParentTypeMapForPolymorphism() throws Exception {
8711         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
8712         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_RESULT_PARENT_TYPES));
8713 
8714         // Schema registration
8715         AppSearchSchema personSchema =
8716                 new AppSearchSchema.Builder("Person")
8717                         .addProperty(
8718                                 new StringPropertyConfig.Builder("name")
8719                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
8720                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
8721                                         .setIndexingType(
8722                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
8723                                         .build())
8724                         .build();
8725         AppSearchSchema artistSchema =
8726                 new AppSearchSchema.Builder("Artist")
8727                         .addParentType("Person")
8728                         .addProperty(
8729                                 new StringPropertyConfig.Builder("name")
8730                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
8731                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
8732                                         .setIndexingType(
8733                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
8734                                         .build())
8735                         .addProperty(
8736                                 new StringPropertyConfig.Builder("company")
8737                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
8738                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
8739                                         .setIndexingType(
8740                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
8741                                         .build())
8742                         .build();
8743         AppSearchSchema musicianSchema =
8744                 new AppSearchSchema.Builder("Musician")
8745                         .addParentType("Artist")
8746                         .addProperty(
8747                                 new StringPropertyConfig.Builder("name")
8748                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
8749                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
8750                                         .setIndexingType(
8751                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
8752                                         .build())
8753                         .addProperty(
8754                                 new StringPropertyConfig.Builder("company")
8755                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
8756                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
8757                                         .setIndexingType(
8758                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
8759                                         .build())
8760                         .build();
8761         AppSearchSchema messageSchema =
8762                 new AppSearchSchema.Builder("Message")
8763                         .addProperty(
8764                                 new AppSearchSchema.DocumentPropertyConfig.Builder(
8765                                         "receivers", "Person")
8766                                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
8767                                         .setShouldIndexNestedProperties(true)
8768                                         .build())
8769                         .build();
8770         mDb1.setSchemaAsync(
8771                         new SetSchemaRequest.Builder()
8772                                 .addSchemas(personSchema)
8773                                 .addSchemas(artistSchema)
8774                                 .addSchemas(musicianSchema)
8775                                 .addSchemas(messageSchema)
8776                                 .build())
8777                 .get();
8778 
8779         // Index documents
8780         GenericDocument personDoc =
8781                 new GenericDocument.Builder<>("namespace", "id1", "Person")
8782                         .setPropertyString("name", "person")
8783                         .build();
8784         GenericDocument artistDoc =
8785                 new GenericDocument.Builder<>("namespace", "id2", "Artist")
8786                         .setPropertyString("name", "artist")
8787                         .setPropertyString("company", "foo")
8788                         .build();
8789         GenericDocument musicianDoc =
8790                 new GenericDocument.Builder<>("namespace", "id3", "Musician")
8791                         .setPropertyString("name", "musician")
8792                         .setPropertyString("company", "foo")
8793                         .build();
8794         GenericDocument messageDoc =
8795                 new GenericDocument.Builder<>("namespace", "id4", "Message")
8796                         .setPropertyDocument("receivers", artistDoc, musicianDoc)
8797                         .build();
8798 
8799         Map<String, List<String>> expectedPersonParentTypeMap = Collections.emptyMap();
8800         Map<String, List<String>> expectedArtistParentTypeMap =
8801                 ImmutableMap.of("Artist", ImmutableList.of("Person"));
8802         Map<String, List<String>> expectedMusicianParentTypeMap =
8803                 ImmutableMap.of("Musician", ImmutableList.of("Artist", "Person"));
8804         // artistDoc and musicianDoc are nested in messageDoc, so messageDoc's parent type map
8805         // should have the entries for both the Artist and Musician type.
8806         Map<String, List<String>> expectedMessageParentTypeMap = ImmutableMap.of(
8807                 "Artist", ImmutableList.of("Person"),
8808                 "Musician", ImmutableList.of("Artist", "Person"));
8809 
8810         checkIsBatchResultSuccess(
8811                 mDb1.putAsync(
8812                         new PutDocumentsRequest.Builder()
8813                                 .addGenericDocuments(personDoc, artistDoc, musicianDoc, messageDoc)
8814                                 .build()));
8815 
8816         // Query to get all the documents
8817         List<SearchResult> searchResults = retrieveAllSearchResults(
8818                 mDb1.search("", new SearchSpec.Builder()
8819                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
8820                         .build()));
8821         assertThat(searchResults).hasSize(4);
8822         assertThat(searchResults.get(0).getGenericDocument().getSchemaType())
8823                 .isEqualTo("Message");
8824         assertThat(searchResults.get(0).getParentTypeMap())
8825                 .isEqualTo(expectedMessageParentTypeMap);
8826 
8827         assertThat(searchResults.get(1).getGenericDocument().getSchemaType())
8828                 .isEqualTo("Musician");
8829         assertThat(searchResults.get(1).getParentTypeMap())
8830                 .isEqualTo(expectedMusicianParentTypeMap);
8831 
8832         assertThat(searchResults.get(2).getGenericDocument().getSchemaType())
8833                 .isEqualTo("Artist");
8834         assertThat(searchResults.get(2).getParentTypeMap())
8835                 .isEqualTo(expectedArtistParentTypeMap);
8836 
8837         assertThat(searchResults.get(3).getGenericDocument().getSchemaType())
8838                 .isEqualTo("Person");
8839         assertThat(searchResults.get(3).getParentTypeMap())
8840                 .isEqualTo(expectedPersonParentTypeMap);
8841     }
8842 
8843     @Test
testSimpleJoin()8844     public void testSimpleJoin() throws Exception {
8845         assumeTrue(mDb1.getFeatures()
8846                 .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
8847 
8848         // A full example of how join might be used
8849         AppSearchSchema actionSchema = new AppSearchSchema.Builder("ViewAction")
8850                 .addProperty(new StringPropertyConfig.Builder("entityId")
8851                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
8852                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
8853                         .setJoinableValueType(StringPropertyConfig
8854                                 .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
8855                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
8856                         .build()
8857                 ).addProperty(new StringPropertyConfig.Builder("note")
8858                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
8859                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
8860                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
8861                         .build()
8862                 ).build();
8863 
8864         // Schema registration
8865         mDb1.setSchemaAsync(
8866                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA, actionSchema)
8867                         .build()).get();
8868 
8869         // Index a document
8870         // While inEmail2 has a higher document score, we will rank based on the number of joined
8871         // documents. inEmail1 will have 1 joined document while inEmail2 will have 0 joined
8872         // documents.
8873         AppSearchEmail inEmail =
8874                 new AppSearchEmail.Builder("namespace", "id1")
8875                         .setFrom("from@example.com")
8876                         .setTo("to1@example.com", "to2@example.com")
8877                         .setSubject("testPut example")
8878                         .setBody("This is the body of the testPut email")
8879                         .setScore(1)
8880                         .build();
8881 
8882         AppSearchEmail inEmail2 =
8883                 new AppSearchEmail.Builder("namespace", "id2")
8884                         .setFrom("from@example.com")
8885                         .setTo("to1@example.com", "to2@example.com")
8886                         .setSubject("testPut example")
8887                         .setBody("This is the body of the testPut email")
8888                         .setScore(10)
8889                         .build();
8890 
8891         String qualifiedId = DocumentIdUtil.createQualifiedId(mContext.getPackageName(), DB_NAME_1,
8892                 "namespace", "id1");
8893         GenericDocument viewAction1 = new GenericDocument.Builder<>("NS", "id3", "ViewAction")
8894                 .setScore(1)
8895                 .setPropertyString("entityId", qualifiedId)
8896                 .setPropertyString("note", "Viewed email on Monday").build();
8897         GenericDocument viewAction2 = new GenericDocument.Builder<>("NS", "id4", "ViewAction")
8898                 .setScore(2)
8899                 .setPropertyString("entityId", qualifiedId)
8900                 .setPropertyString("note", "Viewed email on Tuesday").build();
8901         checkIsBatchResultSuccess(mDb1.putAsync(
8902                 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail, inEmail2,
8903                                 viewAction1, viewAction2)
8904                         .build()));
8905 
8906         SearchSpec nestedSearchSpec =
8907                 new SearchSpec.Builder()
8908                         .setRankingStrategy(SearchSpec.RANKING_STRATEGY_DOCUMENT_SCORE)
8909                         .setOrder(SearchSpec.ORDER_ASCENDING)
8910                         .build();
8911 
8912         JoinSpec js = new JoinSpec.Builder("entityId")
8913                 .setNestedSearch("", nestedSearchSpec)
8914                 .setAggregationScoringStrategy(JoinSpec.AGGREGATION_SCORING_RESULT_COUNT)
8915                 .setMaxJoinedResultCount(1)
8916                 .build();
8917 
8918         SearchResults searchResults = mDb1.search("body email", new SearchSpec.Builder()
8919                 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_JOIN_AGGREGATE_SCORE)
8920                 .setJoinSpec(js)
8921                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
8922                 .build());
8923 
8924         List<SearchResult> sr = searchResults.getNextPageAsync().get();
8925 
8926         // Both email docs are returned, but id1 comes first due to the join
8927         assertThat(sr).hasSize(2);
8928 
8929         assertThat(sr.get(0).getGenericDocument().getId()).isEqualTo("id1");
8930         assertThat(sr.get(0).getJoinedResults()).hasSize(1);
8931         assertThat(sr.get(0).getJoinedResults().get(0).getGenericDocument()).isEqualTo(viewAction1);
8932         // SearchSpec.Builder#setMaxJoinedResultCount only limits the number of child documents
8933         // returned. It does not affect the number of child documents that are scored. So the score
8934         // (the COUNT of the number of children) is 2, even though only one child is returned.
8935         assertThat(sr.get(0).getRankingSignal()).isEqualTo(2.0);
8936 
8937         assertThat(sr.get(1).getGenericDocument().getId()).isEqualTo("id2");
8938         assertThat(sr.get(1).getRankingSignal()).isEqualTo(0.0);
8939         assertThat(sr.get(1).getJoinedResults()).isEmpty();
8940     }
8941 
8942     @Test
testJoin_unsupportedFeature_throwsException()8943     public void testJoin_unsupportedFeature_throwsException() throws Exception {
8944         assumeFalse(mDb1.getFeatures().isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
8945 
8946         SearchSpec nestedSearchSpec = new SearchSpec.Builder().build();
8947         JoinSpec js = new JoinSpec.Builder("entityId").setNestedSearch("", nestedSearchSpec)
8948                 .build();
8949         Exception e = assertThrows(UnsupportedOperationException.class, () -> mDb1.search(
8950                 /*queryExpression */ "",
8951                 new SearchSpec.Builder()
8952                         .setJoinSpec(js)
8953                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
8954                         .build()));
8955         assertThat(e.getMessage()).isEqualTo("JoinSpec is not available on this AppSearch "
8956                 + "implementation.");
8957     }
8958 
8959     @Test
testSearchSuggestion_notSupported()8960     public void testSearchSuggestion_notSupported() throws Exception {
8961         assumeFalse(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION));
8962 
8963         assertThrows(UnsupportedOperationException.class, () ->
8964                 mDb1.searchSuggestionAsync(
8965                         /*suggestionQueryExpression=*/"t",
8966                         new SearchSuggestionSpec.Builder(/*maximumResultCount=*/2).build()).get());
8967     }
8968 
8969     @Test
testSearchSuggestion()8970     public void testSearchSuggestion() throws Exception {
8971         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION));
8972         // Schema registration
8973         AppSearchSchema schema = new AppSearchSchema.Builder("Type").addProperty(
8974                         new StringPropertyConfig.Builder("body")
8975                                 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
8976                                 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
8977                                 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
8978                                 .build())
8979                 .build();
8980         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
8981 
8982         // Index documents
8983         GenericDocument doc1 = new GenericDocument.Builder<>("namespace", "id1", "Type")
8984                 .setPropertyString("body", "termOne termTwo termThree termFour")
8985                 .build();
8986         GenericDocument doc2 = new GenericDocument.Builder<>("namespace", "id2", "Type")
8987                 .setPropertyString("body", "termOne termTwo termThree")
8988                 .build();
8989         GenericDocument doc3 = new GenericDocument.Builder<>("namespace", "id3", "Type")
8990                 .setPropertyString("body", "termOne termTwo")
8991                 .build();
8992         GenericDocument doc4 = new GenericDocument.Builder<>("namespace", "id4", "Type")
8993                 .setPropertyString("body", "termOne")
8994                 .build();
8995 
8996         checkIsBatchResultSuccess(mDb1.putAsync(
8997                 new PutDocumentsRequest.Builder().addGenericDocuments(doc1, doc2, doc3, doc4)
8998                         .build()));
8999 
9000         SearchSuggestionResult resultOne =
9001                 new SearchSuggestionResult.Builder().setSuggestedResult("termone").build();
9002         SearchSuggestionResult resultTwo =
9003                 new SearchSuggestionResult.Builder().setSuggestedResult("termtwo").build();
9004         SearchSuggestionResult resultThree =
9005                 new SearchSuggestionResult.Builder().setSuggestedResult("termthree").build();
9006         SearchSuggestionResult resultFour =
9007                 new SearchSuggestionResult.Builder().setSuggestedResult("termfour").build();
9008 
9009         List<SearchSuggestionResult> suggestions = mDb1.searchSuggestionAsync(
9010                 /*suggestionQueryExpression=*/"t",
9011                 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get();
9012         assertThat(suggestions).containsExactly(resultOne, resultTwo, resultThree, resultFour)
9013                 .inOrder();
9014 
9015         // Query first 2 suggestions, and they will be ranked.
9016         suggestions = mDb1.searchSuggestionAsync(
9017                 /*suggestionQueryExpression=*/"t",
9018                 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/2).build()).get();
9019         assertThat(suggestions).containsExactly(resultOne, resultTwo).inOrder();
9020     }
9021 
9022     @Test
testSearchSuggestion_namespaceFilter()9023     public void testSearchSuggestion_namespaceFilter() throws Exception {
9024         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION));
9025         // Schema registration
9026         AppSearchSchema schema = new AppSearchSchema.Builder("Type").addProperty(
9027                         new StringPropertyConfig.Builder("body")
9028                                 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
9029                                 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
9030                                 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
9031                                 .build())
9032                 .build();
9033         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
9034 
9035         // Index documents
9036         GenericDocument doc1 = new GenericDocument.Builder<>("namespace1", "id1", "Type")
9037                 .setPropertyString("body", "fo foo")
9038                 .build();
9039         GenericDocument doc2 = new GenericDocument.Builder<>("namespace2", "id2", "Type")
9040                 .setPropertyString("body", "foo")
9041                 .build();
9042         GenericDocument doc3 = new GenericDocument.Builder<>("namespace3", "id3", "Type")
9043                 .setPropertyString("body", "fool")
9044                 .build();
9045 
9046         checkIsBatchResultSuccess(mDb1.putAsync(
9047                 new PutDocumentsRequest.Builder().addGenericDocuments(doc1, doc2, doc3).build()));
9048 
9049         SearchSuggestionResult resultFo =
9050                 new SearchSuggestionResult.Builder().setSuggestedResult("fo").build();
9051         SearchSuggestionResult resultFoo =
9052                 new SearchSuggestionResult.Builder().setSuggestedResult("foo").build();
9053         SearchSuggestionResult resultFool =
9054                 new SearchSuggestionResult.Builder().setSuggestedResult("fool").build();
9055 
9056         // namespace1 has 2 results.
9057         List<SearchSuggestionResult> suggestions = mDb1.searchSuggestionAsync(
9058                 /*suggestionQueryExpression=*/"f",
9059                 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10)
9060                         .addFilterNamespaces("namespace1").build()).get();
9061         assertThat(suggestions).containsExactly(resultFoo, resultFo).inOrder();
9062 
9063         // namespace2 has 1 result.
9064         suggestions = mDb1.searchSuggestionAsync(
9065                 /*suggestionQueryExpression=*/"f",
9066                 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10)
9067                         .addFilterNamespaces("namespace2").build()).get();
9068         assertThat(suggestions).containsExactly(resultFoo).inOrder();
9069 
9070         // namespace2 and 3 has 2 results.
9071         suggestions = mDb1.searchSuggestionAsync(
9072                 /*suggestionQueryExpression=*/"f",
9073                 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10)
9074                         .addFilterNamespaces("namespace2", "namespace3")
9075                         .build()).get();
9076         assertThat(suggestions).containsExactly(resultFoo, resultFool);
9077 
9078         // non exist namespace has empty result
9079         suggestions = mDb1.searchSuggestionAsync(
9080                 /*suggestionQueryExpression=*/"f",
9081                 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10)
9082                         .addFilterNamespaces("nonExistNamespace").build()).get();
9083         assertThat(suggestions).isEmpty();
9084     }
9085 
9086     @Test
testSearchSuggestion_documentIdFilter()9087     public void testSearchSuggestion_documentIdFilter() throws Exception {
9088         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION));
9089         // Schema registration
9090         AppSearchSchema schema = new AppSearchSchema.Builder("Type").addProperty(
9091                         new StringPropertyConfig.Builder("body")
9092                                 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
9093                                 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
9094                                 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
9095                                 .build())
9096                 .build();
9097         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
9098 
9099         // Index documents
9100         GenericDocument doc1 = new GenericDocument.Builder<>("namespace1", "id1", "Type")
9101                 .setPropertyString("body", "termone")
9102                 .build();
9103         GenericDocument doc2 = new GenericDocument.Builder<>("namespace1", "id2", "Type")
9104                 .setPropertyString("body", "termtwo")
9105                 .build();
9106         GenericDocument doc3 = new GenericDocument.Builder<>("namespace2", "id3", "Type")
9107                 .setPropertyString("body", "termthree")
9108                 .build();
9109         GenericDocument doc4 = new GenericDocument.Builder<>("namespace2", "id4", "Type")
9110                 .setPropertyString("body", "termfour")
9111                 .build();
9112 
9113         checkIsBatchResultSuccess(mDb1.putAsync(new PutDocumentsRequest.Builder()
9114                 .addGenericDocuments(doc1, doc2, doc3, doc4).build()));
9115 
9116         SearchSuggestionResult resultOne =
9117                 new SearchSuggestionResult.Builder().setSuggestedResult("termone").build();
9118         SearchSuggestionResult resultTwo =
9119                 new SearchSuggestionResult.Builder().setSuggestedResult("termtwo").build();
9120         SearchSuggestionResult resultThree =
9121                 new SearchSuggestionResult.Builder().setSuggestedResult("termthree").build();
9122         SearchSuggestionResult resultFour =
9123                 new SearchSuggestionResult.Builder().setSuggestedResult("termfour").build();
9124 
9125         // Only search for namespace1/doc1
9126         List<SearchSuggestionResult> suggestions = mDb1.searchSuggestionAsync(
9127                 /*suggestionQueryExpression=*/"t",
9128                 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10)
9129                         .addFilterNamespaces("namespace1")
9130                         .addFilterDocumentIds("namespace1", "id1")
9131                         .build()).get();
9132         assertThat(suggestions).containsExactly(resultOne);
9133 
9134         // Only search for namespace1/doc1 and namespace1/doc2
9135         suggestions = mDb1.searchSuggestionAsync(
9136                 /*suggestionQueryExpression=*/"t",
9137                 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10)
9138                         .addFilterNamespaces("namespace1")
9139                         .addFilterDocumentIds("namespace1", ImmutableList.of("id1", "id2"))
9140                         .build()).get();
9141         assertThat(suggestions).containsExactly(resultOne, resultTwo);
9142 
9143         // Only search for namespace1/doc1 and namespace2/doc3
9144         suggestions = mDb1.searchSuggestionAsync(
9145                 /*suggestionQueryExpression=*/"t",
9146                 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10)
9147                         .addFilterNamespaces("namespace1", "namespace2")
9148                         .addFilterDocumentIds("namespace1", "id1")
9149                         .addFilterDocumentIds("namespace2", ImmutableList.of("id3"))
9150                         .build()).get();
9151         assertThat(suggestions).containsExactly(resultOne, resultThree);
9152 
9153         // Only search for namespace1/doc1 and everything in namespace2
9154         suggestions = mDb1.searchSuggestionAsync(
9155                 /*suggestionQueryExpression=*/"t",
9156                 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10)
9157                         .addFilterDocumentIds("namespace1", "id1")
9158                         .build()).get();
9159         assertThat(suggestions).containsExactly(resultOne, resultThree, resultFour);
9160     }
9161 
9162     @Test
testSearchSuggestion_schemaFilter()9163     public void testSearchSuggestion_schemaFilter() throws Exception {
9164         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION));
9165         // Schema registration
9166         AppSearchSchema schemaType1 = new AppSearchSchema.Builder("Type1").addProperty(
9167                         new StringPropertyConfig.Builder("body")
9168                                 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
9169                                 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
9170                                 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
9171                                 .build())
9172                 .build();
9173         AppSearchSchema schemaType2 = new AppSearchSchema.Builder("Type2").addProperty(
9174                         new StringPropertyConfig.Builder("body")
9175                                 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
9176                                 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
9177                                 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
9178                                 .build())
9179                 .build();
9180         AppSearchSchema schemaType3 = new AppSearchSchema.Builder("Type3").addProperty(
9181                         new StringPropertyConfig.Builder("body")
9182                                 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
9183                                 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
9184                                 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
9185                                 .build())
9186                 .build();
9187         mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
9188                 .addSchemas(schemaType1, schemaType2, schemaType3).build()).get();
9189 
9190         // Index documents
9191         GenericDocument doc1 = new GenericDocument.Builder<>("namespace", "id1", "Type1")
9192                 .setPropertyString("body", "fo foo")
9193                 .build();
9194         GenericDocument doc2 = new GenericDocument.Builder<>("namespace", "id2", "Type2")
9195                 .setPropertyString("body", "foo")
9196                 .build();
9197         GenericDocument doc3 = new GenericDocument.Builder<>("namespace", "id3", "Type3")
9198                 .setPropertyString("body", "fool")
9199                 .build();
9200 
9201         checkIsBatchResultSuccess(mDb1.putAsync(
9202                 new PutDocumentsRequest.Builder().addGenericDocuments(doc1, doc2, doc3).build()));
9203 
9204         SearchSuggestionResult resultFo =
9205                 new SearchSuggestionResult.Builder().setSuggestedResult("fo").build();
9206         SearchSuggestionResult resultFoo =
9207                 new SearchSuggestionResult.Builder().setSuggestedResult("foo").build();
9208         SearchSuggestionResult resultFool =
9209                 new SearchSuggestionResult.Builder().setSuggestedResult("fool").build();
9210 
9211         // Type1 has 2 results.
9212         List<SearchSuggestionResult> suggestions = mDb1.searchSuggestionAsync(
9213                 /*suggestionQueryExpression=*/"f",
9214                 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10)
9215                         .addFilterSchemas("Type1").build()).get();
9216         assertThat(suggestions).containsExactly(resultFoo, resultFo).inOrder();
9217 
9218         // Type2 has 1 result.
9219         suggestions = mDb1.searchSuggestionAsync(
9220                 /*suggestionQueryExpression=*/"f",
9221                 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10)
9222                         .addFilterSchemas("Type2").build()).get();
9223         assertThat(suggestions).containsExactly(resultFoo).inOrder();
9224 
9225         // Type2 and 3 has 2 results.
9226         suggestions = mDb1.searchSuggestionAsync(
9227                 /*suggestionQueryExpression=*/"f",
9228                 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10)
9229                         .addFilterSchemas("Type2", "Type3")
9230                         .build()).get();
9231         assertThat(suggestions).containsExactly(resultFoo, resultFool);
9232 
9233         // non exist type has empty result.
9234         suggestions = mDb1.searchSuggestionAsync(
9235                 /*suggestionQueryExpression=*/"f",
9236                 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10)
9237                         .addFilterSchemas("nonExistType").build()).get();
9238         assertThat(suggestions).isEmpty();
9239     }
9240 
9241     @Test
testSearchSuggestion_differentPrefix()9242     public void testSearchSuggestion_differentPrefix() throws Exception {
9243         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION));
9244         // Schema registration
9245         AppSearchSchema schema = new AppSearchSchema.Builder("Type").addProperty(
9246                         new StringPropertyConfig.Builder("body")
9247                                 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
9248                                 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
9249                                 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
9250                                 .build())
9251                 .build();
9252         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
9253 
9254         // Index documents
9255         GenericDocument doc1 = new GenericDocument.Builder<>("namespace", "id1", "Type")
9256                 .setPropertyString("body", "foo")
9257                 .build();
9258         GenericDocument doc2 = new GenericDocument.Builder<>("namespace", "id2", "Type")
9259                 .setPropertyString("body", "fool")
9260                 .build();
9261         GenericDocument doc3 = new GenericDocument.Builder<>("namespace", "id3", "Type")
9262                 .setPropertyString("body", "bar")
9263                 .build();
9264         GenericDocument doc4 = new GenericDocument.Builder<>("namespace", "id4", "Type")
9265                 .setPropertyString("body", "baz")
9266                 .build();
9267 
9268         checkIsBatchResultSuccess(mDb1.putAsync(
9269                 new PutDocumentsRequest.Builder().addGenericDocuments(doc1, doc2, doc3, doc4)
9270                         .build()));
9271 
9272         SearchSuggestionResult resultFoo =
9273                 new SearchSuggestionResult.Builder().setSuggestedResult("foo").build();
9274         SearchSuggestionResult resultFool =
9275                 new SearchSuggestionResult.Builder().setSuggestedResult("fool").build();
9276         SearchSuggestionResult resultBar =
9277                 new SearchSuggestionResult.Builder().setSuggestedResult("bar").build();
9278         SearchSuggestionResult resultBaz =
9279                 new SearchSuggestionResult.Builder().setSuggestedResult("baz").build();
9280 
9281         // prefix f has 2 results.
9282         List<SearchSuggestionResult> suggestions = mDb1.searchSuggestionAsync(
9283                 /*suggestionQueryExpression=*/"f",
9284                 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get();
9285         assertThat(suggestions).containsExactly(resultFoo, resultFool);
9286 
9287         // prefix b has 2 results.
9288         suggestions = mDb1.searchSuggestionAsync(
9289                 /*suggestionQueryExpression=*/"b",
9290                 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get();
9291         assertThat(suggestions).containsExactly(resultBar, resultBaz);
9292     }
9293 
9294     @Test
testSearchSuggestion_differentRankingStrategy()9295     public void testSearchSuggestion_differentRankingStrategy() throws Exception {
9296         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION));
9297         // Schema registration
9298         AppSearchSchema schema = new AppSearchSchema.Builder("Type").addProperty(
9299                         new StringPropertyConfig.Builder("body")
9300                                 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
9301                                 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
9302                                 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
9303                                 .build())
9304                 .build();
9305         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
9306 
9307         // Index documents
9308         // term1 appears 3 times in all 3 docs.
9309         // term2 appears 4 times in 2 docs.
9310         // term3 appears 5 times in 1 doc.
9311         GenericDocument doc1 = new GenericDocument.Builder<>("namespace", "id1", "Type")
9312                 .setPropertyString("body", "term1 term3 term3 term3 term3 term3")
9313                 .build();
9314         GenericDocument doc2 = new GenericDocument.Builder<>("namespace", "id2", "Type")
9315                 .setPropertyString("body", "term1 term2 term2 term2")
9316                 .build();
9317         GenericDocument doc3 = new GenericDocument.Builder<>("namespace", "id3", "Type")
9318                 .setPropertyString("body", "term1 term2")
9319                 .build();
9320 
9321         checkIsBatchResultSuccess(mDb1.putAsync(
9322                 new PutDocumentsRequest.Builder().addGenericDocuments(doc1, doc2, doc3)
9323                         .build()));
9324 
9325         SearchSuggestionResult result1 =
9326                 new SearchSuggestionResult.Builder().setSuggestedResult("term1").build();
9327         SearchSuggestionResult result2 =
9328                 new SearchSuggestionResult.Builder().setSuggestedResult("term2").build();
9329         SearchSuggestionResult result3 =
9330                 new SearchSuggestionResult.Builder().setSuggestedResult("term3").build();
9331 
9332 
9333         // rank by NONE, the order should be arbitrary but all terms appear.
9334         List<SearchSuggestionResult> suggestions = mDb1.searchSuggestionAsync(
9335                 /*suggestionQueryExpression=*/"t",
9336                 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10)
9337                         .setRankingStrategy(SearchSuggestionSpec
9338                                 .SUGGESTION_RANKING_STRATEGY_NONE)
9339                         .build()).get();
9340         assertThat(suggestions).containsExactly(result2, result1, result3);
9341 
9342         // rank by document count, the order should be term1:3 > term2:2 > term3:1
9343         suggestions = mDb1.searchSuggestionAsync(
9344                 /*suggestionQueryExpression=*/"t",
9345                 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10)
9346                         .setRankingStrategy(SearchSuggestionSpec
9347                                 .SUGGESTION_RANKING_STRATEGY_DOCUMENT_COUNT)
9348                         .build()).get();
9349         assertThat(suggestions).containsExactly(result1, result2, result3).inOrder();
9350 
9351         // rank by term frequency, the order should be term3:5 > term2:4 > term1:3
9352         suggestions = mDb1.searchSuggestionAsync(
9353                 /*suggestionQueryExpression=*/"t",
9354                 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10)
9355                         .setRankingStrategy(SearchSuggestionSpec
9356                                 .SUGGESTION_RANKING_STRATEGY_TERM_FREQUENCY)
9357                         .build()).get();
9358         assertThat(suggestions).containsExactly(result3, result2, result1).inOrder();
9359     }
9360 
9361     @Test
testSearchSuggestion_removeDocument()9362     public void testSearchSuggestion_removeDocument() throws Exception {
9363         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION));
9364         // Schema registration
9365         AppSearchSchema schema = new AppSearchSchema.Builder("Type").addProperty(
9366                         new StringPropertyConfig.Builder("body")
9367                                 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
9368                                 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
9369                                 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
9370                                 .build())
9371                 .build();
9372         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
9373 
9374         // Index documents
9375         GenericDocument docTwo = new GenericDocument.Builder<>("namespace", "idTwo", "Type")
9376                 .setPropertyString("body", "two")
9377                 .build();
9378         GenericDocument docThree = new GenericDocument.Builder<>("namespace", "idThree", "Type")
9379                 .setPropertyString("body", "three")
9380                 .build();
9381         GenericDocument docTart = new GenericDocument.Builder<>("namespace", "idTart", "Type")
9382                 .setPropertyString("body", "tart")
9383                 .build();
9384 
9385         checkIsBatchResultSuccess(mDb1.putAsync(
9386                 new PutDocumentsRequest.Builder().addGenericDocuments(docTwo, docThree, docTart)
9387                         .build()));
9388 
9389         SearchSuggestionResult resultTwo =
9390                 new SearchSuggestionResult.Builder().setSuggestedResult("two").build();
9391         SearchSuggestionResult resultThree =
9392                 new SearchSuggestionResult.Builder().setSuggestedResult("three").build();
9393         SearchSuggestionResult resultTart =
9394                 new SearchSuggestionResult.Builder().setSuggestedResult("tart").build();
9395 
9396         // prefix t has 3 results.
9397         List<SearchSuggestionResult> suggestions = mDb1.searchSuggestionAsync(
9398                 /*suggestionQueryExpression=*/"t",
9399                 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get();
9400         assertThat(suggestions).containsExactly(resultTwo, resultThree, resultTart);
9401 
9402         // Delete the document
9403         checkIsBatchResultSuccess(mDb1.removeAsync(
9404                 new RemoveByDocumentIdRequest.Builder("namespace").addIds(
9405                         "idTwo").build()));
9406 
9407         // now prefix t has 2 results.
9408         suggestions = mDb1.searchSuggestionAsync(
9409                 /*suggestionQueryExpression=*/"t",
9410                 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get();
9411         assertThat(suggestions).containsExactly(resultThree, resultTart);
9412     }
9413 
9414     @Test
testSearchSuggestion_replacementDocument()9415     public void testSearchSuggestion_replacementDocument() throws Exception {
9416         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION));
9417         // Schema registration
9418         AppSearchSchema schema = new AppSearchSchema.Builder("Type").addProperty(
9419                         new StringPropertyConfig.Builder("body")
9420                                 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
9421                                 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
9422                                 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
9423                                 .build())
9424                 .build();
9425         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
9426 
9427         // Index documents
9428         GenericDocument doc = new GenericDocument.Builder<>("namespace", "id", "Type")
9429                 .setPropertyString("body", "two three tart")
9430                 .build();
9431 
9432         checkIsBatchResultSuccess(mDb1.putAsync(
9433                 new PutDocumentsRequest.Builder().addGenericDocuments(doc)
9434                         .build()));
9435 
9436         SearchSuggestionResult resultTwo =
9437                 new SearchSuggestionResult.Builder().setSuggestedResult("two").build();
9438         SearchSuggestionResult resultThree =
9439                 new SearchSuggestionResult.Builder().setSuggestedResult("three").build();
9440         SearchSuggestionResult resultTart =
9441                 new SearchSuggestionResult.Builder().setSuggestedResult("tart").build();
9442         SearchSuggestionResult resultTwist =
9443                 new SearchSuggestionResult.Builder().setSuggestedResult("twist").build();
9444 
9445         // prefix t has 3 results.
9446         List<SearchSuggestionResult> suggestions = mDb1.searchSuggestionAsync(
9447                 /*suggestionQueryExpression=*/"t",
9448                 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get();
9449         assertThat(suggestions).containsExactly(resultTwo, resultThree, resultTart);
9450 
9451         // replace the document
9452         GenericDocument replaceDoc = new GenericDocument.Builder<>("namespace", "id", "Type")
9453                 .setPropertyString("body", "twist three")
9454                 .build();
9455         checkIsBatchResultSuccess(mDb1.putAsync(
9456                 new PutDocumentsRequest.Builder().addGenericDocuments(replaceDoc)
9457                         .build()));
9458 
9459         // prefix t has 2 results for now.
9460         suggestions = mDb1.searchSuggestionAsync(
9461                 /*suggestionQueryExpression=*/"t",
9462                 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get();
9463         assertThat(suggestions).containsExactly(resultThree, resultTwist);
9464     }
9465 
9466     @Test
testSearchSuggestion_twoInstances()9467     public void testSearchSuggestion_twoInstances() throws Exception {
9468         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION));
9469         // Schema registration
9470         AppSearchSchema schema = new AppSearchSchema.Builder("Type").addProperty(
9471                         new StringPropertyConfig.Builder("body")
9472                                 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
9473                                 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
9474                                 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
9475                                 .build())
9476                 .build();
9477         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
9478         mDb2.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
9479 
9480         // Index documents to database 1.
9481         GenericDocument doc1 = new GenericDocument.Builder<>("namespace", "id1", "Type")
9482                 .setPropertyString("body", "termOne termTwo")
9483                 .build();
9484         GenericDocument doc2 = new GenericDocument.Builder<>("namespace", "id2", "Type")
9485                 .setPropertyString("body", "termOne")
9486                 .build();
9487         checkIsBatchResultSuccess(mDb1.putAsync(
9488                 new PutDocumentsRequest.Builder().addGenericDocuments(doc1, doc2)
9489                         .build()));
9490 
9491         SearchSuggestionResult resultOne =
9492                 new SearchSuggestionResult.Builder().setSuggestedResult("termone").build();
9493         SearchSuggestionResult resultTwo =
9494                 new SearchSuggestionResult.Builder().setSuggestedResult("termtwo").build();
9495 
9496         // database 1 could get suggestion results
9497         List<SearchSuggestionResult> suggestions = mDb1.searchSuggestionAsync(
9498                 /*suggestionQueryExpression=*/"t",
9499                 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get();
9500         assertThat(suggestions).containsExactly(resultOne, resultTwo).inOrder();
9501 
9502         // database 2 couldn't get suggestion results
9503         suggestions = mDb2.searchSuggestionAsync(
9504                 /*suggestionQueryExpression=*/"t",
9505                 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get();
9506         assertThat(suggestions).isEmpty();
9507     }
9508 
9509     @Test
testSearchSuggestion_multipleTerms()9510     public void testSearchSuggestion_multipleTerms() throws Exception {
9511         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION));
9512         // Schema registration
9513         AppSearchSchema schema = new AppSearchSchema.Builder("Type").addProperty(
9514                         new StringPropertyConfig.Builder("body")
9515                                 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
9516                                 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
9517                                 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
9518                                 .build())
9519                 .build();
9520         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
9521 
9522         // Index documents
9523         GenericDocument doc1 = new GenericDocument.Builder<>("namespace", "id1", "Type")
9524                 .setPropertyString("body", "bar fo")
9525                 .build();
9526         GenericDocument doc2 = new GenericDocument.Builder<>("namespace", "id2", "Type")
9527                 .setPropertyString("body", "cat foo")
9528                 .build();
9529         GenericDocument doc3 = new GenericDocument.Builder<>("namespace", "id3", "Type")
9530                 .setPropertyString("body", "fool")
9531                 .build();
9532         checkIsBatchResultSuccess(mDb1.putAsync(
9533                 new PutDocumentsRequest.Builder().addGenericDocuments(doc1, doc2, doc3)
9534                         .build()));
9535 
9536         // Search "bar AND f" only document 1 should match the search.
9537         List<SearchSuggestionResult> suggestions = mDb1.searchSuggestionAsync(
9538                 /*suggestionQueryExpression=*/"bar f",
9539                 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get();
9540         SearchSuggestionResult barFo =
9541                 new SearchSuggestionResult.Builder().setSuggestedResult("bar fo").build();
9542         assertThat(suggestions).containsExactly(barFo);
9543 
9544         // Search for "(bar OR cat) AND f" both document1 "bar fo" and document2 "cat foo" could
9545         // match.
9546         suggestions = mDb1.searchSuggestionAsync(
9547                 /*suggestionQueryExpression=*/"bar OR cat f",
9548                 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get();
9549         SearchSuggestionResult barCatFo =
9550                 new SearchSuggestionResult.Builder().setSuggestedResult("bar OR cat fo").build();
9551         SearchSuggestionResult barCatFoo =
9552                 new SearchSuggestionResult.Builder().setSuggestedResult("bar OR cat foo").build();
9553         assertThat(suggestions).containsExactly(barCatFo, barCatFoo);
9554 
9555         // Search for "(bar AND cat) OR f", all documents could match.
9556         suggestions = mDb1.searchSuggestionAsync(
9557                 /*suggestionQueryExpression=*/"(bar cat) OR f",
9558                 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get();
9559         SearchSuggestionResult barCatOrFo =
9560                 new SearchSuggestionResult.Builder().setSuggestedResult("(bar cat) OR fo").build();
9561         SearchSuggestionResult barCatOrFoo =
9562                 new SearchSuggestionResult.Builder().setSuggestedResult("(bar cat) OR foo").build();
9563         SearchSuggestionResult barCatOrFool =
9564                 new SearchSuggestionResult.Builder()
9565                         .setSuggestedResult("(bar cat) OR fool").build();
9566         assertThat(suggestions).containsExactly(barCatOrFo, barCatOrFoo, barCatOrFool);
9567 
9568         // Search for "-bar f", document2 "cat foo" could and document3 "fool" could match.
9569         suggestions = mDb1.searchSuggestionAsync(
9570                 /*suggestionQueryExpression=*/"-bar f",
9571                 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get();
9572         SearchSuggestionResult noBarFoo =
9573                 new SearchSuggestionResult.Builder().setSuggestedResult("-bar foo").build();
9574         SearchSuggestionResult noBarFool =
9575                 new SearchSuggestionResult.Builder().setSuggestedResult("-bar fool").build();
9576         assertThat(suggestions).containsExactly(noBarFoo, noBarFool);
9577     }
9578 
9579     @Test
testSearchSuggestion_propertyFilter()9580     public void testSearchSuggestion_propertyFilter() throws Exception {
9581         assumeTrue(mDb1.getFeatures().isFeatureSupported(
9582                 Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
9583         // Schema registration
9584         AppSearchSchema schemaType1 =
9585                 new AppSearchSchema.Builder("Type1")
9586                         .addProperty(
9587                                 new StringPropertyConfig.Builder("propertyone")
9588                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
9589                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
9590                                         .setIndexingType(
9591                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
9592                                         .build())
9593                         .addProperty(
9594                                 new StringPropertyConfig.Builder("propertytwo")
9595                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
9596                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
9597                                         .setIndexingType(
9598                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
9599                                         .build())
9600                         .build();
9601         AppSearchSchema schemaType2 =
9602                 new AppSearchSchema.Builder("Type2")
9603                         .addProperty(
9604                                 new StringPropertyConfig.Builder("propertythree")
9605                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
9606                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
9607                                         .setIndexingType(
9608                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
9609                                         .build())
9610                         .addProperty(
9611                                 new StringPropertyConfig.Builder("propertyfour")
9612                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
9613                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
9614                                         .setIndexingType(
9615                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
9616                                         .build())
9617                         .build();
9618         mDb1.setSchemaAsync(
9619                         new SetSchemaRequest.Builder().addSchemas(schemaType1, schemaType2).build())
9620                 .get();
9621 
9622         // Index documents
9623         GenericDocument doc1 =
9624                 new GenericDocument.Builder<>("namespace", "id1", "Type1")
9625                         .setPropertyString("propertyone", "termone")
9626                         .setPropertyString("propertytwo", "termtwo")
9627                         .build();
9628         GenericDocument doc2 =
9629                 new GenericDocument.Builder<>("namespace", "id2", "Type2")
9630                         .setPropertyString("propertythree", "termthree")
9631                         .setPropertyString("propertyfour", "termfour")
9632                         .build();
9633 
9634         checkIsBatchResultSuccess(
9635                 mDb1.putAsync(
9636                         new PutDocumentsRequest.Builder().addGenericDocuments(doc1, doc2).build()));
9637 
9638         SearchSuggestionResult resultOne =
9639                 new SearchSuggestionResult.Builder().setSuggestedResult("termone").build();
9640         SearchSuggestionResult resultTwo =
9641                 new SearchSuggestionResult.Builder().setSuggestedResult("termtwo").build();
9642         SearchSuggestionResult resultThree =
9643                 new SearchSuggestionResult.Builder().setSuggestedResult("termthree").build();
9644         SearchSuggestionResult resultFour =
9645                 new SearchSuggestionResult.Builder().setSuggestedResult("termfour").build();
9646 
9647         // Only search for type1/propertyone
9648         List<SearchSuggestionResult> suggestions =
9649                 mDb1.searchSuggestionAsync(
9650                                 /* suggestionQueryExpression= */ "t",
9651                                 new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10)
9652                                         .addFilterSchemas("Type1")
9653                                         .addFilterProperties(
9654                                                 "Type1", ImmutableList.of("propertyone"))
9655                                         .build())
9656                         .get();
9657         assertThat(suggestions).containsExactly(resultOne);
9658 
9659         // Only search for type1/propertyone and type1/propertytwo
9660         suggestions =
9661                 mDb1.searchSuggestionAsync(
9662                                 /* suggestionQueryExpression= */ "t",
9663                                 new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10)
9664                                         .addFilterSchemas("Type1")
9665                                         .addFilterProperties(
9666                                                 "Type1",
9667                                                 ImmutableList.of("propertyone", "propertytwo"))
9668                                         .build())
9669                         .get();
9670         assertThat(suggestions).containsExactly(resultOne, resultTwo);
9671 
9672         // Only search for type1/propertyone and type2/propertythree
9673         suggestions =
9674                 mDb1.searchSuggestionAsync(
9675                                 /* suggestionQueryExpression= */ "t",
9676                                 new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10)
9677                                         .addFilterSchemas("Type1", "Type2")
9678                                         .addFilterProperties(
9679                                                 "Type1", ImmutableList.of("propertyone"))
9680                                         .addFilterProperties(
9681                                                 "Type2", ImmutableList.of("propertythree"))
9682                                         .build())
9683                         .get();
9684         assertThat(suggestions).containsExactly(resultOne, resultThree);
9685 
9686         // Only search for type1/propertyone and type2/propertyfour, in addFilterPropertyPaths
9687         suggestions =
9688                 mDb1.searchSuggestionAsync(
9689                                 /* suggestionQueryExpression= */ "t",
9690                                 new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10)
9691                                         .addFilterSchemas("Type1", "Type2")
9692                                         .addFilterProperties(
9693                                                 "Type1", ImmutableList.of("propertyone"))
9694                                         .addFilterPropertyPaths(
9695                                                 "Type2",
9696                                                 ImmutableList.of(new PropertyPath("propertyfour")))
9697                                         .build())
9698                         .get();
9699         assertThat(suggestions).containsExactly(resultOne, resultFour);
9700 
9701         // Only search for type1/propertyone and everything in type2
9702         suggestions =
9703                 mDb1.searchSuggestionAsync(
9704                                 /* suggestionQueryExpression= */ "t",
9705                                 new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10)
9706                                         .addFilterProperties(
9707                                                 "Type1", ImmutableList.of("propertyone"))
9708                                         .build())
9709                         .get();
9710         assertThat(suggestions).containsExactly(resultOne, resultThree, resultFour);
9711     }
9712 
9713     @Test
testSearchSuggestion_propertyFilter_notSupported()9714     public void testSearchSuggestion_propertyFilter_notSupported() throws Exception {
9715         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION));
9716         assumeFalse(mDb1.getFeatures().isFeatureSupported(
9717                 Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
9718 
9719         SearchSuggestionSpec searchSuggestionSpec =
9720                 new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10)
9721                     .addFilterSchemas("Type1")
9722                     .addFilterProperties("Type1", ImmutableList.of("property"))
9723                     .build();
9724 
9725         // Search suggest with type property filters {"Email", ["property"]} and verify that
9726         // unsupported exception is thrown
9727         UnsupportedOperationException exception =
9728                 assertThrows(UnsupportedOperationException.class,
9729                         () -> mDb1.searchSuggestionAsync(
9730                                 /* suggestionQueryExpression= */ "t", searchSuggestionSpec).get());
9731         assertThat(exception).hasMessageThat().contains(Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES
9732                 + " is not available on this AppSearch implementation.");
9733     }
9734 
9735     @Test
testSearchSuggestion_PropertyRestriction()9736     public void testSearchSuggestion_PropertyRestriction() throws Exception {
9737         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION));
9738         // Schema registration
9739         AppSearchSchema schema = new AppSearchSchema.Builder("Type")
9740                 .addProperty(new StringPropertyConfig.Builder("subject")
9741                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
9742                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
9743                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
9744                         .build())
9745                 .addProperty(new StringPropertyConfig.Builder("body")
9746                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
9747                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
9748                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
9749                         .build())
9750                 .build();
9751         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
9752 
9753         // Index documents
9754         GenericDocument doc1 = new GenericDocument.Builder<>("namespace", "id1", "Type")
9755                 .setPropertyString("subject", "bar fo")
9756                 .setPropertyString("body", "fool")
9757                 .build();
9758         GenericDocument doc2 = new GenericDocument.Builder<>("namespace", "id2", "Type")
9759                 .setPropertyString("subject", "bar cat foo")
9760                 .setPropertyString("body", "fool")
9761                 .build();
9762         GenericDocument doc3 = new GenericDocument.Builder<>("namespace", "ide", "Type")
9763                 .setPropertyString("subject", "fool")
9764                 .setPropertyString("body", "fool")
9765                 .build();
9766         checkIsBatchResultSuccess(mDb1.putAsync(
9767                 new PutDocumentsRequest.Builder().addGenericDocuments(doc1, doc2, doc3)
9768                         .build()));
9769 
9770         // Search for "bar AND subject:f"
9771         List<SearchSuggestionResult> suggestions = mDb1.searchSuggestionAsync(
9772                 /*suggestionQueryExpression=*/"bar subject:f",
9773                 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get();
9774         SearchSuggestionResult barSubjectFo =
9775                 new SearchSuggestionResult.Builder().setSuggestedResult("bar subject:fo").build();
9776         SearchSuggestionResult barSubjectFoo =
9777                 new SearchSuggestionResult.Builder().setSuggestedResult("bar subject:foo").build();
9778         assertThat(suggestions).containsExactly(barSubjectFo, barSubjectFoo);
9779     }
9780 
9781     @Test
testGetSchema_parentTypes()9782     public void testGetSchema_parentTypes() throws Exception {
9783         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
9784         AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email").build();
9785         AppSearchSchema messageSchema = new AppSearchSchema.Builder("Message").build();
9786         AppSearchSchema emailMessageSchema =
9787                 new AppSearchSchema.Builder("EmailMessage")
9788                         .addProperty(
9789                                 new StringPropertyConfig.Builder("sender")
9790                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
9791                                         .build())
9792                         .addProperty(
9793                                 new StringPropertyConfig.Builder("email")
9794                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
9795                                         .build())
9796                         .addProperty(
9797                                 new StringPropertyConfig.Builder("content")
9798                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
9799                                         .build())
9800                         .addParentType("Email")
9801                         .addParentType("Message")
9802                         .build();
9803 
9804         SetSchemaRequest request =
9805                 new SetSchemaRequest.Builder()
9806                         .addSchemas(emailMessageSchema)
9807                         .addSchemas(emailSchema)
9808                         .addSchemas(messageSchema)
9809                         .build();
9810 
9811         mDb1.setSchemaAsync(request).get();
9812 
9813         Set<AppSearchSchema> actual = mDb1.getSchemaAsync().get().getSchemas();
9814         assertThat(actual).hasSize(3);
9815         assertThat(actual).isEqualTo(request.getSchemas());
9816 
9817         // Check that calling getParentType() for the EmailMessage schema returns Email and Message
9818         for (AppSearchSchema schema : actual) {
9819             if (schema.getSchemaType().equals("EmailMessage")) {
9820                 assertThat(schema.getParentTypes()).containsExactly("Email", "Message");
9821             }
9822         }
9823     }
9824 
9825     @Test
testGetSchema_parentTypes_notSupported()9826     public void testGetSchema_parentTypes_notSupported() throws Exception {
9827         assumeFalse(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
9828         AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email").build();
9829         AppSearchSchema messageSchema = new AppSearchSchema.Builder("Message").build();
9830         AppSearchSchema emailMessageSchema =
9831                 new AppSearchSchema.Builder("EmailMessage")
9832                         .addParentType("Email")
9833                         .addParentType("Message")
9834                         .build();
9835 
9836         SetSchemaRequest request =
9837                 new SetSchemaRequest.Builder()
9838                         .addSchemas(emailMessageSchema)
9839                         .addSchemas(emailSchema)
9840                         .addSchemas(messageSchema)
9841                         .build();
9842 
9843         UnsupportedOperationException e =
9844                 assertThrows(
9845                         UnsupportedOperationException.class,
9846                         () -> mDb1.setSchemaAsync(request).get());
9847         assertThat(e)
9848                 .hasMessageThat()
9849                 .contains(
9850                         Features.SCHEMA_ADD_PARENT_TYPE
9851                                 + " is not available on this AppSearch implementation.");
9852     }
9853 
9854     @Test
testGetSchema_indexableNestedPropsList()9855     public void testGetSchema_indexableNestedPropsList() throws Exception {
9856         assumeTrue(
9857                 mDb1.getFeatures()
9858                         .isFeatureSupported(Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES));
9859 
9860         AppSearchSchema personSchema =
9861                 new AppSearchSchema.Builder("Person")
9862                         .addProperty(
9863                                 new StringPropertyConfig.Builder("name")
9864                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
9865                                         .setIndexingType(
9866                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
9867                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
9868                                         .build())
9869                         .addProperty(
9870                                 new AppSearchSchema.DocumentPropertyConfig.Builder(
9871                                         "worksFor", "Organization")
9872                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
9873                                         .setShouldIndexNestedProperties(false)
9874                                         .addIndexableNestedProperties(Collections.singleton("name"))
9875                                         .build())
9876                         .build();
9877         AppSearchSchema organizationSchema =
9878                 new AppSearchSchema.Builder("Organization")
9879                         .addProperty(
9880                                 new StringPropertyConfig.Builder("name")
9881                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
9882                                         .setIndexingType(
9883                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
9884                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
9885                                         .build())
9886                         .addProperty(
9887                                 new StringPropertyConfig.Builder("notes")
9888                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
9889                                         .setIndexingType(
9890                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
9891                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
9892                                         .build())
9893                         .build();
9894 
9895         SetSchemaRequest setSchemaRequest =
9896                 new SetSchemaRequest.Builder()
9897                         .addSchemas(personSchema, organizationSchema)
9898                         .build();
9899         mDb1.setSchemaAsync(setSchemaRequest).get();
9900 
9901         Set<AppSearchSchema> actual = mDb1.getSchemaAsync().get().getSchemas();
9902         assertThat(actual).hasSize(2);
9903         assertThat(actual).isEqualTo(setSchemaRequest.getSchemas());
9904 
9905         for (AppSearchSchema schema : actual) {
9906             if (schema.getSchemaType().equals("Person")) {
9907                 for (PropertyConfig property : schema.getProperties()) {
9908                     if (property.getName().equals("worksFor")) {
9909                         assertThat(
9910                                 ((DocumentPropertyConfig) property)
9911                                         .getIndexableNestedProperties()).containsExactly("name");
9912                     }
9913                 }
9914             }
9915         }
9916     }
9917 
9918     @Test
testSetSchema_dataTypeIncompatibleWithParentTypes()9919     public void testSetSchema_dataTypeIncompatibleWithParentTypes() throws Exception {
9920         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
9921         AppSearchSchema messageSchema =
9922                 new AppSearchSchema.Builder("Message")
9923                         .addProperty(
9924                                 new AppSearchSchema.LongPropertyConfig.Builder("sender")
9925                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
9926                                         .build())
9927                         .build();
9928         AppSearchSchema emailSchema =
9929                 new AppSearchSchema.Builder("Email")
9930                         .addParentType("Message")
9931                         .addProperty(
9932                                 new StringPropertyConfig.Builder("sender")
9933                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
9934                                         .build())
9935                         .build();
9936 
9937         SetSchemaRequest request =
9938                 new SetSchemaRequest.Builder()
9939                         .addSchemas(messageSchema)
9940                         .addSchemas(emailSchema)
9941                         .build();
9942 
9943         ExecutionException executionException =
9944                 assertThrows(ExecutionException.class, () -> mDb1.setSchemaAsync(request).get());
9945         assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
9946         AppSearchException exception = (AppSearchException) executionException.getCause();
9947         assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT);
9948         assertThat(exception)
9949                 .hasMessageThat()
9950                 .containsMatch(
9951                         "Property sender from child type .*\\$/Email is not compatible"
9952                                 + " to the parent type .*\\$/Message.");
9953     }
9954 
9955     @Test
testSetSchema_documentTypeIncompatibleWithParentTypes()9956     public void testSetSchema_documentTypeIncompatibleWithParentTypes() throws Exception {
9957         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
9958         AppSearchSchema personSchema = new AppSearchSchema.Builder("Person").build();
9959         AppSearchSchema artistSchema =
9960                 new AppSearchSchema.Builder("Artist").addParentType("Person").build();
9961         AppSearchSchema messageSchema =
9962                 new AppSearchSchema.Builder("Message")
9963                         .addProperty(
9964                                 new AppSearchSchema.DocumentPropertyConfig.Builder(
9965                                         "sender", "Artist")
9966                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
9967                                         .build())
9968                         .build();
9969         AppSearchSchema emailSchema =
9970                 new AppSearchSchema.Builder("Email")
9971                         .addParentType("Message")
9972                         // "sender" is defined as an Artist in the parent type Message, which
9973                         // requires "sender"'s type here to be a subtype of Artist. Thus, this is
9974                         // incompatible because Person is not a subtype of Artist.
9975                         .addProperty(
9976                                 new AppSearchSchema.DocumentPropertyConfig.Builder(
9977                                         "sender", "Person")
9978                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
9979                                         .build())
9980                         .build();
9981 
9982         SetSchemaRequest request =
9983                 new SetSchemaRequest.Builder()
9984                         .addSchemas(personSchema)
9985                         .addSchemas(artistSchema)
9986                         .addSchemas(messageSchema)
9987                         .addSchemas(emailSchema)
9988                         .build();
9989 
9990         ExecutionException executionException =
9991                 assertThrows(ExecutionException.class, () -> mDb1.setSchemaAsync(request).get());
9992         assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
9993         AppSearchException exception = (AppSearchException) executionException.getCause();
9994         assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT);
9995         assertThat(exception)
9996                 .hasMessageThat()
9997                 .containsMatch(
9998                         "Property sender from child type .*\\$/Email is not compatible"
9999                                 + " to the parent type .*\\$/Message.");
10000     }
10001 
10002     @Test
testSetSchema_compatibleWithParentTypes()10003     public void testSetSchema_compatibleWithParentTypes() throws Exception {
10004         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
10005         AppSearchSchema personSchema = new AppSearchSchema.Builder("Person").build();
10006         AppSearchSchema artistSchema =
10007                 new AppSearchSchema.Builder("Artist").addParentType("Person").build();
10008         AppSearchSchema messageSchema =
10009                 new AppSearchSchema.Builder("Message")
10010                         .addProperty(
10011                                 new AppSearchSchema.DocumentPropertyConfig.Builder(
10012                                         "sender", "Person")
10013                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
10014                                         .build())
10015                         .addProperty(
10016                                 new StringPropertyConfig.Builder("note")
10017                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
10018                                         .setIndexingType(
10019                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
10020                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
10021                                         .build())
10022                         .build();
10023         AppSearchSchema emailSchema =
10024                 new AppSearchSchema.Builder("Email")
10025                         .addParentType("Message")
10026                         .addProperty(
10027                                 // Artist is a subtype of Person, so compatible
10028                                 new AppSearchSchema.DocumentPropertyConfig.Builder(
10029                                         "sender", "Artist")
10030                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
10031                                         .build())
10032                         .addProperty(
10033                                 new StringPropertyConfig.Builder("note")
10034                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
10035                                         // A different indexing or tokenizer type is ok.
10036                                         .setIndexingType(
10037                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
10038                                         .setTokenizerType(
10039                                                 StringPropertyConfig.TOKENIZER_TYPE_VERBATIM)
10040                                         .build())
10041                         .build();
10042 
10043         SetSchemaRequest request =
10044                 new SetSchemaRequest.Builder()
10045                         .addSchemas(personSchema)
10046                         .addSchemas(artistSchema)
10047                         .addSchemas(messageSchema)
10048                         .addSchemas(emailSchema)
10049                         .build();
10050 
10051         mDb1.setSchemaAsync(request).get();
10052 
10053         Set<AppSearchSchema> actual = mDb1.getSchemaAsync().get().getSchemas();
10054         assertThat(actual).hasSize(4);
10055         assertThat(actual).isEqualTo(request.getSchemas());
10056     }
10057 
10058     @Test
testSetSchema_indexableNestedPropsList()10059     public void testSetSchema_indexableNestedPropsList() throws Exception {
10060         assumeTrue(
10061                 mDb1.getFeatures()
10062                         .isFeatureSupported(Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES));
10063 
10064         AppSearchSchema personSchema =
10065                 new AppSearchSchema.Builder("Person")
10066                         .addProperty(
10067                                 new StringPropertyConfig.Builder("name")
10068                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
10069                                         .setIndexingType(
10070                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
10071                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
10072                                         .build())
10073                         .addProperty(
10074                                 new AppSearchSchema.DocumentPropertyConfig.Builder(
10075                                         "worksFor", "Organization")
10076                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
10077                                         .setShouldIndexNestedProperties(false)
10078                                         .addIndexableNestedProperties(Collections.singleton("name"))
10079                                         .build())
10080                         .build();
10081         AppSearchSchema organizationSchema =
10082                 new AppSearchSchema.Builder("Organization")
10083                         .addProperty(
10084                                 new StringPropertyConfig.Builder("name")
10085                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
10086                                         .setIndexingType(
10087                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
10088                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
10089                                         .build())
10090                         .addProperty(
10091                                 new StringPropertyConfig.Builder("notes")
10092                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
10093                                         .setIndexingType(
10094                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
10095                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
10096                                         .build())
10097                         .build();
10098 
10099         mDb1.setSchemaAsync(
10100                         new SetSchemaRequest.Builder()
10101                                 .addSchemas(personSchema, organizationSchema)
10102                                 .build())
10103                 .get();
10104 
10105         // Test that properties in Person's indexable_nested_properties_list are indexed and
10106         // searchable
10107         GenericDocument org1 =
10108                 new GenericDocument.Builder<>("namespace", "org1", "Organization")
10109                         .setPropertyString("name", "Org1")
10110                         .setPropertyString("notes", "Some notes")
10111                         .build();
10112         GenericDocument person1 =
10113                 new GenericDocument.Builder<>("namespace", "person1", "Person")
10114                         .setPropertyString("name", "Jane")
10115                         .setPropertyDocument("worksFor", org1)
10116                         .build();
10117 
10118         AppSearchBatchResult<String, Void> putResult =
10119                 checkIsBatchResultSuccess(
10120                         mDb1.putAsync(
10121                                 new PutDocumentsRequest.Builder()
10122                                         .addGenericDocuments(person1, org1)
10123                                         .build()));
10124         assertThat(putResult.getSuccesses()).containsExactly("person1", null, "org1", null);
10125         assertThat(putResult.getFailures()).isEmpty();
10126 
10127         GetByDocumentIdRequest getByDocumentIdRequest =
10128                 new GetByDocumentIdRequest.Builder("namespace").addIds("person1", "org1").build();
10129         List<GenericDocument> outDocuments = doGet(mDb1, getByDocumentIdRequest);
10130         assertThat(outDocuments).hasSize(2);
10131         assertThat(outDocuments).containsExactly(person1, org1);
10132 
10133         // Both org1 and person should be returned for query "Org1"
10134         // For org1 this matches the 'name' property and for person1 this matches the
10135         // 'worksFor.name' property.
10136         SearchResults searchResults =
10137                 mDb1.search(
10138                         "Org1",
10139                         new SearchSpec.Builder()
10140                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
10141                                 .build());
10142         outDocuments = convertSearchResultsToDocuments(searchResults);
10143         assertThat(outDocuments).hasSize(2);
10144         assertThat(outDocuments).containsExactly(person1, org1);
10145 
10146         // Only org1 should be returned for query "notes", since 'worksFor.notes' is not indexed
10147         // for the Person-type.
10148         searchResults =
10149                 mDb1.search(
10150                         "notes",
10151                         new SearchSpec.Builder()
10152                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
10153                                 .build());
10154         outDocuments = convertSearchResultsToDocuments(searchResults);
10155         assertThat(outDocuments).hasSize(1);
10156         assertThat(outDocuments).containsExactly(org1);
10157     }
10158 
10159     @Test
testSetSchema_indexableNestedPropsList_notSupported()10160     public void testSetSchema_indexableNestedPropsList_notSupported() throws Exception {
10161         assumeFalse(
10162                 mDb1.getFeatures()
10163                         .isFeatureSupported(Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES));
10164 
10165         AppSearchSchema personSchema =
10166                 new AppSearchSchema.Builder("Person")
10167                         .addProperty(
10168                                 new StringPropertyConfig.Builder("name")
10169                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
10170                                         .setIndexingType(
10171                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
10172                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
10173                                         .build())
10174                         .addProperty(
10175                                 new AppSearchSchema.DocumentPropertyConfig.Builder(
10176                                         "worksFor", "Organization")
10177                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
10178                                         .setShouldIndexNestedProperties(false)
10179                                         .addIndexableNestedProperties(Collections.singleton("name"))
10180                                         .build())
10181                         .build();
10182         AppSearchSchema organizationSchema =
10183                 new AppSearchSchema.Builder("Organization")
10184                         .addProperty(
10185                                 new StringPropertyConfig.Builder("name")
10186                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
10187                                         .setIndexingType(
10188                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
10189                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
10190                                         .build())
10191                         .addProperty(
10192                                 new StringPropertyConfig.Builder("notes")
10193                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
10194                                         .setIndexingType(
10195                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
10196                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
10197                                         .build())
10198                         .build();
10199 
10200         SetSchemaRequest setSchemaRequest =
10201                 new SetSchemaRequest.Builder().addSchemas(personSchema, organizationSchema).build();
10202         UnsupportedOperationException e =
10203                 assertThrows(
10204                         UnsupportedOperationException.class,
10205                         () -> mDb1.setSchemaAsync(setSchemaRequest).get());
10206         assertThat(e)
10207                 .hasMessageThat()
10208                 .contains(
10209                         "DocumentPropertyConfig.addIndexableNestedProperties is not supported on"
10210                                 + " this AppSearch implementation.");
10211     }
10212 
10213     @Test
testSetSchema_indexableNestedPropsList_nonIndexableProp()10214     public void testSetSchema_indexableNestedPropsList_nonIndexableProp() throws Exception {
10215         assumeTrue(
10216                 mDb1.getFeatures()
10217                         .isFeatureSupported(Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES));
10218 
10219         AppSearchSchema personSchema =
10220                 new AppSearchSchema.Builder("Person")
10221                         .addProperty(
10222                                 new StringPropertyConfig.Builder("name")
10223                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
10224                                         .setIndexingType(
10225                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
10226                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
10227                                         .build())
10228                         .addProperty(
10229                                 new AppSearchSchema.DocumentPropertyConfig.Builder(
10230                                         "worksFor", "Organization")
10231                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
10232                                         .setShouldIndexNestedProperties(false)
10233                                         .addIndexableNestedProperties(Collections.singleton("name"))
10234                                         .build())
10235                         .build();
10236         AppSearchSchema organizationSchema =
10237                 new AppSearchSchema.Builder("Organization")
10238                         .addProperty(
10239                                 new StringPropertyConfig.Builder("name")
10240                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
10241                                         .setIndexingType(
10242                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
10243                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
10244                                         .build())
10245                         .addProperty(
10246                                 new StringPropertyConfig.Builder("notes")
10247                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
10248                                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_NONE)
10249                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_NONE)
10250                                         .build())
10251                         .build();
10252 
10253         mDb1.setSchemaAsync(
10254                         new SetSchemaRequest.Builder()
10255                                 .addSchemas(personSchema, organizationSchema)
10256                                 .build())
10257                 .get();
10258 
10259         // Test that Person's nested properties are indexed correctly.
10260         GenericDocument org1 =
10261                 new GenericDocument.Builder<>("namespace", "org1", "Organization")
10262                         .setPropertyString("name", "Org1")
10263                         .setPropertyString("notes", "Some notes")
10264                         .build();
10265         GenericDocument person1 =
10266                 new GenericDocument.Builder<>("namespace", "person1", "Person")
10267                         .setPropertyString("name", "Jane")
10268                         .setPropertyDocument("worksFor", org1)
10269                         .build();
10270 
10271         AppSearchBatchResult<String, Void> putResult =
10272                 checkIsBatchResultSuccess(
10273                         mDb1.putAsync(
10274                                 new PutDocumentsRequest.Builder()
10275                                         .addGenericDocuments(person1, org1)
10276                                         .build()));
10277         assertThat(putResult.getSuccesses()).containsExactly("person1", null, "org1", null);
10278         assertThat(putResult.getFailures()).isEmpty();
10279 
10280         GetByDocumentIdRequest getByDocumentIdRequest =
10281                 new GetByDocumentIdRequest.Builder("namespace").addIds("person1", "org1").build();
10282         List<GenericDocument> outDocuments = doGet(mDb1, getByDocumentIdRequest);
10283         assertThat(outDocuments).hasSize(2);
10284         assertThat(outDocuments).containsExactly(person1, org1);
10285 
10286         // Both org1 and person should be returned for query "Org1"
10287         // For org1 this matches the 'name' property and for person1 this matches the
10288         // 'worksFor.name' property.
10289         SearchResults searchResults =
10290                 mDb1.search(
10291                         "Org1",
10292                         new SearchSpec.Builder()
10293                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
10294                                 .build());
10295         outDocuments = convertSearchResultsToDocuments(searchResults);
10296         assertThat(outDocuments).hasSize(2);
10297         assertThat(outDocuments).containsExactly(person1, org1);
10298 
10299         // No documents should match for "notes", since both 'Organization:notes'
10300         // and 'Person:worksFor.notes' are non-indexable.
10301         searchResults =
10302                 mDb1.search(
10303                         "notes",
10304                         new SearchSpec.Builder()
10305                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
10306                                 .build());
10307         outDocuments = convertSearchResultsToDocuments(searchResults);
10308         assertThat(outDocuments).hasSize(0);
10309     }
10310 
10311     @Test
testSetSchema_indexableNestedPropsList_multipleNestedLevels()10312     public void testSetSchema_indexableNestedPropsList_multipleNestedLevels() throws Exception {
10313         assumeTrue(
10314                 mDb1.getFeatures()
10315                         .isFeatureSupported(Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES));
10316 
10317         AppSearchSchema emailSchema =
10318                 new AppSearchSchema.Builder("Email")
10319                         .addProperty(
10320                                 new StringPropertyConfig.Builder("subject")
10321                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
10322                                         .setIndexingType(
10323                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
10324                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
10325                                         .build())
10326                         .addProperty(
10327                                 new AppSearchSchema.DocumentPropertyConfig.Builder(
10328                                         "sender", "Person")
10329                                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
10330                                         .setShouldIndexNestedProperties(false)
10331                                         .addIndexableNestedProperties(
10332                                                 Arrays.asList(
10333                                                         "name", "worksFor.name", "worksFor.notes"))
10334                                         .build())
10335                         .addProperty(
10336                                 new AppSearchSchema.DocumentPropertyConfig.Builder(
10337                                         "recipient", "Person")
10338                                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
10339                                         .setShouldIndexNestedProperties(true)
10340                                         .build())
10341                         .build();
10342         AppSearchSchema personSchema =
10343                 new AppSearchSchema.Builder("Person")
10344                         .addProperty(
10345                                 new StringPropertyConfig.Builder("name")
10346                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
10347                                         .setIndexingType(
10348                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
10349                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
10350                                         .build())
10351                         .addProperty(
10352                                 new StringPropertyConfig.Builder("age")
10353                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
10354                                         .setIndexingType(
10355                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
10356                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
10357                                         .build())
10358                         .addProperty(
10359                                 new AppSearchSchema.DocumentPropertyConfig.Builder(
10360                                         "worksFor", "Organization")
10361                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
10362                                         .setShouldIndexNestedProperties(false)
10363                                         .addIndexableNestedProperties(Arrays.asList("name", "id"))
10364                                         .build())
10365                         .build();
10366         AppSearchSchema organizationSchema =
10367                 new AppSearchSchema.Builder("Organization")
10368                         .addProperty(
10369                                 new StringPropertyConfig.Builder("name")
10370                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
10371                                         .setIndexingType(
10372                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
10373                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
10374                                         .build())
10375                         .addProperty(
10376                                 new StringPropertyConfig.Builder("notes")
10377                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
10378                                         .setIndexingType(
10379                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
10380                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
10381                                         .build())
10382                         .addProperty(
10383                                 new StringPropertyConfig.Builder("id")
10384                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
10385                                         .setIndexingType(
10386                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
10387                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
10388                                         .build())
10389                         .build();
10390 
10391         mDb1.setSchemaAsync(
10392                         new SetSchemaRequest.Builder()
10393                                 .addSchemas(emailSchema, personSchema, organizationSchema)
10394                                 .build())
10395                 .get();
10396 
10397         // Test that Email and Person's nested properties are indexed correctly.
10398         GenericDocument org1 =
10399                 new GenericDocument.Builder<>("namespace", "org1", "Organization")
10400                         .setPropertyString("name", "Org1")
10401                         .setPropertyString("notes", "Some notes")
10402                         .setPropertyString("id", "1234")
10403                         .build();
10404         GenericDocument person1 =
10405                 new GenericDocument.Builder<>("namespace", "person1", "Person")
10406                         .setPropertyString("name", "Jane")
10407                         .setPropertyString("age", "20")
10408                         .setPropertyDocument("worksFor", org1)
10409                         .build();
10410         GenericDocument person2 =
10411                 new GenericDocument.Builder<>("namespace", "person2", "Person")
10412                         .setPropertyString("name", "John")
10413                         .setPropertyString("age", "30")
10414                         .setPropertyDocument("worksFor", org1)
10415                         .build();
10416         GenericDocument email1 =
10417                 new GenericDocument.Builder<>("namespace", "email1", "Email")
10418                         .setPropertyString("subject", "Greetings!")
10419                         .setPropertyDocument("sender", person1)
10420                         .setPropertyDocument("recipient", person2)
10421                         .build();
10422         AppSearchBatchResult<String, Void> putResult =
10423                 checkIsBatchResultSuccess(
10424                         mDb1.putAsync(
10425                                 new PutDocumentsRequest.Builder()
10426                                         .addGenericDocuments(person1, org1, person2, email1)
10427                                         .build()));
10428         assertThat(putResult.getSuccesses())
10429                 .containsExactly("person1", null, "org1", null, "person2", null, "email1", null);
10430         assertThat(putResult.getFailures()).isEmpty();
10431 
10432         GetByDocumentIdRequest getByDocumentIdRequest =
10433                 new GetByDocumentIdRequest.Builder("namespace")
10434                         .addIds("person1", "org1", "person2", "email1")
10435                         .build();
10436         List<GenericDocument> outDocuments = doGet(mDb1, getByDocumentIdRequest);
10437         assertThat(outDocuments).hasSize(4);
10438         assertThat(outDocuments).containsExactly(person1, org1, person2, email1);
10439 
10440         // Indexed properties:
10441         // Email: 'subject', 'sender.name', 'sender.worksFor.name', 'sender.worksFor.notes',
10442         //        'recipient.name', 'recipient.age', 'recipient.worksFor.name',
10443         //        'recipient.worksFor.id'
10444         //        (Email:recipient sets index_nested_props=true, so it follows the same indexing
10445         //         configs as the next schema-type level (person))
10446         // Person: 'name', 'age', 'worksFor.name', 'worksFor.id'
10447         // Organization: 'name', 'notes', 'id'
10448         //
10449         // All documents should be returned for query 'Org1' because all schemaTypes index the
10450         // 'Organization:name' property.
10451         SearchResults searchResults =
10452                 mDb1.search(
10453                         "Org1",
10454                         new SearchSpec.Builder()
10455                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
10456                                 .build());
10457         outDocuments = convertSearchResultsToDocuments(searchResults);
10458         assertThat(outDocuments).hasSize(4);
10459         assertThat(outDocuments).containsExactly(person1, org1, person2, email1);
10460 
10461         // org1 and email1 should be returned for query 'notes'
10462         searchResults =
10463                 mDb1.search(
10464                         "notes",
10465                         new SearchSpec.Builder()
10466                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
10467                                 .build());
10468         outDocuments = convertSearchResultsToDocuments(searchResults);
10469         assertThat(outDocuments).hasSize(2);
10470         assertThat(outDocuments).containsExactly(org1, email1);
10471 
10472         // all docs should be returned for query "1234"
10473         searchResults =
10474                 mDb1.search(
10475                         "1234",
10476                         new SearchSpec.Builder()
10477                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
10478                                 .build());
10479         outDocuments = convertSearchResultsToDocuments(searchResults);
10480         assertThat(outDocuments).hasSize(4);
10481         assertThat(outDocuments).containsExactly(person1, org1, person2, email1);
10482 
10483         // email1 should be returned for query "30", but not for "20" since sender.age is not
10484         // indexed, but recipient.age is.
10485         // For query "30", person2 should also be returned
10486         // For query "20, person1 should be returned.
10487         searchResults =
10488                 mDb1.search(
10489                         "30",
10490                         new SearchSpec.Builder()
10491                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
10492                                 .build());
10493         outDocuments = convertSearchResultsToDocuments(searchResults);
10494         assertThat(outDocuments).hasSize(2);
10495         assertThat(outDocuments).containsExactly(person2, email1);
10496 
10497         searchResults =
10498                 mDb1.search(
10499                         "20",
10500                         new SearchSpec.Builder()
10501                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
10502                                 .build());
10503         outDocuments = convertSearchResultsToDocuments(searchResults);
10504         assertThat(outDocuments).hasSize(1);
10505         assertThat(outDocuments).containsExactly(person1);
10506     }
10507 
10508     @Test
testSetSchema_indexableNestedPropsList_circularRefs()10509     public void testSetSchema_indexableNestedPropsList_circularRefs() throws Exception {
10510         assumeTrue(
10511                 mDb1.getFeatures()
10512                         .isFeatureSupported(Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES));
10513         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SET_SCHEMA_CIRCULAR_REFERENCES));
10514 
10515         // Create schema with valid cycle: Person -> Organization -> Person...
10516         AppSearchSchema personSchema =
10517                 new AppSearchSchema.Builder("Person")
10518                         .addProperty(
10519                                 new StringPropertyConfig.Builder("name")
10520                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
10521                                         .setIndexingType(
10522                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
10523                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
10524                                         .build())
10525                         .addProperty(
10526                                 new StringPropertyConfig.Builder("address")
10527                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
10528                                         .setIndexingType(
10529                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
10530                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
10531                                         .build())
10532                         .addProperty(
10533                                 new AppSearchSchema.DocumentPropertyConfig.Builder(
10534                                         "worksFor", "Organization")
10535                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
10536                                         .setShouldIndexNestedProperties(false)
10537                                         .addIndexableNestedProperties(
10538                                                 Arrays.asList("name", "notes", "funder.name"))
10539                                         .build())
10540                         .build();
10541         AppSearchSchema organizationSchema =
10542                 new AppSearchSchema.Builder("Organization")
10543                         .addProperty(
10544                                 new StringPropertyConfig.Builder("name")
10545                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
10546                                         .setIndexingType(
10547                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
10548                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
10549                                         .build())
10550                         .addProperty(
10551                                 new StringPropertyConfig.Builder("notes")
10552                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
10553                                         .setIndexingType(
10554                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
10555                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
10556                                         .build())
10557                         .addProperty(
10558                                 new AppSearchSchema.DocumentPropertyConfig.Builder(
10559                                         "funder", "Person")
10560                                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
10561                                         .setShouldIndexNestedProperties(false)
10562                                         .addIndexableNestedProperties(
10563                                                 Arrays.asList(
10564                                                         "name",
10565                                                         "worksFor.name",
10566                                                         "worksFor.funder.address",
10567                                                         "worksFor.funder.worksFor.notes"))
10568                                         .build())
10569                         .build();
10570         mDb1.setSchemaAsync(
10571                         new SetSchemaRequest.Builder()
10572                                 .addSchemas(personSchema, organizationSchema)
10573                                 .build())
10574                 .get();
10575 
10576         // Test that documents following the circular schema are indexed correctly, and that its
10577         // sections are searchable
10578         GenericDocument person1 =
10579                 new GenericDocument.Builder<>("namespace", "person1", "Person")
10580                         .setPropertyString("name", "Person1")
10581                         .setPropertyString("address", "someAddress")
10582                         .build();
10583         GenericDocument org1 =
10584                 new GenericDocument.Builder<>("namespace", "org1", "Organization")
10585                         .setPropertyString("name", "Org1")
10586                         .setPropertyString("notes", "someNote")
10587                         .setPropertyDocument("funder", person1)
10588                         .build();
10589         GenericDocument person2 =
10590                 new GenericDocument.Builder<>("namespace", "person2", "Person")
10591                         .setPropertyString("name", "Person2")
10592                         .setPropertyString("address", "anotherAddress")
10593                         .setPropertyDocument("worksFor", org1)
10594                         .build();
10595         GenericDocument org2 =
10596                 new GenericDocument.Builder<>("namespace", "org2", "Organization")
10597                         .setPropertyString("name", "Org2")
10598                         .setPropertyString("notes", "anotherNote")
10599                         .setPropertyDocument("funder", person2)
10600                         .build();
10601 
10602         AppSearchBatchResult<String, Void> putResult =
10603                 checkIsBatchResultSuccess(
10604                         mDb1.putAsync(
10605                                 new PutDocumentsRequest.Builder()
10606                                         .addGenericDocuments(person1, org1, person2, org2)
10607                                         .build()));
10608         assertThat(putResult.getSuccesses())
10609                 .containsExactly("person1", null, "org1", null, "person2", null, "org2", null);
10610         assertThat(putResult.getFailures()).isEmpty();
10611 
10612         GetByDocumentIdRequest getByDocumentIdRequest =
10613                 new GetByDocumentIdRequest.Builder("namespace")
10614                         .addIds("person1", "person2", "org1", "org2")
10615                         .build();
10616         List<GenericDocument> outDocuments = doGet(mDb1, getByDocumentIdRequest);
10617         assertThat(outDocuments).hasSize(4);
10618         assertThat(outDocuments).containsExactly(person1, person2, org1, org2);
10619 
10620         // Indexed properties:
10621         // Person: 'name', 'address', 'worksFor.name', 'worksFor.notes', 'worksFor.funder.name'
10622         // Organization: 'name', 'notes', 'funder.name', 'funder.worksFor.name',
10623         //               'funder.worksFor.funder.address', 'funder.worksFor.funder.worksFor.notes'
10624         //
10625         // "Person1" should match person1 (name), org1 (funder.name) and person2
10626         // (worksFor.funder.name)
10627         SearchResults searchResults =
10628                 mDb1.search(
10629                         "Person1",
10630                         new SearchSpec.Builder()
10631                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
10632                                 .build());
10633         outDocuments = convertSearchResultsToDocuments(searchResults);
10634         assertThat(outDocuments).hasSize(3);
10635         assertThat(outDocuments).containsExactly(person1, org1, person2);
10636 
10637         // "someAddress" should match person1 (address) and org2 (funder.worksFor.funder.address)
10638         searchResults =
10639                 mDb1.search(
10640                         "someAddress",
10641                         new SearchSpec.Builder()
10642                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
10643                                 .build());
10644         outDocuments = convertSearchResultsToDocuments(searchResults);
10645         assertThat(outDocuments).hasSize(2);
10646         assertThat(outDocuments).containsExactly(person1, org2);
10647 
10648         // "Org1" should match org1 (name), person2 (worksFor.name) and org2 (funder.worksFor.name)
10649         searchResults =
10650                 mDb1.search(
10651                         "Org1",
10652                         new SearchSpec.Builder()
10653                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
10654                                 .build());
10655         outDocuments = convertSearchResultsToDocuments(searchResults);
10656         assertThat(outDocuments).hasSize(3);
10657         assertThat(outDocuments).containsExactly(org1, person2, org2);
10658 
10659         // "someNote" should match org1 (notes) and person2 (worksFor.notes)
10660         searchResults =
10661                 mDb1.search(
10662                         "someNote",
10663                         new SearchSpec.Builder()
10664                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
10665                                 .build());
10666         outDocuments = convertSearchResultsToDocuments(searchResults);
10667         assertThat(outDocuments).hasSize(2);
10668         assertThat(outDocuments).containsExactly(org1, person2);
10669 
10670         // "Person2" should match person2 (name), org2 (funder.name)
10671         searchResults =
10672                 mDb1.search(
10673                         "Person2",
10674                         new SearchSpec.Builder()
10675                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
10676                                 .build());
10677         outDocuments = convertSearchResultsToDocuments(searchResults);
10678         assertThat(outDocuments).hasSize(2);
10679         assertThat(outDocuments).containsExactly(person2, org2);
10680 
10681         // "anotherAddress" should match only person2 (address)
10682         searchResults =
10683                 mDb1.search(
10684                         "anotherAddress",
10685                         new SearchSpec.Builder()
10686                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
10687                                 .build());
10688         outDocuments = convertSearchResultsToDocuments(searchResults);
10689         assertThat(outDocuments).hasSize(1);
10690         assertThat(outDocuments).containsExactly(person2);
10691 
10692         // "Org2" and "anotherNote" should both match only org2 (name, notes)
10693         searchResults =
10694                 mDb1.search(
10695                         "Org2",
10696                         new SearchSpec.Builder()
10697                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
10698                                 .build());
10699         outDocuments = convertSearchResultsToDocuments(searchResults);
10700         assertThat(outDocuments).hasSize(1);
10701         assertThat(outDocuments).containsExactly(org2);
10702 
10703         searchResults =
10704                 mDb1.search(
10705                         "anotherNote",
10706                         new SearchSpec.Builder()
10707                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
10708                                 .build());
10709         outDocuments = convertSearchResultsToDocuments(searchResults);
10710         assertThat(outDocuments).hasSize(1);
10711         assertThat(outDocuments).containsExactly(org2);
10712     }
10713 
10714     @Test
testSetSchema_toString_containsIndexableNestedPropsList()10715     public void testSetSchema_toString_containsIndexableNestedPropsList() throws Exception {
10716         assumeTrue(
10717                 mDb1.getFeatures()
10718                         .isFeatureSupported(Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES));
10719 
10720         AppSearchSchema emailSchema =
10721                 new AppSearchSchema.Builder("Email")
10722                         .addProperty(
10723                                 new StringPropertyConfig.Builder("subject")
10724                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
10725                                         .setIndexingType(
10726                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
10727                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
10728                                         .build())
10729                         .addProperty(
10730                                 new AppSearchSchema.DocumentPropertyConfig.Builder(
10731                                         "sender", "Person")
10732                                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
10733                                         .setShouldIndexNestedProperties(false)
10734                                         .addIndexableNestedProperties(
10735                                                 Arrays.asList(
10736                                                         "name", "worksFor.name", "worksFor.notes"))
10737                                         .build())
10738                         .addProperty(
10739                                 new AppSearchSchema.DocumentPropertyConfig.Builder(
10740                                         "recipient", "Person")
10741                                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
10742                                         .setShouldIndexNestedProperties(true)
10743                                         .build())
10744                         .build();
10745         String expectedIndexableNestedPropertyMessage =
10746                 "indexableNestedProperties: [name, worksFor.notes, worksFor.name]";
10747 
10748         assertThat(emailSchema.toString()).contains(expectedIndexableNestedPropertyMessage);
10749 
10750     }
10751 
10752     @Test
10753     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
testEmbeddingSearch_simple()10754     public void testEmbeddingSearch_simple() throws Exception {
10755         assumeTrue(
10756                 mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG));
10757 
10758         // Schema registration
10759         AppSearchSchema schema = new AppSearchSchema.Builder("Email")
10760                 .addProperty(new StringPropertyConfig.Builder("body")
10761                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
10762                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
10763                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
10764                         .build())
10765                 .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding1")
10766                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
10767                         .setIndexingType(
10768                                 AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
10769                         .build())
10770                 .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding2")
10771                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
10772                         .setIndexingType(
10773                                 AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
10774                         .build())
10775                 .build();
10776         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
10777 
10778         // Index documents
10779         GenericDocument doc0 =
10780                 new GenericDocument.Builder<>("namespace", "id0", "Email")
10781                         .setPropertyString("body", "foo")
10782                         .setCreationTimestampMillis(1000)
10783                         .setPropertyEmbedding("embedding1", new EmbeddingVector(
10784                                 new float[]{0.1f, 0.2f, 0.3f, 0.4f, 0.5f}, "my_model_v1"))
10785                         .setPropertyEmbedding("embedding2", new EmbeddingVector(
10786                                         new float[]{-0.1f, -0.2f, -0.3f, 0.4f, 0.5f},
10787                                         "my_model_v1"),
10788                                 new EmbeddingVector(
10789                                         new float[]{0.6f, 0.7f, 0.8f}, "my_model_v2"))
10790                         .build();
10791         GenericDocument doc1 =
10792                 new GenericDocument.Builder<>("namespace", "id1", "Email")
10793                         .setPropertyString("body", "bar")
10794                         .setCreationTimestampMillis(1000)
10795                         .setPropertyEmbedding("embedding1", new EmbeddingVector(
10796                                 new float[]{-0.1f, 0.2f, -0.3f, -0.4f, 0.5f}, "my_model_v1"))
10797                         .setPropertyEmbedding("embedding2", new EmbeddingVector(
10798                                 new float[]{0.6f, 0.7f, -0.8f}, "my_model_v2"))
10799                         .build();
10800         checkIsBatchResultSuccess(mDb1.putAsync(
10801                 new PutDocumentsRequest.Builder().addGenericDocuments(doc0, doc1).build()));
10802 
10803         // Add an embedding search with dot product semantic scores:
10804         // - document 0: -0.5 (embedding1), 0.3 (embedding2)
10805         // - document 1: -0.9 (embedding1)
10806         EmbeddingVector searchEmbedding = new EmbeddingVector(
10807                 new float[]{1, -1, -1, 1, -1}, "my_model_v1");
10808 
10809         // Match documents that have embeddings with a similarity closer to 0 that is
10810         // greater than -1.
10811         //
10812         // The matched embeddings for each doc are:
10813         // - document 0: -0.5 (embedding1), 0.3 (embedding2)
10814         // - document 1: -0.9 (embedding1)
10815         // The scoring expression for each doc will be evaluated as:
10816         // - document 0: sum({-0.5, 0.3}) + sum({}) = -0.2
10817         // - document 1: sum({-0.9}) + sum({}) = -0.9
10818         SearchSpec searchSpec = new SearchSpec.Builder()
10819                 .setDefaultEmbeddingSearchMetricType(
10820                         SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT)
10821                 .addEmbeddingParameters(searchEmbedding)
10822                 .setRankingStrategy(
10823                         "sum(this.matchedSemanticScores(getEmbeddingParameter(0)))")
10824                 .setListFilterQueryLanguageEnabled(true)
10825                 .build();
10826         SearchResults searchResults = mDb1.search(
10827                 "semanticSearch(getEmbeddingParameter(0), -1, 1)", searchSpec);
10828         List<SearchResult> results = retrieveAllSearchResults(searchResults);
10829         assertThat(results).hasSize(2);
10830         assertThat(results.get(0).getGenericDocument()).isEqualTo(doc0);
10831         assertThat(results.get(0).getRankingSignal()).isWithin(0.00001).of(-0.2);
10832         assertThat(results.get(1).getGenericDocument()).isEqualTo(doc1);
10833         assertThat(results.get(1).getRankingSignal()).isWithin(0.00001).of(-0.9);
10834     }
10835 
10836     @Test
10837     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
testEmbeddingSearch_propertyRestriction()10838     public void testEmbeddingSearch_propertyRestriction() throws Exception {
10839         assumeTrue(
10840                 mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG));
10841 
10842         // Schema registration
10843         AppSearchSchema schema = new AppSearchSchema.Builder("Email")
10844                 .addProperty(new StringPropertyConfig.Builder("body")
10845                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
10846                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
10847                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
10848                         .build())
10849                 .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding1")
10850                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
10851                         .setIndexingType(
10852                                 AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
10853                         .build())
10854                 .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding2")
10855                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
10856                         .setIndexingType(
10857                                 AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
10858                         .build())
10859                 .build();
10860         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
10861 
10862         // Index documents
10863         GenericDocument doc0 =
10864                 new GenericDocument.Builder<>("namespace", "id0", "Email")
10865                         .setPropertyString("body", "foo")
10866                         .setCreationTimestampMillis(1000)
10867                         .setPropertyEmbedding("embedding1", new EmbeddingVector(
10868                                 new float[]{0.1f, 0.2f, 0.3f, 0.4f, 0.5f}, "my_model_v1"))
10869                         .setPropertyEmbedding("embedding2", new EmbeddingVector(
10870                                         new float[]{-0.1f, -0.2f, -0.3f, 0.4f, 0.5f},
10871                                         "my_model_v1"),
10872                                 new EmbeddingVector(
10873                                         new float[]{0.6f, 0.7f, 0.8f}, "my_model_v2"))
10874                         .build();
10875         GenericDocument doc1 =
10876                 new GenericDocument.Builder<>("namespace", "id1", "Email")
10877                         .setPropertyString("body", "bar")
10878                         .setCreationTimestampMillis(1000)
10879                         .setPropertyEmbedding("embedding1", new EmbeddingVector(
10880                                 new float[]{-0.1f, 0.2f, -0.3f, -0.4f, 0.5f}, "my_model_v1"))
10881                         .setPropertyEmbedding("embedding2", new EmbeddingVector(
10882                                 new float[]{0.6f, 0.7f, -0.8f}, "my_model_v2"))
10883                         .build();
10884         checkIsBatchResultSuccess(mDb1.putAsync(
10885                 new PutDocumentsRequest.Builder().addGenericDocuments(doc0, doc1).build()));
10886 
10887         // Add an embedding search with dot product semantic scores:
10888         // - document 0: -0.5 (embedding1), 0.3 (embedding2)
10889         // - document 1: -0.9 (embedding1)
10890         EmbeddingVector searchEmbedding = new EmbeddingVector(
10891                 new float[]{1, -1, -1, 1, -1}, "my_model_v1");
10892 
10893         // Create a query similar as above but with a property restriction, which still matches
10894         // document 0 and document 1 but the semantic score 0.3 should be removed from document 0.
10895         //
10896         // The matched embeddings for each doc are:
10897         // - document 0: -0.5 (embedding1)
10898         // - document 1: -0.9 (embedding1)
10899         // The scoring expression for each doc will be evaluated as:
10900         // - document 0: sum({-0.5}) = -0.5
10901         // - document 1: sum({-0.9}) = -0.9
10902         SearchSpec searchSpec = new SearchSpec.Builder()
10903                 .setDefaultEmbeddingSearchMetricType(
10904                         SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT)
10905                 .addEmbeddingParameters(searchEmbedding)
10906                 .setRankingStrategy("sum(this.matchedSemanticScores(getEmbeddingParameter(0)))")
10907                 .setListFilterQueryLanguageEnabled(true)
10908                 .build();
10909         SearchResults searchResults = mDb1.search(
10910                 "embedding1:semanticSearch(getEmbeddingParameter(0), -1, 1)", searchSpec);
10911         List<SearchResult> results = retrieveAllSearchResults(searchResults);
10912         assertThat(results).hasSize(2);
10913         assertThat(results.get(0).getGenericDocument()).isEqualTo(doc0);
10914         assertThat(results.get(0).getRankingSignal()).isWithin(0.00001).of(-0.5);
10915         assertThat(results.get(1).getGenericDocument()).isEqualTo(doc1);
10916         assertThat(results.get(1).getRankingSignal()).isWithin(0.00001).of(-0.9);
10917     }
10918 
10919     @Test
10920     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
testEmbeddingSearch_multipleSearchEmbeddings()10921     public void testEmbeddingSearch_multipleSearchEmbeddings() throws Exception {
10922         assumeTrue(
10923                 mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG));
10924 
10925         // Schema registration
10926         AppSearchSchema schema = new AppSearchSchema.Builder("Email")
10927                 .addProperty(new StringPropertyConfig.Builder("body")
10928                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
10929                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
10930                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
10931                         .build())
10932                 .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding1")
10933                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
10934                         .setIndexingType(
10935                                 AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
10936                         .build())
10937                 .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding2")
10938                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
10939                         .setIndexingType(
10940                                 AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
10941                         .build())
10942                 .build();
10943         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
10944 
10945         // Index documents
10946         GenericDocument doc0 =
10947                 new GenericDocument.Builder<>("namespace", "id0", "Email")
10948                         .setPropertyString("body", "foo")
10949                         .setCreationTimestampMillis(1000)
10950                         .setPropertyEmbedding("embedding1", new EmbeddingVector(
10951                                 new float[]{0.1f, 0.2f, 0.3f, 0.4f, 0.5f}, "my_model_v1"))
10952                         .setPropertyEmbedding("embedding2", new EmbeddingVector(
10953                                         new float[]{-0.1f, -0.2f, -0.3f, 0.4f, 0.5f},
10954                                         "my_model_v1"),
10955                                 new EmbeddingVector(
10956                                         new float[]{0.6f, 0.7f, 0.8f}, "my_model_v2"))
10957                         .build();
10958         GenericDocument doc1 =
10959                 new GenericDocument.Builder<>("namespace", "id1", "Email")
10960                         .setPropertyString("body", "bar")
10961                         .setCreationTimestampMillis(1000)
10962                         .setPropertyEmbedding("embedding1", new EmbeddingVector(
10963                                 new float[]{-0.1f, 0.2f, -0.3f, -0.4f, 0.5f}, "my_model_v1"))
10964                         .setPropertyEmbedding("embedding2", new EmbeddingVector(
10965                                 new float[]{0.6f, 0.7f, -0.8f}, "my_model_v2"))
10966                         .build();
10967         checkIsBatchResultSuccess(mDb1.putAsync(
10968                 new PutDocumentsRequest.Builder().addGenericDocuments(doc0, doc1).build()));
10969 
10970         // Add an embedding search with dot product semantic scores:
10971         // - document 0: -0.5 (embedding1), 0.3 (embedding2)
10972         // - document 1: -0.9 (embedding1)
10973         EmbeddingVector searchEmbedding1 = new EmbeddingVector(
10974                 new float[]{1, -1, -1, 1, -1}, "my_model_v1");
10975         // Add an embedding search with dot product semantic scores:
10976         // - document 0: -0.5 (embedding2)
10977         // - document 1: -2.1 (embedding2)
10978         EmbeddingVector searchEmbedding2 = new EmbeddingVector(
10979                 new float[]{-1, -1, 1}, "my_model_v2");
10980 
10981         // Create a complex query that matches all hits from all documents.
10982         //
10983         // The matched embeddings for each doc are:
10984         // - document 0: -0.5 (embedding1), 0.3 (embedding2), -0.5 (embedding2)
10985         // - document 1: -0.9 (embedding1), -2.1 (embedding2)
10986         // The scoring expression for each doc will be evaluated as:
10987         // - document 0: sum({-0.5, 0.3}) + sum({-0.5}) = -0.7
10988         // - document 1: sum({-0.9}) + sum({-2.1}) = -3
10989         SearchSpec searchSpec = new SearchSpec.Builder()
10990                 .setDefaultEmbeddingSearchMetricType(
10991                         SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT)
10992                 .addEmbeddingParameters(searchEmbedding1, searchEmbedding2)
10993                 .setRankingStrategy("sum(this.matchedSemanticScores(getEmbeddingParameter(0))) + "
10994                         + "sum(this.matchedSemanticScores(getEmbeddingParameter(1)))")
10995                 .setListFilterQueryLanguageEnabled(true)
10996                 .build();
10997         SearchResults searchResults = mDb1.search(
10998                 "semanticSearch(getEmbeddingParameter(0)) OR "
10999                         + "semanticSearch(getEmbeddingParameter(1))", searchSpec);
11000         List<SearchResult> results = retrieveAllSearchResults(searchResults);
11001         assertThat(results).hasSize(2);
11002         assertThat(results.get(0).getGenericDocument()).isEqualTo(doc0);
11003         assertThat(results.get(0).getRankingSignal()).isWithin(0.00001).of(-0.7);
11004         assertThat(results.get(1).getGenericDocument()).isEqualTo(doc1);
11005         assertThat(results.get(1).getRankingSignal()).isWithin(0.00001).of(-3);
11006     }
11007 
11008     @Test
11009     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
testEmbeddingSearch_hybrid()11010     public void testEmbeddingSearch_hybrid() throws Exception {
11011         assumeTrue(
11012                 mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG));
11013 
11014         // Schema registration
11015         AppSearchSchema schema = new AppSearchSchema.Builder("Email")
11016                 .addProperty(new StringPropertyConfig.Builder("body")
11017                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
11018                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
11019                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
11020                         .build())
11021                 .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding1")
11022                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
11023                         .setIndexingType(
11024                                 AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
11025                         .build())
11026                 .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding2")
11027                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
11028                         .setIndexingType(
11029                                 AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
11030                         .build())
11031                 .build();
11032         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
11033 
11034         // Index documents
11035         GenericDocument doc0 =
11036                 new GenericDocument.Builder<>("namespace", "id0", "Email")
11037                         .setPropertyString("body", "foo")
11038                         .setCreationTimestampMillis(1000)
11039                         .setPropertyEmbedding("embedding1", new EmbeddingVector(
11040                                 new float[]{0.1f, 0.2f, 0.3f, 0.4f, 0.5f}, "my_model_v1"))
11041                         .setPropertyEmbedding("embedding2", new EmbeddingVector(
11042                                         new float[]{-0.1f, -0.2f, -0.3f, 0.4f, 0.5f},
11043                                         "my_model_v1"),
11044                                 new EmbeddingVector(
11045                                         new float[]{0.6f, 0.7f, 0.8f}, "my_model_v2"))
11046                         .build();
11047         GenericDocument doc1 =
11048                 new GenericDocument.Builder<>("namespace", "id1", "Email")
11049                         .setPropertyString("body", "bar")
11050                         .setCreationTimestampMillis(1000)
11051                         .setPropertyEmbedding("embedding1", new EmbeddingVector(
11052                                 new float[]{-0.1f, 0.2f, -0.3f, -0.4f, 0.5f}, "my_model_v1"))
11053                         .setPropertyEmbedding("embedding2", new EmbeddingVector(
11054                                 new float[]{0.6f, 0.7f, -0.8f}, "my_model_v2"))
11055                         .build();
11056         checkIsBatchResultSuccess(mDb1.putAsync(
11057                 new PutDocumentsRequest.Builder().addGenericDocuments(doc0, doc1).build()));
11058 
11059         // Add an embedding search with dot product semantic scores:
11060         // - document 0: -0.5 (embedding2)
11061         // - document 1: -2.1 (embedding2)
11062         EmbeddingVector searchEmbedding = new EmbeddingVector(
11063                 new float[]{-1, -1, 1}, "my_model_v2");
11064 
11065         // Create a hybrid query that matches document 0 because of term-based search
11066         // and document 1 because of embedding-based search.
11067         //
11068         // The matched embeddings for each doc are:
11069         // - document 1: -2.1 (embedding2)
11070         // The scoring expression for each doc will be evaluated as:
11071         // - document 0: sum({}) = 0
11072         // - document 1: sum({-2.1}) = -2.1
11073         SearchSpec searchSpec = new SearchSpec.Builder()
11074                 .setDefaultEmbeddingSearchMetricType(
11075                         SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT)
11076                 .addEmbeddingParameters(searchEmbedding)
11077                 .setRankingStrategy("sum(this.matchedSemanticScores(getEmbeddingParameter(0)))")
11078                 .setListFilterQueryLanguageEnabled(true)
11079                 .build();
11080         SearchResults searchResults = mDb1.search(
11081                 "foo OR semanticSearch(getEmbeddingParameter(0), -10, -1)", searchSpec);
11082         List<SearchResult> results = retrieveAllSearchResults(searchResults);
11083         assertThat(results).hasSize(2);
11084         assertThat(results.get(0).getGenericDocument()).isEqualTo(doc0);
11085         assertThat(results.get(0).getRankingSignal()).isWithin(0.00001).of(0);
11086         assertThat(results.get(1).getGenericDocument()).isEqualTo(doc1);
11087         assertThat(results.get(1).getRankingSignal()).isWithin(0.00001).of(-2.1);
11088     }
11089 
11090     @Test
11091     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
testEmbeddingSearch_notSupported()11092     public void testEmbeddingSearch_notSupported() throws Exception {
11093         assumeTrue(
11094                 mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_QUERY_LANGUAGE));
11095         assumeFalse(
11096                 mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG));
11097 
11098         EmbeddingVector searchEmbedding = new EmbeddingVector(
11099                 new float[]{-1, -1, 1}, "my_model_v2");
11100         SearchSpec searchSpec = new SearchSpec.Builder()
11101                 .setListFilterQueryLanguageEnabled(true)
11102                 .addEmbeddingParameters(searchEmbedding)
11103                 .build();
11104         UnsupportedOperationException exception = assertThrows(
11105                 UnsupportedOperationException.class,
11106                 () -> mDb1.search("semanticSearch(getEmbeddingParameter(0), -1, 1)",
11107                         searchSpec).getNextPageAsync().get());
11108         assertThat(exception).hasMessageThat().contains(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG
11109                 + " is not available on this AppSearch implementation.");
11110     }
11111 
11112     @Test
11113     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SEARCH_SPEC_SEARCH_STRING_PARAMETERS)
testSearchSpecStrings_simple()11114     public void testSearchSpecStrings_simple() throws Exception {
11115         assumeTrue(
11116                 mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_QUERY_LANGUAGE));
11117         assumeTrue(
11118                 mDb1.getFeatures().isFeatureSupported(
11119                         Features.SEARCH_SPEC_SEARCH_STRING_PARAMETERS));
11120 
11121         // Schema registration
11122         AppSearchSchema schema = new AppSearchSchema.Builder("Email")
11123                 .addProperty(new StringPropertyConfig.Builder("body")
11124                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
11125                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
11126                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
11127                         .build())
11128                 .build();
11129         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
11130 
11131         // Index documents
11132         GenericDocument doc0 =
11133                 new GenericDocument.Builder<>("namespace", "id0", "Email")
11134                         .setPropertyString("body", "foo bar")
11135                         .setCreationTimestampMillis(1000)
11136                         .build();
11137         GenericDocument doc1 =
11138                 new GenericDocument.Builder<>("namespace", "id1", "Email")
11139                         .setPropertyString("body", "bar")
11140                         .setCreationTimestampMillis(1000)
11141                         .build();
11142         GenericDocument doc2 =
11143                 new GenericDocument.Builder<>("namespace", "id2", "Email")
11144                         .setPropertyString("body", "foo")
11145                         .setCreationTimestampMillis(1000)
11146                         .build();
11147         checkIsBatchResultSuccess(mDb1.putAsync(
11148                 new PutDocumentsRequest.Builder().addGenericDocuments(doc0, doc1, doc2).build()));
11149 
11150         SearchSpec searchSpec = new SearchSpec.Builder()
11151                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
11152                 .setListFilterQueryLanguageEnabled(true)
11153                 .addSearchStringParameters("foo.")
11154                 .build();
11155         SearchResults searchResults = mDb1.search("getSearchStringParameter(0)", searchSpec);
11156         List<GenericDocument> results = convertSearchResultsToDocuments(searchResults);
11157         assertThat(results).containsExactly(doc2, doc0);
11158 
11159         searchSpec = new SearchSpec.Builder()
11160                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
11161                 .setListFilterQueryLanguageEnabled(true)
11162                 .addSearchStringParameters("bar, foo")
11163                 .build();
11164         searchResults = mDb1.search("getSearchStringParameter(0)", searchSpec);
11165         results = convertSearchResultsToDocuments(searchResults);
11166         assertThat(results).containsExactly(doc0);
11167 
11168         searchSpec = new SearchSpec.Builder()
11169                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
11170                 .setListFilterQueryLanguageEnabled(true)
11171                 .addSearchStringParameters("\\\"bar, \\\"foo\\\"")
11172                 .build();
11173         searchResults = mDb1.search("getSearchStringParameter(0)", searchSpec);
11174         results = convertSearchResultsToDocuments(searchResults);
11175         assertThat(results).containsExactly(doc0);
11176 
11177         searchSpec = new SearchSpec.Builder()
11178                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
11179                 .setListFilterQueryLanguageEnabled(true)
11180                 .addSearchStringParameters("bar ) foo")
11181                 .build();
11182         searchResults = mDb1.search("getSearchStringParameter(0)", searchSpec);
11183         results = convertSearchResultsToDocuments(searchResults);
11184         assertThat(results).containsExactly(doc0);
11185 
11186         searchSpec = new SearchSpec.Builder()
11187                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
11188                 .setListFilterQueryLanguageEnabled(true)
11189                 .addSearchStringParameters("bar foo(")
11190                 .build();
11191         searchResults = mDb1.search("getSearchStringParameter(0)", searchSpec);
11192         results = convertSearchResultsToDocuments(searchResults);
11193         assertThat(results).containsExactly(doc0);
11194     }
11195 
11196     @Test
11197     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SEARCH_SPEC_SEARCH_STRING_PARAMETERS)
testSearchSpecString_notSupported()11198     public void testSearchSpecString_notSupported() throws Exception {
11199         assumeTrue(
11200                 mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_QUERY_LANGUAGE));
11201         assumeFalse(
11202                 mDb1.getFeatures().isFeatureSupported(
11203                         Features.SEARCH_SPEC_SEARCH_STRING_PARAMETERS));
11204 
11205         SearchSpec searchSpec = new SearchSpec.Builder()
11206                 .setListFilterQueryLanguageEnabled(true)
11207                 .addSearchStringParameters("bar foo(")
11208                 .build();
11209         UnsupportedOperationException exception = assertThrows(
11210                 UnsupportedOperationException.class,
11211                 () -> mDb1.search("getSearchStringParameter(0)", searchSpec));
11212         assertThat(exception).hasMessageThat().contains(
11213                 Features.SEARCH_SPEC_SEARCH_STRING_PARAMETERS
11214                 + " is not available on this AppSearch implementation.");
11215     }
11216 
11217 
11218     @Test
11219     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SEARCH_SPEC_SEARCH_STRING_PARAMETERS)
testSuggestionSearchSpecStringParameters_simple()11220     public void testSuggestionSearchSpecStringParameters_simple() throws Exception {
11221         assumeTrue(
11222                 mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_QUERY_LANGUAGE));
11223         assumeTrue(
11224                 mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION));
11225         assumeTrue(
11226                 mDb1.getFeatures().isFeatureSupported(
11227                         Features.SEARCH_SPEC_SEARCH_STRING_PARAMETERS));
11228 
11229         // Schema registration
11230         AppSearchSchema schema = new AppSearchSchema.Builder("Email")
11231                 .addProperty(new StringPropertyConfig.Builder("body")
11232                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
11233                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
11234                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
11235                         .build())
11236                 .build();
11237         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
11238 
11239         // Index documents
11240         GenericDocument doc0 =
11241                 new GenericDocument.Builder<>("namespace", "id0", "Email")
11242                         .setPropertyString("body", "foo bar")
11243                         .setCreationTimestampMillis(1000)
11244                         .build();
11245         checkIsBatchResultSuccess(mDb1.putAsync(
11246                 new PutDocumentsRequest.Builder().addGenericDocuments(doc0).build()));
11247 
11248         // Get a suggestion for 'foo b'. This should be expanded to 'foo bar'. Using search string
11249         // parameters to replace a token other than the last one, should work exactly the same as if
11250         // the parameter were written in the string itself.
11251         SearchSuggestionSpec spec = new SearchSuggestionSpec.Builder(/*maximumResultCount=*/1)
11252                 .addSearchStringParameters("foo")
11253                 .build();
11254         List<SearchSuggestionResult> suggestions =
11255                 mDb1.searchSuggestionAsync("getSearchStringParameter(0) b", spec).get();
11256         assertThat(suggestions).hasSize(1);
11257         assertThat(suggestions.get(0).getSuggestedResult())
11258                 .isEqualTo("getSearchStringParameter(0) bar");
11259     }
11260 
11261     @Test
11262     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SEARCH_SPEC_SEARCH_STRING_PARAMETERS)
testSearchSuggestionSpecStringParameters_notSupported()11263     public void testSearchSuggestionSpecStringParameters_notSupported() throws Exception {
11264         assumeTrue(
11265                 mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION));
11266         assumeFalse(
11267                 mDb1.getFeatures().isFeatureSupported(
11268                         Features.SEARCH_SPEC_SEARCH_STRING_PARAMETERS));
11269 
11270         SearchSuggestionSpec spec = new SearchSuggestionSpec.Builder(/*maximumResultCount=*/1)
11271                 .addSearchStringParameters("foo")
11272                 .build();
11273         UnsupportedOperationException exception = assertThrows(
11274                 UnsupportedOperationException.class,
11275                         () -> mDb1.searchSuggestionAsync(
11276                                 "getSearchStringParameter(0) b", spec).get());
11277         assertThat(exception).hasMessageThat().contains(
11278                 Features.SEARCH_SPEC_SEARCH_STRING_PARAMETERS
11279                         + " is not available on this AppSearch implementation.");
11280     }
11281 
11282     @Test
11283     @RequiresFlagsEnabled({
11284             Flags.FLAG_ENABLE_INFORMATIONAL_RANKING_EXPRESSIONS,
11285             Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG})
testInformationalRankingExpressions()11286     public void testInformationalRankingExpressions() throws Exception {
11287         assumeTrue(
11288                 mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG));
11289         assumeTrue(mDb1.getFeatures().isFeatureSupported(
11290                 Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS));
11291 
11292         // Schema registration
11293         AppSearchSchema schema = new AppSearchSchema.Builder("Email")
11294                 .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding")
11295                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
11296                         .setIndexingType(
11297                                 AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
11298                         .build())
11299                 .build();
11300         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
11301 
11302         // Index documents
11303         final int doc0DocScore = 2;
11304         GenericDocument doc0 =
11305                 new GenericDocument.Builder<>("namespace", "id0", "Email")
11306                         .setScore(doc0DocScore)
11307                         .setCreationTimestampMillis(1000)
11308                         .setPropertyEmbedding("embedding", new EmbeddingVector(
11309                                 new float[]{-0.1f, -0.2f, -0.3f, -0.4f, -0.5f}, "my_model"))
11310                         .build();
11311         final int doc1DocScore = 3;
11312         GenericDocument doc1 =
11313                 new GenericDocument.Builder<>("namespace", "id1", "Email")
11314                         .setScore(doc1DocScore)
11315                         .setCreationTimestampMillis(1000)
11316                         .setPropertyEmbedding("embedding", new EmbeddingVector(
11317                                         new float[]{-0.1f, 0.2f, -0.3f, -0.4f, 0.5f}, "my_model"),
11318                                 new EmbeddingVector(
11319                                         new float[]{-0.1f, -0.2f, -0.3f, -0.4f, -0.5f}, "my_model"))
11320                         .build();
11321         checkIsBatchResultSuccess(mDb1.putAsync(
11322                 new PutDocumentsRequest.Builder().addGenericDocuments(doc0, doc1).build()));
11323 
11324         // Add an embedding search with dot product semantic scores:
11325         // - document 0: 0.5
11326         // - document 1: -0.9, 0.5
11327         EmbeddingVector searchEmbedding = new EmbeddingVector(
11328                 new float[]{1, -1, -1, 1, -1}, "my_model");
11329 
11330         // Make an embedding query that matches all documents.
11331         SearchSpec searchSpec = new SearchSpec.Builder()
11332                 .setDefaultEmbeddingSearchMetricType(
11333                         SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT)
11334                 .addEmbeddingParameters(searchEmbedding)
11335                 .setRankingStrategy(
11336                         "sum(this.matchedSemanticScores(getEmbeddingParameter(0)))")
11337                 .addInformationalRankingExpressions(
11338                         "len(this.matchedSemanticScores(getEmbeddingParameter(0)))")
11339                 .addInformationalRankingExpressions("this.documentScore()")
11340                 .setListFilterQueryLanguageEnabled(true)
11341                 .build();
11342         SearchResults searchResults = mDb1.search(
11343                 "semanticSearch(getEmbeddingParameter(0))", searchSpec);
11344         List<SearchResult> results = retrieveAllSearchResults(searchResults);
11345         assertThat(results).hasSize(2);
11346         // doc0:
11347         assertThat(results.get(0).getGenericDocument()).isEqualTo(doc0);
11348         assertThat(results.get(0).getRankingSignal()).isWithin(0.00001).of(0.5);
11349         // doc0 has 1 embedding vector and a document score of 2.
11350         assertThat(results.get(0).getInformationalRankingSignals())
11351                 .containsExactly(1.0, (double) doc0DocScore).inOrder();
11352 
11353         // doc1:
11354         assertThat(results.get(1).getGenericDocument()).isEqualTo(doc1);
11355         assertThat(results.get(1).getRankingSignal()).isWithin(0.00001).of(-0.9 + 0.5);
11356         // doc1 has 2 embedding vectors and a document score of 3.
11357         assertThat(results.get(1).getInformationalRankingSignals())
11358                 .containsExactly(2.0, (double) doc1DocScore).inOrder();
11359     }
11360 
11361     @Test
11362     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_INFORMATIONAL_RANKING_EXPRESSIONS)
testInformationalRankingExpressions_notSupported()11363     public void testInformationalRankingExpressions_notSupported() throws Exception {
11364         assumeTrue(mDb1.getFeatures().isFeatureSupported(
11365                 Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION));
11366         assumeFalse(mDb1.getFeatures().isFeatureSupported(
11367                 Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS));
11368 
11369         SearchSpec searchSpec = new SearchSpec.Builder()
11370                 .setRankingStrategy("this.documentScore() + 1")
11371                 .addInformationalRankingExpressions("this.documentScore()")
11372                 .build();
11373         UnsupportedOperationException exception = assertThrows(
11374                 UnsupportedOperationException.class,
11375                 () -> mDb1.search("foo", searchSpec));
11376         assertThat(exception).hasMessageThat().contains(
11377                 Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS
11378                 + " are not available on this AppSearch implementation.");
11379     }
11380 
11381     @Test
testPutDocuments_emptyBytesAndDocuments()11382     public void testPutDocuments_emptyBytesAndDocuments() throws Exception {
11383         // Schema registration
11384         AppSearchSchema schema = new AppSearchSchema.Builder("testSchema")
11385                 .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder("bytes")
11386                         .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
11387                         .build())
11388                 .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(
11389                         "document", AppSearchEmail.SCHEMA_TYPE)
11390                         .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
11391                         .setShouldIndexNestedProperties(true)
11392                         .build())
11393                 .build();
11394         mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
11395                 .addSchemas(schema, AppSearchEmail.SCHEMA).build()).get();
11396 
11397         // Index a document
11398         GenericDocument document = new GenericDocument.Builder<>("namespace", "id1", "testSchema")
11399                 .setPropertyBytes("bytes")
11400                 .setPropertyDocument("document")
11401                 .build();
11402 
11403         AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb1.putAsync(
11404                 new PutDocumentsRequest.Builder().addGenericDocuments(document).build()));
11405         assertThat(result.getSuccesses()).containsExactly("id1", null);
11406         assertThat(result.getFailures()).isEmpty();
11407 
11408         GetByDocumentIdRequest request = new GetByDocumentIdRequest.Builder("namespace")
11409                 .addIds("id1")
11410                 .build();
11411         List<GenericDocument> outDocuments = doGet(mDb1, request);
11412         assertThat(outDocuments).hasSize(1);
11413         GenericDocument outDocument = outDocuments.get(0);
11414         assertThat(outDocument.getPropertyBytesArray("bytes")).isEmpty();
11415         assertThat(outDocument.getPropertyDocumentArray("document")).isEmpty();
11416     }
11417 
11418     @Test
11419     @RequiresFlagsEnabled({Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG,
11420             Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_QUANTIZATION})
testEmbeddingQuantization()11421     public void testEmbeddingQuantization() throws Exception {
11422         assumeTrue(
11423                 mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG));
11424         assumeTrue(
11425                 mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_QUANTIZATION));
11426 
11427         // Schema registration
11428         AppSearchSchema schema = new AppSearchSchema.Builder("Email")
11429                 .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding")
11430                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
11431                         .setIndexingType(
11432                                 AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
11433                         .setQuantizationType(
11434                                 AppSearchSchema.EmbeddingPropertyConfig.QUANTIZATION_TYPE_8_BIT)
11435                         .build())
11436                 .build();
11437         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
11438 
11439         // Index a document
11440         GenericDocument doc =
11441                 new GenericDocument.Builder<>("namespace", "id", "Email")
11442                         .setCreationTimestampMillis(1000)
11443                         // Since quantization is enabled, this vector will be quantized to
11444                         // {0, 1, 255}.
11445                         .setPropertyEmbedding("embedding", new EmbeddingVector(
11446                                 new float[]{0, 1.45f, 255}, "my_model"))
11447                         .build();
11448         checkIsBatchResultSuccess(mDb1.putAsync(
11449                 new PutDocumentsRequest.Builder().addGenericDocuments(doc).build()));
11450 
11451         // Verify the embedding will be quantized, so that the embedding score would be
11452         // 0 + 1 + 255 = 256, instead of 0 + 1.45 + 255 = 256.45.
11453         SearchSpec searchSpec = new SearchSpec.Builder()
11454                 .setDefaultEmbeddingSearchMetricType(
11455                         SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT)
11456                 .addEmbeddingParameters(new EmbeddingVector(new float[]{1, 1, 1}, "my_model"))
11457                 .setRankingStrategy(
11458                         "sum(this.matchedSemanticScores(getEmbeddingParameter(0)))")
11459                 .setListFilterQueryLanguageEnabled(true)
11460                 .build();
11461         SearchResults searchResults = mDb1.search(
11462                 "semanticSearch(getEmbeddingParameter(0), -1000, 1000)", searchSpec);
11463         List<SearchResult> results = retrieveAllSearchResults(searchResults);
11464         assertThat(results).hasSize(1);
11465         assertThat(results.get(0).getGenericDocument()).isEqualTo(doc);
11466         assertThat(results.get(0).getRankingSignal())
11467                 .isWithin(0.0001)
11468                 .of(256);
11469     }
11470     @Test
11471     @RequiresFlagsEnabled({Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG,
11472             Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_QUANTIZATION})
testEmbeddingQuantization_changeSchema()11473     public void testEmbeddingQuantization_changeSchema() throws Exception {
11474         assumeTrue(
11475                 mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG));
11476         assumeTrue(
11477                 mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_QUANTIZATION));
11478 
11479         // Set Schema with an embedding property for QUANTIZATION_TYPE_NONE
11480         AppSearchSchema schema = new AppSearchSchema.Builder("Email")
11481                 .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding")
11482                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
11483                         .setIndexingType(
11484                                 AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
11485                         .setQuantizationType(
11486                                 AppSearchSchema.EmbeddingPropertyConfig.QUANTIZATION_TYPE_NONE)
11487                         .build())
11488                 .build();
11489         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
11490 
11491         // Index a document
11492         GenericDocument doc =
11493                 new GenericDocument.Builder<>("namespace", "id", "Email")
11494                         .setCreationTimestampMillis(1000)
11495                         // Since quantization is enabled, this vector will be quantized to
11496                         // {0, 1, 255}.
11497                         .setPropertyEmbedding("embedding", new EmbeddingVector(
11498                                 new float[]{0, 1.45f, 255}, "my_model"))
11499                         .build();
11500         checkIsBatchResultSuccess(mDb1.putAsync(
11501                 new PutDocumentsRequest.Builder().addGenericDocuments(doc).build()));
11502 
11503         // Update the embedding property to QUANTIZATION_TYPE_8_BIT
11504         schema = new AppSearchSchema.Builder("Email")
11505                 .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding")
11506                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
11507                         .setIndexingType(
11508                                 AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
11509                         .setQuantizationType(
11510                                 AppSearchSchema.EmbeddingPropertyConfig.QUANTIZATION_TYPE_8_BIT)
11511                         .build())
11512                 .build();
11513         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
11514 
11515         // Verify the embedding will be quantized, so that the embedding score would be
11516         // 0 + 1 + 255 = 256, instead of 0 + 1.45 + 255 = 256.45.
11517         SearchSpec searchSpec = new SearchSpec.Builder()
11518                 .setDefaultEmbeddingSearchMetricType(
11519                         SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT)
11520                 .addEmbeddingParameters(new EmbeddingVector(new float[]{1, 1, 1}, "my_model"))
11521                 .setRankingStrategy(
11522                         "sum(this.matchedSemanticScores(getEmbeddingParameter(0)))")
11523                 .setListFilterQueryLanguageEnabled(true)
11524                 .build();
11525         SearchResults searchResults = mDb1.search(
11526                 "semanticSearch(getEmbeddingParameter(0), -1000, 1000)", searchSpec);
11527         List<SearchResult> results = retrieveAllSearchResults(searchResults);
11528         assertThat(results).hasSize(1);
11529         assertThat(results.get(0).getGenericDocument()).isEqualTo(doc);
11530         assertThat(results.get(0).getRankingSignal())
11531                 .isWithin(0.0001)
11532                 .of(256);
11533 
11534         // Update the embedding property back to QUANTIZATION_TYPE_NONE
11535         schema = new AppSearchSchema.Builder("Email")
11536                 .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding")
11537                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
11538                         .setIndexingType(
11539                                 AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
11540                         .setQuantizationType(
11541                                 AppSearchSchema.EmbeddingPropertyConfig.QUANTIZATION_TYPE_NONE)
11542                         .build())
11543                 .build();
11544         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
11545 
11546         // Verify the embedding will not be quantized, so that the embedding score would be
11547         // 0 + 1.45 + 255 = 256.45.
11548         searchResults = mDb1.search(
11549                 "semanticSearch(getEmbeddingParameter(0), -1000, 1000)", searchSpec);
11550         results = retrieveAllSearchResults(searchResults);
11551         assertThat(results).hasSize(1);
11552         assertThat(results.get(0).getGenericDocument()).isEqualTo(doc);
11553         assertThat(results.get(0).getRankingSignal())
11554                 .isWithin(0.0001)
11555                 .of(256.45);
11556     }
11557 
11558     @Test
11559     @RequiresFlagsEnabled({Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG,
11560             Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_QUANTIZATION})
testEmbeddingQuantization_notSupported()11561     public void testEmbeddingQuantization_notSupported() throws Exception {
11562         assumeTrue(
11563                 mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_QUERY_LANGUAGE));
11564         assumeTrue(
11565                 mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG));
11566         assumeFalse(
11567                 mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_QUANTIZATION));
11568 
11569         AppSearchSchema schema = new AppSearchSchema.Builder("Email")
11570                 .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding")
11571                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
11572                         .setIndexingType(
11573                                 AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
11574                         .setQuantizationType(
11575                                 AppSearchSchema.EmbeddingPropertyConfig
11576                                         .QUANTIZATION_TYPE_8_BIT)
11577                         .build())
11578                 .build();
11579 
11580         UnsupportedOperationException e =
11581                 assertThrows(
11582                         UnsupportedOperationException.class,
11583                         () -> mDb1.setSchemaAsync(
11584                                 new SetSchemaRequest.Builder()
11585                                         .addSchemas(schema).build()).get());
11586         assertThat(e)
11587                 .hasMessageThat()
11588                 .contains(
11589                         Features.SCHEMA_EMBEDDING_QUANTIZATION
11590                                 + " is not available on this AppSearch implementation.");
11591     }
11592 
11593     @Test
11594     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
testRankingFunction_maxMinOrDefault()11595     public void testRankingFunction_maxMinOrDefault() throws Exception {
11596         assumeTrue(
11597                 mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG));
11598         assumeTrue(mDb1.getFeatures().isFeatureSupported(
11599                 Features.SEARCH_SPEC_RANKING_FUNCTION_MAX_MIN_OR_DEFAULT));
11600 
11601         // Schema registration
11602         AppSearchSchema schema = new AppSearchSchema.Builder("Email")
11603                 .addProperty(new StringPropertyConfig.Builder("body")
11604                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
11605                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
11606                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
11607                         .build())
11608                 .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding")
11609                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
11610                         .setIndexingType(
11611                                 AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
11612                         .build())
11613                 .build();
11614         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
11615 
11616         // Index documents
11617         GenericDocument doc0 =
11618                 new GenericDocument.Builder<>("namespace", "id0", "Email")
11619                         .setPropertyString("body", "foo")
11620                         .setCreationTimestampMillis(1000)
11621                         .setPropertyEmbedding("embedding",
11622                                 new EmbeddingVector(
11623                                         new float[]{0.6f, 0.7f, 0.8f}, "my_model"))
11624                         .build();
11625         GenericDocument doc1 =
11626                 new GenericDocument.Builder<>("namespace", "id1", "Email")
11627                         .setPropertyString("body", "bar")
11628                         .setCreationTimestampMillis(1000)
11629                         .setPropertyEmbedding("embedding", new EmbeddingVector(
11630                                         new float[]{0.6f, 0.7f, -0.8f}, "my_model"),
11631                                 new EmbeddingVector(
11632                                         new float[]{0.2f, 0.1f, -1.2f}, "my_model"))
11633                         .build();
11634         checkIsBatchResultSuccess(mDb1.putAsync(
11635                 new PutDocumentsRequest.Builder().addGenericDocuments(doc0, doc1).build()));
11636 
11637         // Add an embedding search with dot product semantic scores:
11638         // - document 0: -0.5
11639         // - document 1: -2.1, -1.5
11640         EmbeddingVector searchEmbedding = new EmbeddingVector(
11641                 new float[]{-1, -1, 1}, "my_model");
11642 
11643         // Create a hybrid query that matches document 0 because of term-based search
11644         // and document 1 because of embedding-based search.
11645         //
11646         // The scoring expression for each doc will be evaluated as:
11647         // - document 0: maxOrDefault({}, -100) = -100
11648         // - document 1: maxOrDefault({-2.1, -1.5}, -100) = -1.5
11649         SearchSpec searchSpec1 = new SearchSpec.Builder()
11650                 .setDefaultEmbeddingSearchMetricType(
11651                         SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT)
11652                 .addEmbeddingParameters(searchEmbedding)
11653                 .setRankingStrategy(
11654                         "maxOrDefault(this.matchedSemanticScores(getEmbeddingParameter(0)), -100)")
11655                 .setListFilterQueryLanguageEnabled(true)
11656                 .build();
11657         SearchResults searchResults = mDb1.search(
11658                 "foo OR semanticSearch(getEmbeddingParameter(0), -10, -1)", searchSpec1);
11659         List<SearchResult> results = retrieveAllSearchResults(searchResults);
11660         assertThat(results).hasSize(2);
11661         assertThat(results.get(0).getGenericDocument()).isEqualTo(doc1);
11662         assertThat(results.get(0).getRankingSignal()).isWithin(0.00001).of(-1.5);
11663         assertThat(results.get(1).getGenericDocument()).isEqualTo(doc0);
11664         assertThat(results.get(1).getRankingSignal()).isWithin(0.00001).of(-100);
11665 
11666         // Create the same query with minOrDefault.
11667         //
11668         // The scoring expression for each doc will be evaluated as:
11669         // - document 0: minOrDefault({}, -100) = -100
11670         // - document 1: minOrDefault({-2.1, -1.5}, -100) = -2.1
11671         SearchSpec searchSpec2 = new SearchSpec.Builder()
11672                 .setDefaultEmbeddingSearchMetricType(
11673                         SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT)
11674                 .addEmbeddingParameters(searchEmbedding)
11675                 .setRankingStrategy(
11676                         "minOrDefault(this.matchedSemanticScores(getEmbeddingParameter(0)), -100)")
11677                 .setListFilterQueryLanguageEnabled(true)
11678                 .build();
11679         searchResults = mDb1.search(
11680                 "foo OR semanticSearch(getEmbeddingParameter(0), -10, -1)", searchSpec2);
11681         results = retrieveAllSearchResults(searchResults);
11682         assertThat(results).hasSize(2);
11683         assertThat(results.get(0).getGenericDocument()).isEqualTo(doc1);
11684         assertThat(results.get(0).getRankingSignal()).isWithin(0.00001).of(-2.1);
11685         assertThat(results.get(1).getGenericDocument()).isEqualTo(doc0);
11686         assertThat(results.get(1).getRankingSignal()).isWithin(0.00001).of(-100);
11687     }
11688 
11689     @Test
testRankingFunction_maxMinOrDefault_notSupported()11690     public void testRankingFunction_maxMinOrDefault_notSupported()
11691             throws Exception {
11692         assumeTrue(mDb1.getFeatures().isFeatureSupported(
11693                 Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION));
11694         assumeFalse(mDb1.getFeatures().isFeatureSupported(
11695                 Features.SEARCH_SPEC_RANKING_FUNCTION_MAX_MIN_OR_DEFAULT));
11696 
11697         // Schema registration
11698         mDb1.setSchemaAsync(
11699                 new SetSchemaRequest.Builder()
11700                         .addSchemas(AppSearchEmail.SCHEMA)
11701                         .build()).get();
11702 
11703         // Index a document
11704         AppSearchEmail inEmail =
11705                 new AppSearchEmail.Builder("namespace", "id1")
11706                         .setFrom("from@example.com")
11707                         .setTo("to1@example.com", "to2@example.com")
11708                         .setSubject("testPut example")
11709                         .setBody("This is the body of the testPut email")
11710                         .build();
11711         checkIsBatchResultSuccess(mDb1.putAsync(
11712                 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
11713 
11714         // Query for the document with the unsupported ranking function maxOrDefault.
11715         SearchResults searchResults1 = mDb1.search("foo",
11716                 new SearchSpec.Builder()
11717                         .setRankingStrategy("maxOrDefault()")
11718                         .build());
11719         ExecutionException executionException = assertThrows(ExecutionException.class,
11720                 () -> searchResults1.getNextPageAsync().get());
11721         assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
11722         AppSearchException exception = (AppSearchException) executionException.getCause();
11723         assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT);
11724         assertThat(exception).hasMessageThat().contains("Unknown function: maxOrDefault");
11725 
11726         // Query for the document with the unsupported ranking function minOrDefault.
11727         SearchResults searchResults2 = mDb1.search("foo",
11728                 new SearchSpec.Builder()
11729                         .setRankingStrategy("minOrDefault()")
11730                         .build());
11731         executionException = assertThrows(ExecutionException.class,
11732                 () -> searchResults2.getNextPageAsync().get());
11733         assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
11734         exception = (AppSearchException) executionException.getCause();
11735         assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT);
11736         assertThat(exception).hasMessageThat().contains("Unknown function: minOrDefault");
11737     }
11738 
11739     @Test
11740     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
testRankingFunction_filterByRange()11741     public void testRankingFunction_filterByRange() throws Exception {
11742         assumeTrue(
11743                 mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG));
11744         assumeTrue(mDb1.getFeatures().isFeatureSupported(
11745                 Features.SEARCH_SPEC_RANKING_FUNCTION_FILTER_BY_RANGE));
11746 
11747         // Schema registration
11748         AppSearchSchema schema = new AppSearchSchema.Builder("Email")
11749                 .addProperty(new StringPropertyConfig.Builder("body")
11750                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
11751                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
11752                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
11753                         .build())
11754                 .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding")
11755                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
11756                         .setIndexingType(
11757                                 AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
11758                         .build())
11759                 .build();
11760         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
11761 
11762         // Index documents
11763         GenericDocument doc =
11764                 new GenericDocument.Builder<>("namespace", "id", "Email")
11765                         .setPropertyString("body", "bar")
11766                         .setCreationTimestampMillis(1000)
11767                         .setPropertyEmbedding("embedding", new EmbeddingVector(
11768                                         new float[]{0.6f, 0.7f, -0.8f}, "my_model"),
11769                                 new EmbeddingVector(
11770                                         new float[]{0.2f, 0.1f, -1.2f}, "my_model"),
11771                                 new EmbeddingVector(
11772                                         new float[]{0.f, 0.f, 0.1f}, "my_model"))
11773                         .build();
11774         checkIsBatchResultSuccess(mDb1.putAsync(
11775                 new PutDocumentsRequest.Builder().addGenericDocuments(doc).build()));
11776 
11777         // Add an embedding search with dot product semantic scores: -2.1, -1.5, 0.1
11778         EmbeddingVector searchEmbedding = new EmbeddingVector(
11779                 new float[]{-1, -1, 1}, "my_model");
11780 
11781         // Create a query with a ranking signal as the sum of all matched semantic scores within
11782         // [-2, 1], which will be evaluated as -1.5 + 0.1 = -1.4.
11783         SearchSpec searchSpec = new SearchSpec.Builder()
11784                 .setDefaultEmbeddingSearchMetricType(
11785                         SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT)
11786                 .addEmbeddingParameters(searchEmbedding)
11787                 .setRankingStrategy(
11788                         "sum(filterByRange(this.matchedSemanticScores("
11789                                 + "getEmbeddingParameter(0)), -2, 1))")
11790                 .setListFilterQueryLanguageEnabled(true)
11791                 .build();
11792         SearchResults searchResults = mDb1.search(
11793                 "semanticSearch(getEmbeddingParameter(0))", searchSpec);
11794         List<SearchResult> results = retrieveAllSearchResults(searchResults);
11795         assertThat(results).hasSize(1);
11796         assertThat(results.get(0).getGenericDocument()).isEqualTo(doc);
11797         assertThat(results.get(0).getRankingSignal()).isWithin(0.00001).of(-1.5 + 0.1);
11798     }
11799 
11800     @Test
testRankingFunction_filterByRange_notSupported()11801     public void testRankingFunction_filterByRange_notSupported()
11802             throws Exception {
11803         assumeTrue(mDb1.getFeatures().isFeatureSupported(
11804                 Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION));
11805         assumeFalse(mDb1.getFeatures().isFeatureSupported(
11806                 Features.SEARCH_SPEC_RANKING_FUNCTION_FILTER_BY_RANGE));
11807 
11808         // Schema registration
11809         mDb1.setSchemaAsync(
11810                 new SetSchemaRequest.Builder()
11811                         .addSchemas(AppSearchEmail.SCHEMA)
11812                         .build()).get();
11813 
11814         // Index a document
11815         AppSearchEmail inEmail =
11816                 new AppSearchEmail.Builder("namespace", "id1")
11817                         .setFrom("from@example.com")
11818                         .setTo("to1@example.com", "to2@example.com")
11819                         .setSubject("testPut example")
11820                         .setBody("This is the body of the testPut email")
11821                         .build();
11822         checkIsBatchResultSuccess(mDb1.putAsync(
11823                 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
11824 
11825         // Query for the document with the unsupported ranking function filterByRange.
11826         SearchResults searchResults1 = mDb1.search("foo",
11827                 new SearchSpec.Builder()
11828                         .setRankingStrategy("filterByRange()")
11829                         .build());
11830         ExecutionException executionException = assertThrows(ExecutionException.class,
11831                 () -> searchResults1.getNextPageAsync().get());
11832         assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
11833         AppSearchException exception = (AppSearchException) executionException.getCause();
11834         assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT);
11835         assertThat(exception).hasMessageThat().contains("Unknown function: filterByRange");
11836     }
11837 
11838     @Test
11839     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCORABLE_PROPERTY)
testRankWithScorableProperty_getScorablePropertyFunction_notSupported()11840     public void testRankWithScorableProperty_getScorablePropertyFunction_notSupported()
11841             throws Exception {
11842         assumeTrue(mDb1.getFeatures().isFeatureSupported(
11843                 Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION));
11844         assumeFalse(mDb1.getFeatures().isFeatureSupported(
11845                 Features.SCHEMA_SCORABLE_PROPERTY_CONFIG));
11846 
11847         UnsupportedOperationException exception = assertThrows(
11848                 UnsupportedOperationException.class,
11849                 () -> mDb1.search("body",
11850                         new SearchSpec.Builder()
11851                                 .setScorablePropertyRankingEnabled(true)
11852                                 .setRankingStrategy(
11853                                         "sum(getScorableProperty(\"Gmail\", \"invalid\"))")
11854                                 .build()));
11855         assertThat(exception).hasMessageThat().contains(
11856                 Features.SCHEMA_SCORABLE_PROPERTY_CONFIG
11857                         + " is not available on this AppSearch implementation.");
11858     }
11859 
11860     @Test
11861     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCORABLE_PROPERTY)
testRankWithScorableProperty_setScoringEnabledInSchema_notSupported()11862     public void testRankWithScorableProperty_setScoringEnabledInSchema_notSupported()
11863             throws Exception {
11864         assumeTrue(mDb1.getFeatures().isFeatureSupported(
11865                 Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION));
11866         assumeFalse(mDb1.getFeatures().isFeatureSupported(
11867                 Features.SCHEMA_SCORABLE_PROPERTY_CONFIG));
11868         AppSearchSchema schema = new AppSearchSchema.Builder("Gmail")
11869                 .addProperty(new BooleanPropertyConfig.Builder("important")
11870                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
11871                         .setScoringEnabled(true)
11872                         .build())
11873                 .build();
11874         UnsupportedOperationException exception = assertThrows(
11875                 UnsupportedOperationException.class,
11876                 () -> mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
11877                         .addSchemas(schema).build()).get()
11878         );
11879         assertThat(exception).hasMessageThat().contains(
11880                 Features.SCHEMA_SCORABLE_PROPERTY_CONFIG
11881                         + " is not available on this AppSearch implementation.");
11882     }
11883 
11884     @Test
11885     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCORABLE_PROPERTY)
testRankWithScorableProperty_simple()11886     public void testRankWithScorableProperty_simple() throws Exception {
11887         assumeTrue(mDb1.getFeatures().isFeatureSupported(
11888                 Features.SCHEMA_SCORABLE_PROPERTY_CONFIG));
11889         AppSearchSchema schema = new AppSearchSchema.Builder("Gmail")
11890                 .addProperty(new StringPropertyConfig.Builder("subject")
11891                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
11892                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
11893                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
11894                         .build())
11895                 .addProperty(new BooleanPropertyConfig.Builder("important")
11896                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
11897                         .setScoringEnabled(true)
11898                         .build())
11899                 .build();
11900         mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
11901                 .addSchemas(schema).build()).get();
11902 
11903         GenericDocument doc1 =
11904                 new GenericDocument.Builder<>("namespace", "id1", "Gmail")
11905                         .setPropertyString("subject", "bar")
11906                         .setPropertyBoolean("important", true)
11907                         .setScore(1)
11908                         .build();
11909         int rankingScoreOfDoc1 = 2;
11910         GenericDocument doc2 =
11911                 new GenericDocument.Builder<>("namespace", "id2", "Gmail")
11912                         .setPropertyString("subject", "bar 2")
11913                         .setPropertyBoolean("important", true)
11914                         .setScore(2)
11915                         .build();
11916         int rankingScoreOfDoc2 = 3;
11917         checkIsBatchResultSuccess(mDb1.putAsync(
11918                 new PutDocumentsRequest.Builder().addGenericDocuments(doc1, doc2).build()));
11919 
11920         SearchSpec searchSpec = new SearchSpec.Builder()
11921                 .setScorablePropertyRankingEnabled(true)
11922                 .setRankingStrategy(
11923                         "this.documentScore() + sum(getScorableProperty(\"Gmail\", \"important\"))")
11924                 .build();
11925 
11926         SearchResults searchResults =
11927                 mDb1.search("", searchSpec);
11928         List<SearchResult> results = retrieveAllSearchResults(searchResults);
11929         assertThat(results).hasSize(2);
11930         assertThat(results.get(0).getGenericDocument()).isEqualTo(doc2);
11931         assertThat(results.get(0).getRankingSignal())
11932                 .isWithin(0.00001).of(rankingScoreOfDoc2);
11933         assertThat(results.get(1).getGenericDocument()).isEqualTo(doc1);
11934         assertThat(results.get(1).getRankingSignal())
11935                 .isWithin(0.00001).of(rankingScoreOfDoc1);
11936     }
11937 
11938     @Test
11939     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCORABLE_PROPERTY)
testRankWithScorableProperty_withNestedSchema()11940     public void testRankWithScorableProperty_withNestedSchema() throws Exception {
11941         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_SCORABLE_PROPERTY_CONFIG));
11942         AppSearchSchema personSchema = new AppSearchSchema.Builder("Person")
11943                 .addProperty(new DoublePropertyConfig.Builder("income")
11944                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
11945                         .setScoringEnabled(true)
11946                         .build())
11947                 .addProperty(new LongPropertyConfig.Builder("age")
11948                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
11949                         .setScoringEnabled(true)
11950                         .build())
11951                 .addProperty(new BooleanPropertyConfig.Builder("isStarred")
11952                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
11953                         .setScoringEnabled(true)
11954                         .build())
11955                 .build();
11956         AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email")
11957                 .addProperty(new DocumentPropertyConfig
11958                         .Builder("sender", "Person")
11959                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
11960                         .build())
11961                 .addProperty(new DocumentPropertyConfig
11962                         .Builder("recipient", "Person")
11963                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
11964                         .build())
11965                 .addProperty(new LongPropertyConfig.Builder("viewTimes")
11966                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
11967                         .setScoringEnabled(true)
11968                         .build())
11969                 .build();
11970         mDb1.setSchemaAsync(
11971                 new SetSchemaRequest.Builder()
11972                         .addSchemas(personSchema, emailSchema)
11973                         .build()).get();
11974 
11975         GenericDocument sender1 = new GenericDocument.Builder<>("namespace", "person1", "Person")
11976                 .setPropertyBoolean("isStarred", true)
11977                 .setPropertyDouble("income", 1000, 2000)
11978                 .setPropertyLong("age", 30)
11979                 .build();
11980         GenericDocument sender2 = new GenericDocument.Builder<>("namespace", "person2", "Person")
11981                 .setPropertyBoolean("isStarred", true)
11982                 .setPropertyDouble("income", 5000, 3000)
11983                 .setPropertyLong("age", 40)
11984                 .build();
11985         GenericDocument recipient = new GenericDocument.Builder<>("namespace", "person2", "Person")
11986                 .setPropertyBoolean("isStarred", true)
11987                 .setPropertyDouble("income", 2000, 3000)
11988                 .setPropertyLong("age", 50)
11989                 .build();
11990 
11991         GenericDocument email =
11992                 new GenericDocument.Builder<>("namespace", "email1", "Email")
11993                         .setPropertyDocument(
11994                                 "sender", sender1, sender2)
11995                         .setPropertyDocument(
11996                                 "recipient", recipient)
11997                         .setPropertyLong("viewTimes", 10)
11998                         .build();
11999 
12000         // Put the email document to AppSearch and verify its success.
12001         AppSearchBatchResult<String, Void> result =
12002                 checkIsBatchResultSuccess(
12003                         mDb1.putAsync(
12004                                 new PutDocumentsRequest.Builder()
12005                                         .addGenericDocuments(email)
12006                                         .build()));
12007         assertThat(result.getSuccesses()).containsExactly("email1", null);
12008         assertThat(result.getFailures()).isEmpty();
12009 
12010         // Search and ranking with the scorable properties
12011         String rankingStrategy =
12012                 "sum(getScorableProperty(\"Email\", \"viewTimes\")) + " +
12013                         "max(getScorableProperty(\"Email\", \"recipient.age\")) + " +
12014                         "100 * max(getScorableProperty(\"Email\", \"recipient.isStarred\")) + " +
12015                         "5 * sum(getScorableProperty(\"Email\", \"sender.income\"))";
12016         SearchSpec searchSpec = new SearchSpec.Builder()
12017                 .setScorablePropertyRankingEnabled(true)
12018                 .setRankingStrategy(rankingStrategy)
12019                 .build();
12020         double expectedScore = /*viewTimes=*/10 + /*age=*/50 + 100 * /*isStarred=*/1 +
12021                 5 * (1000 + 2000 + 5000 + 3000);
12022         SearchResults searchResults = mDb1.search("", searchSpec);
12023         List<SearchResult> results = retrieveAllSearchResults(searchResults);
12024         assertThat(results).hasSize(1);
12025         assertThat(results.get(0).getGenericDocument()).isEqualTo(email);
12026         assertThat(results.get(0).getRankingSignal())
12027                 .isWithin(0.00001).of(expectedScore);
12028     }
12029 
12030     @Test
12031     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCORABLE_PROPERTY)
testRankWithScorableProperty_updateSchemaByAddScorableProperty()12032     public void testRankWithScorableProperty_updateSchemaByAddScorableProperty() throws Exception {
12033         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_SCORABLE_PROPERTY_CONFIG));
12034 
12035         AppSearchSchema schema = new AppSearchSchema.Builder("Gmail")
12036                 .addProperty(new StringPropertyConfig.Builder("subject")
12037                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
12038                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
12039                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
12040                         .build())
12041                 .build();
12042         GenericDocument gmailDoc =
12043                 new GenericDocument.Builder<>("namespace", "id", "Gmail")
12044                         .setPropertyString("subject", "foo")
12045                         .build();
12046         mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
12047                 .addSchemas(schema).build()).get();
12048 
12049         // Put the email document to AppSearch and verify its success.
12050         AppSearchBatchResult<String, Void> result =
12051                 checkIsBatchResultSuccess(
12052                         mDb1.putAsync(
12053                                 new PutDocumentsRequest.Builder()
12054                                         .addGenericDocuments(gmailDoc)
12055                                         .build()));
12056         assertThat(result.getSuccesses()).containsExactly("id", null);
12057         assertThat(result.getFailures()).isEmpty();
12058 
12059         // Update the schema by adding a scorable property.
12060         schema = new AppSearchSchema.Builder("Gmail")
12061                 .addProperty(new StringPropertyConfig.Builder("subject")
12062                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
12063                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
12064                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
12065                         .build())
12066                 .addProperty(new BooleanPropertyConfig.Builder("important")
12067                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
12068                         .setScoringEnabled(true)
12069                         .build())
12070                 .build();
12071         mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
12072                 .addSchemas(schema).build()).get();
12073 
12074         // Search and rank over the existing doc.
12075         // The existing document's scorable property has been populated with the default values.
12076         SearchSpec searchSpec = new SearchSpec.Builder()
12077                 .setScorablePropertyRankingEnabled(true)
12078                 .setRankingStrategy("sum(getScorableProperty(\"Gmail\", \"important\"))")
12079                 .build();
12080         SearchResults searchResults = mDb1.search("", searchSpec);
12081         List<SearchResult> results = retrieveAllSearchResults(searchResults);
12082         assertThat(results).hasSize(1);
12083         assertThat(results.get(0).getRankingSignal())
12084                 .isWithin(0.00001).of(0);
12085     }
12086 
12087     @Test
12088     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCORABLE_PROPERTY)
testRankWithScorableProperty_updateScorableTypeInNestedSchema()12089     public void testRankWithScorableProperty_updateScorableTypeInNestedSchema() throws Exception {
12090         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_SCORABLE_PROPERTY_CONFIG));
12091 
12092         AppSearchSchema personSchema = new AppSearchSchema.Builder("Person")
12093                 .addProperty(new DoublePropertyConfig.Builder("income")
12094                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
12095                         .setScoringEnabled(false)
12096                         .build())
12097                 .build();
12098         AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email")
12099                 .addProperty(new DocumentPropertyConfig
12100                         .Builder("sender", "Person")
12101                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
12102                         .build())
12103                 .addProperty(new DocumentPropertyConfig
12104                         .Builder("recipient", "Person")
12105                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
12106                         .build())
12107                 .build();
12108         mDb1.setSchemaAsync(
12109                 new SetSchemaRequest.Builder()
12110                         .addSchemas(personSchema, emailSchema)
12111                         .build()).get();
12112 
12113         GenericDocument sender = new GenericDocument
12114                 .Builder<>("namespace", "person1", "Person")
12115                 .setPropertyDouble("income", 1000)
12116                 .build();
12117         GenericDocument recipient = new GenericDocument
12118                 .Builder<>("namespace", "person2", "Person")
12119                 .setPropertyDouble("income", 5000)
12120                 .build();
12121         GenericDocument email =
12122                 new GenericDocument.Builder<>("namespace", "email1", "Email")
12123                         .setPropertyDocument(
12124                                 "sender", sender)
12125                         .setPropertyDocument(
12126                                 "recipient", recipient)
12127                         .build();
12128         checkIsBatchResultSuccess(
12129                 mDb1.putAsync(
12130                         new PutDocumentsRequest.Builder().addGenericDocuments(email).build()));
12131 
12132         // Update the 'Person' schema by setting Person.income as scorable.
12133         // It would trigger the re-generation of the scorable property cache for the
12134         // the schema 'Email', as it is a parent schema of 'Person'.
12135         personSchema = new AppSearchSchema.Builder("Person")
12136                 .addProperty(new DoublePropertyConfig.Builder("income")
12137                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
12138                         .setScoringEnabled(true)
12139                         .build())
12140                 .build();
12141         mDb1.setSchemaAsync(
12142                 new SetSchemaRequest.Builder()
12143                         .addSchemas(personSchema, emailSchema)
12144                         .build()).get();
12145 
12146         // Search and ranking with the Email.Person.income
12147         String rankingStrategy =
12148                 "sum(getScorableProperty(\"Email\", \"sender.income\")) + " +
12149                         "max(getScorableProperty(\"Email\", \"recipient.income\"))";
12150         SearchSpec searchSpec = new SearchSpec.Builder()
12151                 .setScorablePropertyRankingEnabled(true)
12152                 .setRankingStrategy(rankingStrategy)
12153                 .build();
12154         double expectedScore = 1000 + 5000;
12155         SearchResults searchResults = mDb1.search("", searchSpec);
12156         List<SearchResult> results = retrieveAllSearchResults(searchResults);
12157         assertThat(results).hasSize(1);
12158         assertThat(results.get(0).getGenericDocument()).isEqualTo(email);
12159         assertThat(results.get(0).getRankingSignal())
12160                 .isWithin(0.00001).of(expectedScore);
12161     }
12162 
12163     @Test
12164     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCORABLE_PROPERTY)
testRankWithScorableProperty_updateSchemaByFlippingScorableType()12165     public void testRankWithScorableProperty_updateSchemaByFlippingScorableType() throws Exception {
12166         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_SCORABLE_PROPERTY_CONFIG));
12167 
12168         AppSearchSchema schemaWithPropertyScorable = new AppSearchSchema.Builder("Gmail")
12169                 .addProperty(new BooleanPropertyConfig.Builder("important")
12170                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
12171                         .setScoringEnabled(true)
12172                         .build())
12173                 .build();
12174         GenericDocument doc =
12175                 new GenericDocument.Builder<>("namespace", "id", "Gmail")
12176                         .setPropertyBoolean("important", true)
12177                         .build();
12178         mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
12179                 .addSchemas(schemaWithPropertyScorable).build()).get();
12180 
12181         checkIsBatchResultSuccess(mDb1.putAsync(
12182                 new PutDocumentsRequest.Builder().addGenericDocuments(doc).build()));
12183         SearchSpec searchSpec = new SearchSpec.Builder()
12184                 .setScorablePropertyRankingEnabled(true)
12185                 .setRankingStrategy("sum(getScorableProperty(\"Gmail\", \"important\"))")
12186                 .build();
12187         SearchResults searchResults =
12188                 mDb1.search("", searchSpec);
12189         List<SearchResult> results = retrieveAllSearchResults(searchResults);
12190         assertThat(results).hasSize(1);
12191         assertThat(results.get(0).getGenericDocument()).isEqualTo(doc);
12192         assertThat(results.get(0).getRankingSignal())
12193                 .isWithin(0.00001).of(1);
12194 
12195         // Update the Schema by updating the property as not scorable
12196         AppSearchSchema schemaWithPropertyNotScorable = new AppSearchSchema
12197                 .Builder("Gmail")
12198                 .addProperty(new BooleanPropertyConfig.Builder("important")
12199                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
12200                         .setScoringEnabled(false)
12201                         .build())
12202                 .build();
12203         mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
12204                 .addSchemas(schemaWithPropertyNotScorable).build()).get();
12205 
12206         // Update the schema by updating the property to scorable again.
12207         mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
12208                 .addSchemas(schemaWithPropertyScorable).build()).get();
12209         // Verify that the property can be used for scoring.
12210         searchResults = mDb1.search("", searchSpec);
12211         results = retrieveAllSearchResults(searchResults);
12212         assertThat(results).hasSize(1);
12213         assertThat(results.get(0).getGenericDocument()).isEqualTo(doc);
12214         assertThat(results.get(0).getRankingSignal())
12215                 .isWithin(0.00001).of(1);
12216     }
12217 
12218     @Test
12219     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCORABLE_PROPERTY)
testRankWithScorableProperty_reorderSchemaProperties()12220     public void testRankWithScorableProperty_reorderSchemaProperties() throws Exception {
12221         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_SCORABLE_PROPERTY_CONFIG));
12222 
12223         AppSearchSchema personSchema = new AppSearchSchema.Builder("Person")
12224                 .addProperty(new DoublePropertyConfig.Builder("income")
12225                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
12226                         .setScoringEnabled(true)
12227                         .build())
12228                 .addProperty(new LongPropertyConfig.Builder("age")
12229                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
12230                         .setScoringEnabled(true)
12231                         .build())
12232                 .build();
12233         GenericDocument person = new GenericDocument
12234                 .Builder<>("namespace", "person1", "Person")
12235                 .setPropertyDouble("income", 1000, 2000)
12236                 .setPropertyLong("age", 30)
12237                 .build();
12238         mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
12239                 .addSchemas(personSchema).build()).get();
12240         checkIsBatchResultSuccess(mDb1.putAsync(
12241                 new PutDocumentsRequest.Builder().addGenericDocuments(person).build()));
12242         String rankingStrategy =
12243                 "sum(getScorableProperty(\"Person\", \"income\")) + " +
12244                         "sum(getScorableProperty(\"Person\", \"age\"))";
12245         SearchSpec searchSpec = new SearchSpec.Builder()
12246                 .setScorablePropertyRankingEnabled(true)
12247                 .setRankingStrategy(rankingStrategy)
12248                 .build();
12249         double expectedRankingScore = 3030;
12250 
12251         SearchResults searchResults = mDb1.search("", searchSpec);
12252         List<SearchResult> results = retrieveAllSearchResults(searchResults);
12253         assertThat(results.get(0).getRankingSignal())
12254                 .isWithin(0.00001).of(expectedRankingScore);
12255 
12256         // Update the schema by swapping the order of property 'age' and 'income'
12257         personSchema = new AppSearchSchema.Builder("Person")
12258                 .addProperty(new LongPropertyConfig.Builder("age")
12259                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
12260                         .setScoringEnabled(true)
12261                         .build())
12262                 .addProperty(new DoublePropertyConfig.Builder("income")
12263                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
12264                         .setScoringEnabled(true)
12265                         .build())
12266                 .build();
12267         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(personSchema).build()).get();
12268 
12269         // Verify that the ranking is still working as expected.
12270         searchResults = mDb1.search("", searchSpec);
12271         results = retrieveAllSearchResults(searchResults);
12272         assertThat(results.get(0).getRankingSignal())
12273                 .isWithin(0.00001).of(expectedRankingScore);
12274     }
12275 
12276     @Test
12277     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCORABLE_PROPERTY)
testRankWithScorableProperty_matchedDocumentHasDifferentSchemaType()12278     public void testRankWithScorableProperty_matchedDocumentHasDifferentSchemaType()
12279             throws Exception {
12280         assumeTrue(mDb1.getFeatures().isFeatureSupported(
12281                 Features.SCHEMA_SCORABLE_PROPERTY_CONFIG));
12282         AppSearchSchema gmailSchema = new AppSearchSchema.Builder("Gmail")
12283                 .addProperty(new BooleanPropertyConfig.Builder("important")
12284                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
12285                         .setScoringEnabled(true)
12286                         .build())
12287                 .build();
12288         AppSearchSchema personSchema = new AppSearchSchema.Builder("Person")
12289                 .addProperty(new LongPropertyConfig.Builder("income")
12290                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
12291                         .setScoringEnabled(true)
12292                         .build())
12293                 .build();
12294         mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
12295                 .addSchemas(gmailSchema, personSchema).build()).get();
12296 
12297         GenericDocument gmailDoc =
12298                 new GenericDocument.Builder<>("namespace", "id1", "Gmail")
12299                         .setPropertyBoolean("important", true)
12300                         .setScore(1)
12301                         .build();
12302         GenericDocument personDoc =
12303                 new GenericDocument.Builder<>("namespace", "id2", "Person")
12304                         .setPropertyLong ("income", 100)
12305                         .setScore(1)
12306                         .build();
12307         checkIsBatchResultSuccess(mDb1.putAsync(
12308                 new PutDocumentsRequest.Builder().
12309                         addGenericDocuments(gmailDoc, personDoc).build()));
12310 
12311         SearchSpec searchSpec = new SearchSpec.Builder()
12312                 .setScorablePropertyRankingEnabled(true)
12313                 .setRankingStrategy(
12314                         "this.documentScore() + sum(getScorableProperty(\"Gmail\", \"important\"))")
12315                 .build();
12316         double expectedGmailDocScore = 2;
12317         double expectedPersonDocScore = 1;
12318 
12319         SearchResults searchResults = mDb1.search("", searchSpec);
12320         List<SearchResult> results = retrieveAllSearchResults(searchResults);
12321         assertThat(results).hasSize(2);
12322         assertThat(results.get(0).getGenericDocument()).isEqualTo(gmailDoc);
12323         assertThat(results.get(0).getRankingSignal())
12324                 .isWithin(0.00001).of(expectedGmailDocScore);
12325         assertThat(results.get(1).getGenericDocument()).isEqualTo(personDoc);
12326         assertThat(results.get(1).getRankingSignal())
12327                 .isWithin(0.00001).of(expectedPersonDocScore);
12328     }
12329 
12330     @Test
12331     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCORABLE_PROPERTY)
testRankWithScorableProperty_matchedDocumentHasNoDataUnderTheRankingProperty()12332     public void testRankWithScorableProperty_matchedDocumentHasNoDataUnderTheRankingProperty()
12333             throws Exception {
12334         assumeTrue(mDb1.getFeatures().isFeatureSupported(
12335                 Features.SCHEMA_SCORABLE_PROPERTY_CONFIG));
12336         AppSearchSchema gmailSchema = new AppSearchSchema.Builder("Gmail")
12337                 .addProperty(new LongPropertyConfig.Builder("viewTimes")
12338                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
12339                         .setScoringEnabled(true)
12340                         .build())
12341                 .build();
12342         mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
12343                 .addSchemas(gmailSchema).build()).get();
12344 
12345         GenericDocument gmailDoc1 =
12346                 new GenericDocument.Builder<>("namespace", "id1", "Gmail")
12347                         .setPropertyLong("viewTimes", 100)
12348                         .setScore(1)
12349                         .build();
12350         GenericDocument gmailDoc2 =
12351                 new GenericDocument.Builder<>("namespace", "id2", "Gmail")
12352                         .setScore(1)
12353                         .build();
12354         checkIsBatchResultSuccess(mDb1.putAsync(
12355                 new PutDocumentsRequest.Builder().
12356                         addGenericDocuments(gmailDoc1, gmailDoc2).build()));
12357 
12358         SearchSpec searchSpec = new SearchSpec.Builder()
12359                 .setScorablePropertyRankingEnabled(true)
12360                 .setRankingStrategy(
12361                         "this.documentScore() + sum(getScorableProperty(\"Gmail\", \"viewTimes\"))")
12362                 .build();
12363         double expectedGmailDoc1Score = 101;
12364         double expectedGmailDoc2Score = 1;
12365 
12366         SearchResults searchResults = mDb1.search("", searchSpec);
12367         List<SearchResult> results = retrieveAllSearchResults(searchResults);
12368         assertThat(results).hasSize(2);
12369         assertThat(results.get(0).getGenericDocument()).isEqualTo(gmailDoc1);
12370         assertThat(results.get(0).getRankingSignal())
12371                 .isWithin(0.00001).of(expectedGmailDoc1Score);
12372         assertThat(results.get(1).getGenericDocument()).isEqualTo(gmailDoc2);
12373         assertThat(results.get(1).getRankingSignal())
12374                 .isWithin(0.00001).of(expectedGmailDoc2Score);
12375     }
12376 
12377     @Test
12378     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCORABLE_PROPERTY)
testRankWithScorableProperty_joinWithChildQuery()12379     public void testRankWithScorableProperty_joinWithChildQuery() throws Exception {
12380         assumeTrue(mDb1.getFeatures().isFeatureSupported(
12381                 Features.SCHEMA_SCORABLE_PROPERTY_CONFIG));
12382         assumeTrue(mDb1.getFeatures().isFeatureSupported(
12383                 Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION));
12384         assumeTrue(mDb1.getFeatures()
12385                 .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
12386 
12387         AppSearchSchema personSchema = new AppSearchSchema.Builder("Person")
12388                 .addProperty(new StringPropertyConfig.Builder("name")
12389                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
12390                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
12391                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
12392                         .build())
12393                 .addProperty(new DoublePropertyConfig.Builder("income")
12394                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
12395                         .setScoringEnabled(true)
12396                         .build())
12397                 .addProperty(new BooleanPropertyConfig.Builder("isStarred")
12398                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
12399                         .setScoringEnabled(true)
12400                         .build())
12401                 .build();
12402         AppSearchSchema callLogSchema = new AppSearchSchema.Builder("CallLog")
12403                 .addProperty(new StringPropertyConfig.Builder("personQualifiedId")
12404                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
12405                         .setJoinableValueType(StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID)
12406                         .build())
12407                 .addProperty(new DoublePropertyConfig.Builder("rfsScore")
12408                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
12409                         .setScoringEnabled(true)
12410                         .build())
12411                 .build();
12412         AppSearchSchema smsLogSchema = new AppSearchSchema.Builder("SmsLog")
12413                 .addProperty(new StringPropertyConfig.Builder("personQualifiedId")
12414                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
12415                         .setJoinableValueType(StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID)
12416                         .build())
12417                 .addProperty(new DoublePropertyConfig.Builder("rfsScore")
12418                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
12419                         .setScoringEnabled(true)
12420                         .build())
12421                 .build();
12422         mDb1.setSchemaAsync(
12423                 new SetSchemaRequest.Builder()
12424                         .addSchemas(personSchema, callLogSchema, smsLogSchema).build()).get();
12425 
12426         // John will have two CallLog docs to join and one sms log to join.
12427         GenericDocument personJohn = new GenericDocument.Builder<>(
12428                 "namespace", "johnId", "Person")
12429                 .setPropertyString("name", "John")
12430                 .setPropertyBoolean("isStarred", true)
12431                 .setPropertyDouble("income", 30)
12432                 .setScore(10)
12433                 .build();
12434         // Kevin will have two CallLog docs to join and one sms log to join.
12435         GenericDocument personKevin = new GenericDocument.Builder<>(
12436                 "namespace", "kevinId", "Person")
12437                 .setPropertyString("name", "Kevin")
12438                 .setPropertyBoolean("isStarred", false)
12439                 .setPropertyDouble("income", 40)
12440                 .setScore(20)
12441                 .build();
12442         // Tim has no CallLog or SmsLog to join.
12443         GenericDocument personTim = new GenericDocument.Builder<>("namespace", "timId", "Person")
12444                 .setPropertyString("name", "Tim")
12445                 .setPropertyDouble("income", 60)
12446                 .setPropertyBoolean("isStarred", true)
12447                 .setScore(50)
12448                 .build();
12449 
12450         GenericDocument johnCallLog1 =
12451                 new GenericDocument.Builder<>(
12452                         "namespace", "johnCallLog1", "CallLog")
12453                         .setScore(5)
12454                         .setPropertyDouble("rfsScore", 100, 200)
12455                         .setPropertyString("personQualifiedId",
12456                                 DocumentIdUtil.createQualifiedId(mContext.getPackageName(),
12457                                         DB_NAME_1, personJohn))
12458                         .build();
12459         GenericDocument johnCallLog2 =
12460                 new GenericDocument.Builder<>(
12461                         "namespace", "johnCallLog2", "CallLog")
12462                         .setScore(5)
12463                         .setPropertyDouble("rfsScore", 300, 500)
12464                         .setPropertyString("personQualifiedId",
12465                                 DocumentIdUtil.createQualifiedId(mContext.getPackageName(),
12466                                         DB_NAME_1, personJohn))
12467                         .build();
12468         GenericDocument kevinCallLog1 =
12469                 new GenericDocument.Builder<>(
12470                         "namespace", "kevinCallLog1", "CallLog")
12471                         .setScore(5)
12472                         .setPropertyDouble("rfsScore", 300, 400)
12473                         .setPropertyString("personQualifiedId",
12474                                 DocumentIdUtil.createQualifiedId(mContext.getPackageName(),
12475                                         DB_NAME_1, personKevin))
12476                         .build();
12477         GenericDocument kevinCallLog2 =
12478                 new GenericDocument.Builder<>(
12479                         "namespace", "kevinCallLog2", "CallLog")
12480                         .setScore(5)
12481                         .setPropertyDouble("rfsScore", 500, 800)
12482                         .setPropertyString("personQualifiedId",
12483                                 DocumentIdUtil.createQualifiedId(mContext.getPackageName(),
12484                                         DB_NAME_1, personKevin))
12485                         .build();
12486         GenericDocument johnSmsLog1 =
12487                 new GenericDocument.Builder<>(
12488                         "namespace", "johnSmsLog1", "SmsLog")
12489                         .setScore(5)
12490                         .setPropertyDouble("rfsScore", 1000, 2000)
12491                         .setPropertyString("personQualifiedId",
12492                                 DocumentIdUtil.createQualifiedId(mContext.getPackageName(),
12493                                         DB_NAME_1, personJohn))
12494                         .build();
12495         GenericDocument kevinSmsLog1 =
12496                 new GenericDocument.Builder<>(
12497                         "namespace", "kevinSmsLog1", "SmsLog")
12498                         .setScore(5)
12499                         .setPropertyDouble("rfsScore",  2000, 3000)
12500                         .setPropertyString("personQualifiedId",
12501                                 DocumentIdUtil.createQualifiedId(mContext.getPackageName(),
12502                                         DB_NAME_1, personKevin))
12503                         .build();
12504 
12505         // Put all documents to AppSearch and verify its success.
12506         AppSearchBatchResult<String, Void> result =
12507                 checkIsBatchResultSuccess(
12508                         mDb1.putAsync(
12509                                 new PutDocumentsRequest.Builder()
12510                                         .addGenericDocuments(
12511                                                 personTim, personJohn, personKevin,
12512                                                 kevinCallLog1, kevinCallLog2, kevinSmsLog1,
12513                                                 johnCallLog1, johnCallLog2, johnSmsLog1)
12514                                         .build()));
12515         assertThat(result.getSuccesses().size()).isEqualTo(9);
12516         assertThat(result.getFailures()).isEmpty();
12517 
12518         String childRankingStrategy =
12519                 "sum(getScorableProperty(\"CallLog\", \"rfsScore\")) + " +
12520                         "sum(getScorableProperty(\"SmsLog\", \"rfsScore\"))";
12521         double johnChildDocScore = 100 + 200 + 300 + 500 + 1000 + 2000;
12522         double kevinChildDocScore = 300 + 400 + 500 + 800 + 2000 + 3000;
12523         double timChildDocScore = 0;
12524 
12525         SearchSpec childSearchSpec = new SearchSpec.Builder()
12526                 .setScorablePropertyRankingEnabled(true)
12527                 .setRankingStrategy(childRankingStrategy)
12528                 .build();
12529         JoinSpec js = new JoinSpec.Builder("personQualifiedId")
12530                 .setNestedSearch("", childSearchSpec)
12531                 .build();
12532         String parentRankingStrategy =
12533                         "sum(getScorableProperty(\"Person\", \"income\")) + " +
12534                         "20 * sum(getScorableProperty(\"Person\", \"isStarred\")) + " +
12535                         "sum(this.childrenRankingSignals())";
12536         SearchSpec parentSearchSpec = new SearchSpec.Builder()
12537                 .setScorablePropertyRankingEnabled(true)
12538                 .setJoinSpec(js)
12539                 .setRankingStrategy(parentRankingStrategy)
12540                 .addFilterSchemas("Person")
12541                 .build();
12542         double johnExpectScore = /*income=*/30 + 20 * /*isStarred=*/1 + johnChildDocScore;
12543         double kevinExpectScore = /*income=*/40 + 20 * /*isStarred=*/0 + kevinChildDocScore;
12544         double timExpectScore = /*income=*/60 + 20 * /*isStarred=*/1 + timChildDocScore;
12545 
12546         SearchResults searchResults =
12547                 mDb1.search("", parentSearchSpec);
12548         List<SearchResult> results = retrieveAllSearchResults(searchResults);
12549         assertThat(results).hasSize(3);
12550         assertThat(results.get(0).getRankingSignal())
12551                 .isWithin(0.00001).of(kevinExpectScore);
12552         assertThat(results.get(1).getRankingSignal())
12553                 .isWithin(0.00001).of(johnExpectScore);
12554         assertThat(results.get(2).getRankingSignal())
12555                 .isWithin(0.00001).of(timExpectScore);
12556     }
12557 
12558     @Test
12559     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SEARCH_RESULT_PARENT_TYPES)
testQuery_typeFilterWithPolymorphism()12560     public void testQuery_typeFilterWithPolymorphism() throws Exception {
12561         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
12562         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_RESULT_PARENT_TYPES));
12563 
12564         // Schema registration
12565         AppSearchSchema personSchema =
12566                 new AppSearchSchema.Builder("Person")
12567                         .addProperty(
12568                                 new StringPropertyConfig.Builder("name")
12569                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
12570                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
12571                                         .setIndexingType(
12572                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
12573                                         .build())
12574                         .build();
12575         AppSearchSchema artistSchema =
12576                 new AppSearchSchema.Builder("Artist")
12577                         .addParentType("Person")
12578                         .addProperty(
12579                                 new StringPropertyConfig.Builder("name")
12580                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
12581                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
12582                                         .setIndexingType(
12583                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
12584                                         .build())
12585                         .build();
12586         mDb1.setSchemaAsync(
12587                         new SetSchemaRequest.Builder()
12588                                 .addSchemas(personSchema)
12589                                 .addSchemas(artistSchema)
12590                                 .addSchemas(AppSearchEmail.SCHEMA)
12591                                 .build())
12592                 .get();
12593 
12594         // Index some documents
12595         GenericDocument personDoc =
12596                 new GenericDocument.Builder<>("namespace", "id1", "Person")
12597                         .setPropertyString("name", "Foo")
12598                         .build();
12599         GenericDocument artistDoc =
12600                 new GenericDocument.Builder<>("namespace", "id2", "Artist")
12601                         .setPropertyString("name", "Foo")
12602                         .build();
12603         AppSearchEmail emailDoc =
12604                 new AppSearchEmail.Builder("namespace", "id3")
12605                         .setFrom("from@example.com")
12606                         .setTo("to1@example.com", "to2@example.com")
12607                         .setSubject("testPut example")
12608                         .setBody("Foo")
12609                         .build();
12610         checkIsBatchResultSuccess(
12611                 mDb1.putAsync(
12612                         new PutDocumentsRequest.Builder()
12613                                 .addGenericDocuments(personDoc, artistDoc, emailDoc)
12614                                 .build()));
12615 
12616         // Query for the documents
12617         SearchResults searchResults =
12618                 mDb1.search(
12619                         "Foo",
12620                         new SearchSpec.Builder()
12621                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
12622                                 .build());
12623         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
12624         assertThat(documents).hasSize(3);
12625         assertThat(documents).containsExactly(personDoc, artistDoc, emailDoc);
12626 
12627         // Query with a filter for the "Person" type should also include the "Artist" type.
12628         searchResults =
12629                 mDb1.search(
12630                         "Foo",
12631                         new SearchSpec.Builder()
12632                                 .addFilterSchemas("Person")
12633                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
12634                                 .build());
12635         documents = convertSearchResultsToDocuments(searchResults);
12636         assertThat(documents).hasSize(2);
12637         assertThat(documents).containsExactly(personDoc, artistDoc);
12638 
12639         // Query with a filters for the "Artist" type should not include the "Person" type.
12640         searchResults =
12641                 mDb1.search(
12642                         "Foo",
12643                         new SearchSpec.Builder()
12644                                 .addFilterSchemas("Artist")
12645                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
12646                                 .build());
12647         documents = convertSearchResultsToDocuments(searchResults);
12648         assertThat(documents).hasSize(1);
12649         assertThat(documents).containsExactly(artistDoc);
12650     }
12651 
12652     @Test
12653     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SEARCH_RESULT_PARENT_TYPES)
testQuery_projectionWithPolymorphism()12654     public void testQuery_projectionWithPolymorphism() throws Exception {
12655         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
12656         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_RESULT_PARENT_TYPES));
12657 
12658         // Schema registration
12659         AppSearchSchema personSchema =
12660                 new AppSearchSchema.Builder("Person")
12661                         .addProperty(
12662                                 new StringPropertyConfig.Builder("name")
12663                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
12664                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
12665                                         .setIndexingType(
12666                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
12667                                         .build())
12668                         .addProperty(
12669                                 new StringPropertyConfig.Builder("emailAddress")
12670                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
12671                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
12672                                         .setIndexingType(
12673                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
12674                                         .build())
12675                         .build();
12676         AppSearchSchema artistSchema =
12677                 new AppSearchSchema.Builder("Artist")
12678                         .addParentType("Person")
12679                         .addProperty(
12680                                 new StringPropertyConfig.Builder("name")
12681                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
12682                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
12683                                         .setIndexingType(
12684                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
12685                                         .build())
12686                         .addProperty(
12687                                 new StringPropertyConfig.Builder("emailAddress")
12688                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
12689                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
12690                                         .setIndexingType(
12691                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
12692                                         .build())
12693                         .addProperty(
12694                                 new StringPropertyConfig.Builder("company")
12695                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
12696                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
12697                                         .setIndexingType(
12698                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
12699                                         .build())
12700                         .build();
12701         mDb1.setSchemaAsync(
12702                         new SetSchemaRequest.Builder()
12703                                 .addSchemas(personSchema)
12704                                 .addSchemas(artistSchema)
12705                                 .build())
12706                 .get();
12707 
12708         // Index two documents
12709         GenericDocument personDoc =
12710                 new GenericDocument.Builder<>("namespace", "id1", "Person")
12711                         .setCreationTimestampMillis(1000)
12712                         .setPropertyString("name", "Foo Person")
12713                         .setPropertyString("emailAddress", "person@gmail.com")
12714                         .build();
12715         GenericDocument artistDoc =
12716                 new GenericDocument.Builder<>("namespace", "id2", "Artist")
12717                         .setCreationTimestampMillis(1000)
12718                         .setPropertyString("name", "Foo Artist")
12719                         .setPropertyString("emailAddress", "artist@gmail.com")
12720                         .setPropertyString("company", "Company")
12721                         .build();
12722         checkIsBatchResultSuccess(
12723                 mDb1.putAsync(
12724                         new PutDocumentsRequest.Builder()
12725                                 .addGenericDocuments(personDoc, artistDoc)
12726                                 .build()));
12727 
12728         // Query with type property paths {"Person", ["name"]}, {"Artist", ["emailAddress"]}
12729         // This will be expanded to paths {"Person", ["name"]}, {"Artist", ["name", "emailAddress"]}
12730         // via polymorphism.
12731         SearchResults searchResults =
12732                 mDb1.search(
12733                         "Foo",
12734                         new SearchSpec.Builder()
12735                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
12736                                 .addProjection("Person", ImmutableList.of("name"))
12737                                 .addProjection("Artist", ImmutableList.of("emailAddress"))
12738                                 .build());
12739         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
12740 
12741         // The person document should have been returned with only the "name" property. The artist
12742         // document should have been returned with all of its properties.
12743         GenericDocument expectedPerson =
12744                 new GenericDocument.Builder<>("namespace", "id1", "Person")
12745                         .setCreationTimestampMillis(1000)
12746                         .setPropertyString("name", "Foo Person")
12747                         .build();
12748         GenericDocument expectedArtist =
12749                 new GenericDocument.Builder<>("namespace", "id2", "Artist")
12750                         .setCreationTimestampMillis(1000)
12751                         .setPropertyString("name", "Foo Artist")
12752                         .setPropertyString("emailAddress", "artist@gmail.com")
12753                         .build();
12754         assertThat(documents).containsExactly(expectedPerson, expectedArtist);
12755     }
12756 
12757     @Test
12758     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SEARCH_RESULT_PARENT_TYPES)
testQuery_projectionWithPolymorphismAndSchemaFilter()12759     public void testQuery_projectionWithPolymorphismAndSchemaFilter() throws Exception {
12760         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
12761         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_RESULT_PARENT_TYPES));
12762 
12763         // Schema registration
12764         AppSearchSchema personSchema =
12765                 new AppSearchSchema.Builder("Person")
12766                         .addProperty(
12767                                 new StringPropertyConfig.Builder("name")
12768                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
12769                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
12770                                         .setIndexingType(
12771                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
12772                                         .build())
12773                         .addProperty(
12774                                 new StringPropertyConfig.Builder("emailAddress")
12775                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
12776                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
12777                                         .setIndexingType(
12778                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
12779                                         .build())
12780                         .build();
12781         AppSearchSchema artistSchema =
12782                 new AppSearchSchema.Builder("Artist")
12783                         .addParentType("Person")
12784                         .addProperty(
12785                                 new StringPropertyConfig.Builder("name")
12786                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
12787                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
12788                                         .setIndexingType(
12789                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
12790                                         .build())
12791                         .addProperty(
12792                                 new StringPropertyConfig.Builder("emailAddress")
12793                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
12794                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
12795                                         .setIndexingType(
12796                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
12797                                         .build())
12798                         .addProperty(
12799                                 new StringPropertyConfig.Builder("company")
12800                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
12801                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
12802                                         .setIndexingType(
12803                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
12804                                         .build())
12805                         .build();
12806         mDb1.setSchemaAsync(
12807                         new SetSchemaRequest.Builder()
12808                                 .addSchemas(personSchema)
12809                                 .addSchemas(artistSchema)
12810                                 .build())
12811                 .get();
12812 
12813         // Index two documents
12814         GenericDocument personDoc =
12815                 new GenericDocument.Builder<>("namespace", "id1", "Person")
12816                         .setCreationTimestampMillis(1000)
12817                         .setPropertyString("name", "Foo Person")
12818                         .setPropertyString("emailAddress", "person@gmail.com")
12819                         .build();
12820         GenericDocument artistDoc =
12821                 new GenericDocument.Builder<>("namespace", "id2", "Artist")
12822                         .setCreationTimestampMillis(1000)
12823                         .setPropertyString("name", "Foo Artist")
12824                         .setPropertyString("emailAddress", "artist@gmail.com")
12825                         .setPropertyString("company", "Company")
12826                         .build();
12827         checkIsBatchResultSuccess(
12828                 mDb1.putAsync(
12829                         new PutDocumentsRequest.Builder()
12830                                 .addGenericDocuments(personDoc, artistDoc)
12831                                 .build()));
12832 
12833         // Query with type property paths {"Person", ["name"]} and {"Artist", ["emailAddress"]}, and
12834         // a schema filter for the "Person".
12835         // This will be expanded to paths {"Person", ["name"]} and
12836         // {"Artist", ["name", "emailAddress"]}, and filters for both "Person" and "Artist" via
12837         // polymorphism.
12838         SearchResults searchResults =
12839                 mDb1.search(
12840                         "Foo",
12841                         new SearchSpec.Builder()
12842                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
12843                                 .addFilterSchemas("Person")
12844                                 .addProjection("Person", ImmutableList.of("name"))
12845                                 .addProjection("Artist", ImmutableList.of("emailAddress"))
12846                                 .build());
12847         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
12848 
12849         // The person document should have been returned with only the "name" property. The artist
12850         // document should have been returned with all of its properties.
12851         GenericDocument expectedPerson =
12852                 new GenericDocument.Builder<>("namespace", "id1", "Person")
12853                         .setCreationTimestampMillis(1000)
12854                         .setPropertyString("name", "Foo Person")
12855                         .build();
12856         GenericDocument expectedArtist =
12857                 new GenericDocument.Builder<>("namespace", "id2", "Artist")
12858                         .setCreationTimestampMillis(1000)
12859                         .setPropertyString("name", "Foo Artist")
12860                         .setPropertyString("emailAddress", "artist@gmail.com")
12861                         .build();
12862         assertThat(documents).containsExactly(expectedPerson, expectedArtist);
12863     }
12864 
12865     @Test
12866     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SEARCH_RESULT_PARENT_TYPES)
testQuery_indexBasedOnParentTypePolymorphism()12867     public void testQuery_indexBasedOnParentTypePolymorphism() throws Exception {
12868         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
12869         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_RESULT_PARENT_TYPES));
12870 
12871         // Schema registration
12872         AppSearchSchema personSchema =
12873                 new AppSearchSchema.Builder("Person")
12874                         .addProperty(
12875                                 new StringPropertyConfig.Builder("name")
12876                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
12877                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
12878                                         .setIndexingType(
12879                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
12880                                         .build())
12881                         .build();
12882         AppSearchSchema artistSchema =
12883                 new AppSearchSchema.Builder("Artist")
12884                         .addParentType("Person")
12885                         .addProperty(
12886                                 new StringPropertyConfig.Builder("name")
12887                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
12888                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
12889                                         .setIndexingType(
12890                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
12891                                         .build())
12892                         .addProperty(
12893                                 new StringPropertyConfig.Builder("company")
12894                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
12895                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
12896                                         .setIndexingType(
12897                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
12898                                         .build())
12899                         .build();
12900         AppSearchSchema messageSchema =
12901                 new AppSearchSchema.Builder("Message")
12902                         .addProperty(
12903                                 new AppSearchSchema.DocumentPropertyConfig.Builder(
12904                                         "sender", "Person")
12905                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
12906                                         .setShouldIndexNestedProperties(true)
12907                                         .build())
12908                         .build();
12909         mDb1.setSchemaAsync(
12910                         new SetSchemaRequest.Builder()
12911                                 .addSchemas(personSchema)
12912                                 .addSchemas(artistSchema)
12913                                 .addSchemas(messageSchema)
12914                                 .build())
12915                 .get();
12916 
12917         // Index some an artistDoc and a messageDoc
12918         GenericDocument artistDoc =
12919                 new GenericDocument.Builder<>("namespace", "id1", "Artist")
12920                         .setPropertyString("name", "Foo")
12921                         .setPropertyString("company", "Bar")
12922                         .build();
12923         GenericDocument messageDoc =
12924                 new GenericDocument.Builder<>("namespace", "id2", "Message")
12925                         // sender is defined as a Person, which accepts an Artist because Artist <:
12926                         // Person.
12927                         // However, indexing will be based on what's defined in Person, so the
12928                         // "company"
12929                         // property in artistDoc cannot be used to search this messageDoc.
12930                         .setPropertyDocument("sender", artistDoc)
12931                         .build();
12932         checkIsBatchResultSuccess(
12933                 mDb1.putAsync(
12934                         new PutDocumentsRequest.Builder()
12935                                 .addGenericDocuments(artistDoc, messageDoc)
12936                                 .build()));
12937 
12938         // Query for the documents
12939         SearchResults searchResults =
12940                 mDb1.search(
12941                         "Foo",
12942                         new SearchSpec.Builder()
12943                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
12944                                 .build());
12945         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
12946         assertThat(documents).hasSize(2);
12947         assertThat(documents).containsExactly(artistDoc, messageDoc);
12948 
12949         // The "company" property in artistDoc cannot be used to search messageDoc.
12950         searchResults =
12951                 mDb1.search(
12952                         "Bar",
12953                         new SearchSpec.Builder()
12954                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
12955                                 .build());
12956         documents = convertSearchResultsToDocuments(searchResults);
12957         assertThat(documents).hasSize(1);
12958         assertThat(documents).containsExactly(artistDoc);
12959     }
12960 
12961     @Test
12962     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SEARCH_RESULT_PARENT_TYPES)
testQuery_parentTypeListIsTopologicalOrder()12963     public void testQuery_parentTypeListIsTopologicalOrder() throws Exception {
12964         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
12965         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_RESULT_PARENT_TYPES));
12966         // Create the following subtype relation graph, where
12967         // 1. A's direct parents are B and C.
12968         // 2. B's direct parent is D.
12969         // 3. C's direct parent is B and D.
12970         // DFS order from A: [A, B, D, C]. Not acceptable because B and D appear before C.
12971         // BFS order from A: [A, B, C, D]. Not acceptable because B appears before C.
12972         // Topological order (all subtypes appear before supertypes) from A: [A, C, B, D].
12973         AppSearchSchema schemaA =
12974                 new AppSearchSchema.Builder("A")
12975                         .addParentType("B")
12976                         .addParentType("C")
12977                         .build();
12978         AppSearchSchema schemaB =
12979                 new AppSearchSchema.Builder("B")
12980                         .addParentType("D")
12981                         .build();
12982         AppSearchSchema schemaC =
12983                 new AppSearchSchema.Builder("C")
12984                         .addParentType("B")
12985                         .addParentType("D")
12986                         .build();
12987         AppSearchSchema schemaD =
12988                 new AppSearchSchema.Builder("D")
12989                         .build();
12990         mDb1.setSchemaAsync(
12991                         new SetSchemaRequest.Builder()
12992                                 .addSchemas(schemaA)
12993                                 .addSchemas(schemaB)
12994                                 .addSchemas(schemaC)
12995                                 .addSchemas(schemaD)
12996                                 .build())
12997                 .get();
12998 
12999         // Index some documents
13000         GenericDocument docA =
13001                 new GenericDocument.Builder<>("namespace", "id1", "A")
13002                         .build();
13003         GenericDocument docB =
13004                 new GenericDocument.Builder<>("namespace", "id2", "B")
13005                         .build();
13006         GenericDocument docC =
13007                 new GenericDocument.Builder<>("namespace", "id3", "C")
13008                         .build();
13009         GenericDocument docD =
13010                 new GenericDocument.Builder<>("namespace", "id4", "D")
13011                         .build();
13012         checkIsBatchResultSuccess(
13013                 mDb1.putAsync(
13014                         new PutDocumentsRequest.Builder()
13015                                 .addGenericDocuments(docA, docB, docC, docD)
13016                                 .build()));
13017 
13018         Map<String, List<String>> expectedDocAParentTypeMap =
13019                 ImmutableMap.of("A", ImmutableList.of("C", "B", "D"));
13020         Map<String, List<String>> expectedDocBParentTypeMap =
13021                 ImmutableMap.of("B", ImmutableList.of("D"));
13022         Map<String, List<String>> expectedDocCParentTypeMap =
13023                 ImmutableMap.of("C", ImmutableList.of("B", "D"));
13024         Map<String, List<String>> expectedDocDParentTypeMap = Collections.emptyMap();
13025         // Query for the documents
13026         List<SearchResult> searchResults = retrieveAllSearchResults(
13027                 mDb1.search("", new SearchSpec.Builder().build())
13028         );
13029         assertThat(searchResults).hasSize(4);
13030         assertThat(searchResults.get(0).getGenericDocument()).isEqualTo(docD);
13031         assertThat(searchResults.get(0).getParentTypeMap()).isEqualTo(expectedDocDParentTypeMap);
13032 
13033         assertThat(searchResults.get(1).getGenericDocument()).isEqualTo(docC);
13034         assertThat(searchResults.get(1).getParentTypeMap()).isEqualTo(expectedDocCParentTypeMap);
13035 
13036         assertThat(searchResults.get(2).getGenericDocument()).isEqualTo(docB);
13037         assertThat(searchResults.get(2).getParentTypeMap()).isEqualTo(expectedDocBParentTypeMap);
13038 
13039         assertThat(searchResults.get(3).getGenericDocument()).isEqualTo(docA);
13040         assertThat(searchResults.get(3).getParentTypeMap()).isEqualTo(expectedDocAParentTypeMap);
13041     }
13042 
13043     @Test
13044     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SEARCH_RESULT_PARENT_TYPES)
testQuery_wildcardProjection_polymorphism()13045     public void testQuery_wildcardProjection_polymorphism() throws Exception {
13046         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
13047         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_RESULT_PARENT_TYPES));
13048 
13049         AppSearchSchema messageSchema = new AppSearchSchema.Builder("Message")
13050                 .addProperty(new StringPropertyConfig.Builder("sender")
13051                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
13052                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
13053                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
13054                         .build())
13055                 .addProperty(new StringPropertyConfig.Builder("content")
13056                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
13057                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
13058                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
13059                         .build())
13060                 .build();
13061         AppSearchSchema textSchema = new AppSearchSchema.Builder("Text")
13062                 .addParentType("Message")
13063                 .addProperty(new StringPropertyConfig.Builder("sender")
13064                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
13065                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
13066                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
13067                         .build())
13068                 .addProperty(new StringPropertyConfig.Builder("content")
13069                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
13070                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
13071                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
13072                         .build())
13073                 .build();
13074         AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email")
13075                 .addParentType("Message")
13076                 .addProperty(new StringPropertyConfig.Builder("sender")
13077                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
13078                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
13079                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
13080                         .build())
13081                 .addProperty(new StringPropertyConfig.Builder("content")
13082                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
13083                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
13084                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
13085                         .build())
13086                 .build();
13087 
13088         // Schema registration
13089         mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
13090                 .addSchemas(messageSchema, textSchema, emailSchema).build()).get();
13091 
13092         // Index two child documents
13093         GenericDocument text = new GenericDocument.Builder<>("namespace", "id1", "Text")
13094                 .setCreationTimestampMillis(1000)
13095                 .setPropertyString("sender", "Some sender")
13096                 .setPropertyString("content", "Some note")
13097                 .build();
13098         GenericDocument email = new GenericDocument.Builder<>("namespace", "id2", "Email")
13099                 .setCreationTimestampMillis(1000)
13100                 .setPropertyString("sender", "Some sender")
13101                 .setPropertyString("content", "Some note")
13102                 .build();
13103         checkIsBatchResultSuccess(mDb1.putAsync(new PutDocumentsRequest.Builder()
13104                 .addGenericDocuments(email, text).build()));
13105 
13106         SearchResults searchResults = mDb1.search("Some", new SearchSpec.Builder()
13107                 .addFilterSchemas("Message")
13108                 .addProjection(SearchSpec.SCHEMA_TYPE_WILDCARD, ImmutableList.of("sender"))
13109                 .addFilterProperties(SearchSpec.SCHEMA_TYPE_WILDCARD, ImmutableList.of("content"))
13110                 .build());
13111         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
13112 
13113         // We specified the parent document in the filter schemas, but only indexed child documents.
13114         // As we also specified a wildcard schema type projection, it should apply to the child docs
13115         // The content property must not appear. Also emailNoContent should not appear as we are
13116         // filter on the content property
13117         GenericDocument expectedText = new GenericDocument.Builder<>("namespace", "id1", "Text")
13118                 .setCreationTimestampMillis(1000)
13119                 .setPropertyString("sender", "Some sender")
13120                 .build();
13121         GenericDocument expectedEmail = new GenericDocument.Builder<>("namespace", "id2", "Email")
13122                 .setCreationTimestampMillis(1000)
13123                 .setPropertyString("sender", "Some sender")
13124                 .build();
13125         assertThat(documents).containsExactly(expectedText, expectedEmail);
13126     }
13127 
13128     @Test
13129     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SEARCH_RESULT_PARENT_TYPES)
testQuery_wildcardFilterSchema_polymorphism()13130     public void testQuery_wildcardFilterSchema_polymorphism() throws Exception {
13131         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
13132         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_RESULT_PARENT_TYPES));
13133 
13134         AppSearchSchema messageSchema = new AppSearchSchema.Builder("Message")
13135                 .addProperty(new StringPropertyConfig.Builder("content")
13136                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
13137                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
13138                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
13139                         .build())
13140                 .build();
13141         AppSearchSchema textSchema = new AppSearchSchema.Builder("Text")
13142                 .addParentType("Message")
13143                 .addProperty(new StringPropertyConfig.Builder("content")
13144                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
13145                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
13146                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
13147                         .build())
13148                 .addProperty(new StringPropertyConfig.Builder("carrier")
13149                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
13150                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
13151                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
13152                         .build())
13153                 .build();
13154         AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email")
13155                 .addParentType("Message")
13156                 .addProperty(new StringPropertyConfig.Builder("content")
13157                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
13158                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
13159                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
13160                         .build())
13161                 .addProperty(new StringPropertyConfig.Builder("attachment")
13162                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
13163                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
13164                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
13165                         .build())
13166                 .build();
13167 
13168         // Schema registration
13169         mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
13170                 .addSchemas(messageSchema, textSchema, emailSchema).build()).get();
13171 
13172         // Index two child documents
13173         GenericDocument text = new GenericDocument.Builder<>("namespace", "id1", "Text")
13174                 .setCreationTimestampMillis(1000)
13175                 .setPropertyString("content", "Some note")
13176                 .setPropertyString("carrier", "Network Inc")
13177                 .build();
13178         GenericDocument email = new GenericDocument.Builder<>("namespace", "id2", "Email")
13179                 .setCreationTimestampMillis(1000)
13180                 .setPropertyString("content", "Some note")
13181                 .setPropertyString("attachment", "Network report")
13182                 .build();
13183 
13184         checkIsBatchResultSuccess(mDb1.putAsync(new PutDocumentsRequest.Builder()
13185                 .addGenericDocuments(email, text).build()));
13186 
13187         // Both email and text would match for "Network", but only text should match as it is in the
13188         // right property
13189         SearchResults searchResults = mDb1.search("Network", new SearchSpec.Builder()
13190                 .addFilterSchemas("Message")
13191                 .addFilterProperties(SearchSpec.SCHEMA_TYPE_WILDCARD, ImmutableList.of("carrier"))
13192                 .build());
13193         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
13194 
13195         // We specified the parent document in the filter schemas, but only indexed child documents.
13196         // As we also specified a wildcard schema type projection, it should apply to the child docs
13197         // The content property must not appear. Also emailNoContent should not appear as we are
13198         // filter on the content property
13199         GenericDocument expectedText = new GenericDocument.Builder<>("namespace", "id1", "Text")
13200                 .setCreationTimestampMillis(1000)
13201                 .setPropertyString("content", "Some note")
13202                 .setPropertyString("carrier", "Network Inc")
13203                 .build();
13204         assertThat(documents).containsExactly(expectedText);
13205     }
13206 
13207     @Test
13208     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCORABLE_PROPERTY)
testRankWithNonScorableProperty()13209     public void testRankWithNonScorableProperty() throws Exception {
13210         // TODO(b/379923400): Implement this test.
13211     }
13212 
13213     @Test
13214     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCORABLE_PROPERTY)
testRankWithInvalidPropertyName()13215     public void testRankWithInvalidPropertyName() throws Exception {
13216         // TODO(b/379923400): Implement this test.
13217     }
13218 }
13219