xref: /aosp_15_r20/cts/tests/appsearch/src/com/android/cts/appsearch/external/app/AppSearchSessionCtsTestBase.java (revision b7c941bb3fa97aba169d73cee0bed2de8ac964bf)
1 /*
2  * Copyright 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.app.appsearch.cts.app;
18 
19 import static android.app.appsearch.AppSearchResult.RESULT_INVALID_ARGUMENT;
20 import static android.app.appsearch.AppSearchResult.RESULT_INVALID_SCHEMA;
21 import static android.app.appsearch.AppSearchResult.RESULT_NOT_FOUND;
22 import static android.app.appsearch.testutil.AppSearchTestUtils.checkIsBatchResultSuccess;
23 import static android.app.appsearch.testutil.AppSearchTestUtils.convertSearchResultsToDocuments;
24 import static android.app.appsearch.testutil.AppSearchTestUtils.doGet;
25 import static android.app.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.annotation.NonNull;
34 import android.app.appsearch.AppSearchBatchResult;
35 import android.app.appsearch.AppSearchResult;
36 import android.app.appsearch.AppSearchSchema;
37 import android.app.appsearch.AppSearchSchema.BooleanPropertyConfig;
38 import android.app.appsearch.AppSearchSchema.DocumentPropertyConfig;
39 import android.app.appsearch.AppSearchSchema.DoublePropertyConfig;
40 import android.app.appsearch.AppSearchSchema.LongPropertyConfig;
41 import android.app.appsearch.AppSearchSchema.PropertyConfig;
42 import android.app.appsearch.AppSearchSchema.StringPropertyConfig;
43 import android.app.appsearch.AppSearchSessionShim;
44 import android.app.appsearch.EmbeddingVector;
45 import android.app.appsearch.Features;
46 import android.app.appsearch.GenericDocument;
47 import android.app.appsearch.GetByDocumentIdRequest;
48 import android.app.appsearch.GetSchemaResponse;
49 import android.app.appsearch.JoinSpec;
50 import android.app.appsearch.PackageIdentifier;
51 import android.app.appsearch.PropertyPath;
52 import android.app.appsearch.PutDocumentsRequest;
53 import android.app.appsearch.RemoveByDocumentIdRequest;
54 import android.app.appsearch.ReportUsageRequest;
55 import android.app.appsearch.SchemaVisibilityConfig;
56 import android.app.appsearch.SearchResult;
57 import android.app.appsearch.SearchResults;
58 import android.app.appsearch.SearchResultsShim;
59 import android.app.appsearch.SearchSpec;
60 import android.app.appsearch.SearchSuggestionResult;
61 import android.app.appsearch.SearchSuggestionSpec;
62 import android.app.appsearch.SetSchemaRequest;
63 import android.app.appsearch.StorageInfo;
64 import android.app.appsearch.exceptions.AppSearchException;
65 import android.app.appsearch.testutil.AppSearchEmail;
66 import android.app.appsearch.testutil.AppSearchTestUtils;
67 import android.app.appsearch.util.DocumentIdUtil;
68 import android.content.Context;
69 import android.platform.test.annotations.RequiresFlagsEnabled;
70 import android.util.ArrayMap;
71 
72 import androidx.test.core.app.ApplicationProvider;
73 
74 import com.android.appsearch.flags.Flags;
75 
76 import com.google.common.collect.ImmutableList;
77 import com.google.common.collect.ImmutableMap;
78 import com.google.common.collect.ImmutableSet;
79 import com.google.common.util.concurrent.ListenableFuture;
80 import com.google.common.util.concurrent.MoreExecutors;
81 
82 import org.junit.After;
83 import org.junit.Before;
84 import org.junit.Rule;
85 import org.junit.Test;
86 import org.junit.rules.RuleChain;
87 
88 import java.util.ArrayList;
89 import java.util.Arrays;
90 import java.util.Collections;
91 import java.util.HashSet;
92 import java.util.List;
93 import java.util.Map;
94 import java.util.Set;
95 import java.util.concurrent.ExecutionException;
96 import java.util.concurrent.ExecutorService;
97 
98 public abstract class AppSearchSessionCtsTestBase {
99     static final String DB_NAME_1 = "";
100     static final String DB_NAME_2 = "testDb2";
101 
102     // Since we cannot call non-public API in the cts test, make a copy of these 3 action types, so
103     // we can create taken actions in GenericDocument form.
104     private static final int ACTION_TYPE_SEARCH = 1;
105     private static final int ACTION_TYPE_CLICK = 2;
106     private static final int ACTION_TYPE_IMPRESSION = 3;
107     private static final int ACTION_TYPE_DISMISS = 4;
108 
109     @Rule public final RuleChain mRuleChain = AppSearchTestUtils.createCommonTestRules();
110 
111     private final Context mContext = ApplicationProvider.getApplicationContext();
112 
113     private AppSearchSessionShim mDb1;
114     private AppSearchSessionShim mDb2;
115 
createSearchSessionAsync( @onNull String dbName)116     protected abstract ListenableFuture<AppSearchSessionShim> createSearchSessionAsync(
117             @NonNull String dbName) throws Exception;
118 
createSearchSessionAsync( @onNull String dbName, @NonNull ExecutorService executor)119     protected abstract ListenableFuture<AppSearchSessionShim> createSearchSessionAsync(
120             @NonNull String dbName, @NonNull ExecutorService executor) throws Exception;
121 
122     @Before
setUp()123     public void setUp() throws Exception {
124         mDb1 = createSearchSessionAsync(DB_NAME_1).get();
125         mDb2 = createSearchSessionAsync(DB_NAME_2).get();
126 
127         // Cleanup whatever documents may still exist in these databases. This is needed in
128         // addition to tearDown in case a test exited without completing properly.
129         cleanup();
130     }
131 
132     @After
tearDown()133     public void tearDown() throws Exception {
134         // Cleanup whatever documents may still exist in these databases.
135         cleanup();
136     }
137 
cleanup()138     private void cleanup() throws Exception {
139         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
140         mDb2.setSchemaAsync(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
141     }
142 
143     @Test
testSetSchema()144     public void testSetSchema() throws Exception {
145         AppSearchSchema emailSchema =
146                 new AppSearchSchema.Builder("Email")
147                         .addProperty(
148                                 new StringPropertyConfig.Builder("subject")
149                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
150                                         .setIndexingType(
151                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
152                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
153                                         .build())
154                         .addProperty(
155                                 new StringPropertyConfig.Builder("body")
156                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
157                                         .setIndexingType(
158                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
159                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
160                                         .build())
161                         .build();
162         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(emailSchema).build()).get();
163     }
164 
165     @Test
testSetSchema_Failure()166     public void testSetSchema_Failure() throws Exception {
167         mDb1.setSchemaAsync(
168                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
169                 .get();
170         AppSearchSchema emailSchema1 =
171                 new AppSearchSchema.Builder(AppSearchEmail.SCHEMA_TYPE).build();
172 
173         SetSchemaRequest setSchemaRequest1 =
174                 new SetSchemaRequest.Builder().addSchemas(emailSchema1).build();
175         ExecutionException executionException =
176                 assertThrows(
177                         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 =
187                 assertThrows(
188                         ExecutionException.class,
189                         () -> mDb1.setSchemaAsync(setSchemaRequest2).get());
190         assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
191         exception = (AppSearchException) executionException.getCause();
192         assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_SCHEMA);
193         assertThat(exception).hasMessageThat().contains("Schema is incompatible.");
194         assertThat(exception).hasMessageThat().contains("Deleted types: {builtin:Email}");
195     }
196 
197     @Test
198     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_APP_FUNCTIONS) // setDescription
testSetSchema_schemaDescription_notSupported()199     public void testSetSchema_schemaDescription_notSupported() throws Exception {
200         assumeFalse(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_SET_DESCRIPTION));
201         AppSearchSchema schema =
202                 new AppSearchSchema.Builder("Email1")
203                         .setDescription("Unsupported description")
204                         .addProperty(
205                                 new StringPropertyConfig.Builder("body")
206                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
207                                         .setIndexingType(
208                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
209                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
210                                         .build())
211                         .build();
212 
213         SetSchemaRequest request = new SetSchemaRequest.Builder().addSchemas(schema).build();
214 
215         UnsupportedOperationException exception =
216                 assertThrows(
217                         UnsupportedOperationException.class,
218                         () -> mDb1.setSchemaAsync(request).get());
219         assertThat(exception)
220                 .hasMessageThat()
221                 .contains(
222                         Features.SCHEMA_SET_DESCRIPTION
223                                 + " is not available on this AppSearch implementation.");
224     }
225 
226     @Test
227     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_APP_FUNCTIONS) // setDescription
testSetSchema_propertyDescription_notSupported()228     public void testSetSchema_propertyDescription_notSupported() throws Exception {
229         assumeFalse(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_SET_DESCRIPTION));
230         AppSearchSchema schema =
231                 new AppSearchSchema.Builder("Email1")
232                         .addProperty(
233                                 new StringPropertyConfig.Builder("body")
234                                         .setDescription("Unsupported description")
235                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
236                                         .setIndexingType(
237                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
238                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
239                                         .build())
240                         .build();
241 
242         SetSchemaRequest request = new SetSchemaRequest.Builder().addSchemas(schema).build();
243 
244         UnsupportedOperationException exception =
245                 assertThrows(
246                         UnsupportedOperationException.class,
247                         () -> mDb1.setSchemaAsync(request).get());
248         assertThat(exception)
249                 .hasMessageThat()
250                 .contains(
251                         Features.SCHEMA_SET_DESCRIPTION
252                                 + " is not available on this AppSearch implementation.");
253     }
254 
255     @Test
256     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_APP_FUNCTIONS) // setDescription
testSetSchema_updateSchemaDescription()257     public void testSetSchema_updateSchemaDescription() throws Exception {
258         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_SET_DESCRIPTION));
259 
260         AppSearchSchema schema1 =
261                 new AppSearchSchema.Builder("Email")
262                         .setDescription("A type of electronic message.")
263                         .addProperty(
264                                 new StringPropertyConfig.Builder("subject")
265                                         .setDescription("A summary of the email.")
266                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
267                                         .setIndexingType(
268                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
269                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
270                                         .build())
271                         .addProperty(
272                                 new StringPropertyConfig.Builder("body")
273                                         .setDescription("All of the content of the email.")
274                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
275                                         .setIndexingType(
276                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
277                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
278                                         .build())
279                         .build();
280 
281         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema1).build()).get();
282 
283         Set<AppSearchSchema> actualSchemaTypes = mDb1.getSchemaAsync().get().getSchemas();
284         assertThat(actualSchemaTypes).containsExactly(schema1);
285 
286         // Change the type description.
287         AppSearchSchema schema2 =
288                 new AppSearchSchema.Builder("Email")
289                         .setDescription("Like mail but with an 'a'.")
290                         .addProperty(
291                                 new StringPropertyConfig.Builder("subject")
292                                         .setDescription("A summary of the email.")
293                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
294                                         .setIndexingType(
295                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
296                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
297                                         .build())
298                         .addProperty(
299                                 new StringPropertyConfig.Builder("body")
300                                         .setDescription("All of the content of the email.")
301                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
302                                         .setIndexingType(
303                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
304                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
305                                         .build())
306                         .build();
307 
308         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema2).build()).get();
309 
310         GetSchemaResponse getSchemaResponse = mDb1.getSchemaAsync().get();
311         assertThat(getSchemaResponse.getSchemas()).containsExactly(schema2);
312     }
313 
314     @Test
315     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_APP_FUNCTIONS) // setDescription
testSetSchema_updatePropertyDescription()316     public void testSetSchema_updatePropertyDescription() throws Exception {
317         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_SET_DESCRIPTION));
318 
319         AppSearchSchema schema1 =
320                 new AppSearchSchema.Builder("Email")
321                         .setDescription("A type of electronic message.")
322                         .addProperty(
323                                 new StringPropertyConfig.Builder("subject")
324                                         .setDescription("A summary of the email.")
325                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
326                                         .setIndexingType(
327                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
328                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
329                                         .build())
330                         .addProperty(
331                                 new StringPropertyConfig.Builder("body")
332                                         .setDescription("All of the content of the email.")
333                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
334                                         .setIndexingType(
335                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
336                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
337                                         .build())
338                         .build();
339 
340         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema1).build()).get();
341 
342         Set<AppSearchSchema> actualSchemaTypes = mDb1.getSchemaAsync().get().getSchemas();
343         assertThat(actualSchemaTypes).containsExactly(schema1);
344 
345         // Change the type description.
346         AppSearchSchema schema2 =
347                 new AppSearchSchema.Builder("Email")
348                         .setDescription("A type of electronic message.")
349                         .addProperty(
350                                 new StringPropertyConfig.Builder("subject")
351                                         .setDescription("The most important part of the email.")
352                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
353                                         .setIndexingType(
354                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
355                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
356                                         .build())
357                         .addProperty(
358                                 new StringPropertyConfig.Builder("body")
359                                         .setDescription("All the other stuff.")
360                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
361                                         .setIndexingType(
362                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
363                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
364                                         .build())
365                         .build();
366 
367         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema2).build()).get();
368 
369         GetSchemaResponse getSchemaResponse = mDb1.getSchemaAsync().get();
370         assertThat(getSchemaResponse.getSchemas()).containsExactly(schema2);
371     }
372 
373     @Test
testSetSchema_updateVersion()374     public void testSetSchema_updateVersion() throws Exception {
375         AppSearchSchema schema =
376                 new AppSearchSchema.Builder("Email")
377                         .addProperty(
378                                 new StringPropertyConfig.Builder("subject")
379                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
380                                         .setIndexingType(
381                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
382                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
383                                         .build())
384                         .addProperty(
385                                 new StringPropertyConfig.Builder("body")
386                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
387                                         .setIndexingType(
388                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
389                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
390                                         .build())
391                         .build();
392 
393         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).setVersion(1).build())
394                 .get();
395 
396         Set<AppSearchSchema> actualSchemaTypes = mDb1.getSchemaAsync().get().getSchemas();
397         assertThat(actualSchemaTypes).containsExactly(schema);
398 
399         // increase version number
400         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).setVersion(2).build())
401                 .get();
402 
403         GetSchemaResponse getSchemaResponse = mDb1.getSchemaAsync().get();
404         assertThat(getSchemaResponse.getSchemas()).containsExactly(schema);
405         assertThat(getSchemaResponse.getVersion()).isEqualTo(2);
406     }
407 
408     @Test
testSetSchema_checkVersion()409     public void testSetSchema_checkVersion() throws Exception {
410         AppSearchSchema schema =
411                 new AppSearchSchema.Builder("Email")
412                         .addProperty(
413                                 new StringPropertyConfig.Builder("subject")
414                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
415                                         .setIndexingType(
416                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
417                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
418                                         .build())
419                         .addProperty(
420                                 new StringPropertyConfig.Builder("body")
421                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
422                                         .setIndexingType(
423                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
424                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
425                                         .build())
426                         .build();
427 
428         // set different version number to different database.
429         mDb1.setSchemaAsync(
430                         new SetSchemaRequest.Builder().addSchemas(schema).setVersion(135).build())
431                 .get();
432         mDb2.setSchemaAsync(
433                         new SetSchemaRequest.Builder().addSchemas(schema).setVersion(246).build())
434                 .get();
435 
436         // check the version has been set correctly.
437         GetSchemaResponse getSchemaResponse = mDb1.getSchemaAsync().get();
438         assertThat(getSchemaResponse.getSchemas()).containsExactly(schema);
439         assertThat(getSchemaResponse.getVersion()).isEqualTo(135);
440 
441         getSchemaResponse = mDb2.getSchemaAsync().get();
442         assertThat(getSchemaResponse.getSchemas()).containsExactly(schema);
443         assertThat(getSchemaResponse.getVersion()).isEqualTo(246);
444     }
445 
446     @Test
testSetSchema_addIndexedNestedDocumentProperty()447     public void testSetSchema_addIndexedNestedDocumentProperty() throws Exception {
448         // Create schema with a nested document type
449         // SectionId assignment for 'Person':
450         // - "name": string type, indexed. Section id = 0.
451         // - "worksFor.name": string type, (nested) indexed. Section id = 1.
452         AppSearchSchema personSchema =
453                 new AppSearchSchema.Builder("Person")
454                         .addProperty(
455                                 new StringPropertyConfig.Builder("name")
456                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
457                                         .setIndexingType(
458                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
459                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
460                                         .build())
461                         .addProperty(
462                                 new DocumentPropertyConfig.Builder("worksFor", "Organization")
463                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
464                                         .setShouldIndexNestedProperties(true)
465                                         .build())
466                         .build();
467         AppSearchSchema organizationSchema =
468                 new AppSearchSchema.Builder("Organization")
469                         .addProperty(
470                                 new StringPropertyConfig.Builder("name")
471                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
472                                         .setIndexingType(
473                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
474                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
475                                         .build())
476                         .build();
477         mDb1.setSchemaAsync(
478                         new SetSchemaRequest.Builder()
479                                 .addSchemas(personSchema, organizationSchema)
480                                 .build())
481                 .get();
482 
483         // Index documents and verify using getDocuments
484         GenericDocument person =
485                 new GenericDocument.Builder<>("namespace", "person1", "Person")
486                         .setPropertyString("name", "John")
487                         .setPropertyDocument(
488                                 "worksFor",
489                                 new GenericDocument.Builder<>("namespace", "org1", "Organization")
490                                         .setPropertyString("name", "Google")
491                                         .build())
492                         .build();
493 
494         AppSearchBatchResult<String, Void> putResult =
495                 checkIsBatchResultSuccess(
496                         mDb1.putAsync(
497                                 new PutDocumentsRequest.Builder()
498                                         .addGenericDocuments(person)
499                                         .build()));
500         assertThat(putResult.getSuccesses()).containsExactly("person1", null);
501         assertThat(putResult.getFailures()).isEmpty();
502 
503         GetByDocumentIdRequest getByDocumentIdRequest =
504                 new GetByDocumentIdRequest.Builder("namespace").addIds("person1").build();
505         List<GenericDocument> outDocuments = doGet(mDb1, getByDocumentIdRequest);
506         assertThat(outDocuments).hasSize(1);
507         assertThat(outDocuments).containsExactly(person);
508 
509         // Verify search using property filter
510         SearchResultsShim searchResults =
511                 mDb1.search(
512                         "worksFor.name:Google",
513                         new SearchSpec.Builder()
514                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
515                                 .build());
516         outDocuments = convertSearchResultsToDocuments(searchResults);
517         assertThat(outDocuments).hasSize(1);
518         assertThat(outDocuments).containsExactly(person);
519 
520         // Change the schema to add another nested document property to 'Person'
521         // The added property has 'optional' cardinality, so this change is compatible and indexed
522         // documents should still be searchable.
523         //
524         // New section id assignment for 'Person':
525         // - "almaMater.name", string type, (nested) indexed. Section id = 0
526         // - "name": string type, indexed. Section id = 1
527         // - "worksFor.name": string type, (nested) indexed. Section id = 2
528         AppSearchSchema newPersonSchema =
529                 new AppSearchSchema.Builder("Person")
530                         .addProperty(
531                                 new StringPropertyConfig.Builder("name")
532                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
533                                         .setIndexingType(
534                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
535                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
536                                         .build())
537                         .addProperty(
538                                 new DocumentPropertyConfig.Builder("worksFor", "Organization")
539                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
540                                         .setShouldIndexNestedProperties(true)
541                                         .build())
542                         .addProperty(
543                                 new DocumentPropertyConfig.Builder("almaMater", "Organization")
544                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
545                                         .setShouldIndexNestedProperties(true)
546                                         .build())
547                         .build();
548         mDb1.setSchemaAsync(
549                         new SetSchemaRequest.Builder()
550                                 .addSchemas(newPersonSchema, organizationSchema)
551                                 .build())
552                 .get();
553         Set<AppSearchSchema> outSchemaTypes = mDb1.getSchemaAsync().get().getSchemas();
554         assertThat(outSchemaTypes).containsExactly(newPersonSchema, organizationSchema);
555 
556         getByDocumentIdRequest =
557                 new GetByDocumentIdRequest.Builder("namespace").addIds("person1").build();
558         outDocuments = doGet(mDb1, getByDocumentIdRequest);
559         assertThat(outDocuments).hasSize(1);
560         assertThat(outDocuments).containsExactly(person);
561 
562         // Verify that index rebuild was triggered correctly. The same query "worksFor.name:Google"
563         // should still match the same result.
564         searchResults =
565                 mDb1.search(
566                         "worksFor.name:Google",
567                         new SearchSpec.Builder()
568                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
569                                 .build());
570         outDocuments = convertSearchResultsToDocuments(searchResults);
571         assertThat(outDocuments).hasSize(1);
572         assertThat(outDocuments).containsExactly(person);
573 
574         // In new_schema the 'name' property is now indexed at section id 1. If searching for
575         // "name:Google" matched the document, this means that index rebuild was not triggered
576         // correctly and Icing is still searching the old index, where 'worksFor.name' was
577         // indexed at section id 1.
578         searchResults =
579                 mDb1.search(
580                         "name:Google",
581                         new SearchSpec.Builder()
582                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
583                                 .build());
584         outDocuments = convertSearchResultsToDocuments(searchResults);
585         assertThat(outDocuments).isEmpty();
586     }
587 
588     @Test
testSetSchemaWithValidCycle_allowCircularReferences()589     public void testSetSchemaWithValidCycle_allowCircularReferences() throws Exception {
590         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SET_SCHEMA_CIRCULAR_REFERENCES));
591 
592         // Create schema with valid cycle: Person -> Organization -> Person...
593         AppSearchSchema personSchema =
594                 new AppSearchSchema.Builder("Person")
595                         .addProperty(
596                                 new StringPropertyConfig.Builder("name")
597                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
598                                         .setIndexingType(
599                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
600                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
601                                         .build())
602                         .addProperty(
603                                 new DocumentPropertyConfig.Builder("worksFor", "Organization")
604                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
605                                         .setShouldIndexNestedProperties(true)
606                                         .build())
607                         .build();
608         AppSearchSchema organizationSchema =
609                 new AppSearchSchema.Builder("Organization")
610                         .addProperty(
611                                 new StringPropertyConfig.Builder("name")
612                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
613                                         .setIndexingType(
614                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
615                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
616                                         .build())
617                         .addProperty(
618                                 new DocumentPropertyConfig.Builder("funder", "Person")
619                                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
620                                         .setShouldIndexNestedProperties(false)
621                                         .build())
622                         .build();
623         mDb1.setSchemaAsync(
624                         new SetSchemaRequest.Builder()
625                                 .addSchemas(personSchema, organizationSchema)
626                                 .build())
627                 .get();
628 
629         // Test that documents following the circular schema are indexable, and that its sections
630         // are searchable
631         GenericDocument person =
632                 new GenericDocument.Builder<>("namespace", "person1", "Person")
633                         .setPropertyString("name", "John")
634                         .setPropertyDocument("worksFor")
635                         .build();
636         GenericDocument org =
637                 new GenericDocument.Builder<>("namespace", "org1", "Organization")
638                         .setPropertyString("name", "Org")
639                         .setPropertyDocument("funder", person)
640                         .build();
641         GenericDocument person2 =
642                 new GenericDocument.Builder<>("namespace", "person2", "Person")
643                         .setPropertyString("name", "Jane")
644                         .setPropertyDocument("worksFor", org)
645                         .build();
646 
647         AppSearchBatchResult<String, Void> putResult =
648                 checkIsBatchResultSuccess(
649                         mDb1.putAsync(
650                                 new PutDocumentsRequest.Builder()
651                                         .addGenericDocuments(person, org, person2)
652                                         .build()));
653         assertThat(putResult.getSuccesses())
654                 .containsExactly("person1", null, "org1", null, "person2", null);
655         assertThat(putResult.getFailures()).isEmpty();
656 
657         GetByDocumentIdRequest getByDocumentIdRequest =
658                 new GetByDocumentIdRequest.Builder("namespace")
659                         .addIds("person1", "person2", "org1")
660                         .build();
661         List<GenericDocument> outDocuments = doGet(mDb1, getByDocumentIdRequest);
662         assertThat(outDocuments).hasSize(3);
663         assertThat(outDocuments).containsExactly(person, person2, org);
664 
665         // Both org1 and person2 should be returned for query "Org"
666         // For org1 this matches the 'name' property and for person2 this matches the
667         // 'worksFor.name' property.
668         SearchResultsShim searchResults =
669                 mDb1.search(
670                         "Org",
671                         new SearchSpec.Builder()
672                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
673                                 .build());
674         outDocuments = convertSearchResultsToDocuments(searchResults);
675         assertThat(outDocuments).hasSize(2);
676         assertThat(outDocuments).containsExactly(person2, org);
677     }
678 
679     @Test
testSetSchemaWithInvalidCycle_circularReferencesSupported()680     public void testSetSchemaWithInvalidCycle_circularReferencesSupported() throws Exception {
681         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SET_SCHEMA_CIRCULAR_REFERENCES));
682 
683         // Create schema with invalid cycle: Person -> Organization -> Person... where all
684         // DocumentPropertyConfigs have setShouldIndexNestedProperties(true).
685         AppSearchSchema personSchema =
686                 new AppSearchSchema.Builder("Person")
687                         .addProperty(
688                                 new StringPropertyConfig.Builder("name")
689                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
690                                         .setIndexingType(
691                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
692                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
693                                         .build())
694                         .addProperty(
695                                 new DocumentPropertyConfig.Builder("worksFor", "Organization")
696                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
697                                         .setShouldIndexNestedProperties(true)
698                                         .build())
699                         .build();
700         AppSearchSchema organizationSchema =
701                 new AppSearchSchema.Builder("Organization")
702                         .addProperty(
703                                 new StringPropertyConfig.Builder("name")
704                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
705                                         .setIndexingType(
706                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
707                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
708                                         .build())
709                         .addProperty(
710                                 new DocumentPropertyConfig.Builder("funder", "Person")
711                                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
712                                         .setShouldIndexNestedProperties(true)
713                                         .build())
714                         .build();
715 
716         SetSchemaRequest setSchemaRequest =
717                 new SetSchemaRequest.Builder().addSchemas(personSchema, organizationSchema).build();
718         ExecutionException executionException =
719                 assertThrows(
720                         ExecutionException.class,
721                         () -> mDb1.setSchemaAsync(setSchemaRequest).get());
722         assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
723         AppSearchException exception = (AppSearchException) executionException.getCause();
724         assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT);
725         assertThat(exception).hasMessageThat().containsMatch("Invalid cycle|Infinite loop");
726     }
727 
728     @Test
testSetSchemaWithValidCycle_circularReferencesNotSupported()729     public void testSetSchemaWithValidCycle_circularReferencesNotSupported() {
730         assumeFalse(mDb1.getFeatures().isFeatureSupported(Features.SET_SCHEMA_CIRCULAR_REFERENCES));
731 
732         // Create schema with valid cycle: Person -> Organization -> Person...
733         AppSearchSchema personSchema =
734                 new AppSearchSchema.Builder("Person")
735                         .addProperty(
736                                 new StringPropertyConfig.Builder("name")
737                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
738                                         .setIndexingType(
739                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
740                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
741                                         .build())
742                         .addProperty(
743                                 new DocumentPropertyConfig.Builder("worksFor", "Organization")
744                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
745                                         .setShouldIndexNestedProperties(true)
746                                         .build())
747                         .build();
748         AppSearchSchema organizationSchema =
749                 new AppSearchSchema.Builder("Organization")
750                         .addProperty(
751                                 new StringPropertyConfig.Builder("name")
752                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
753                                         .setIndexingType(
754                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
755                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
756                                         .build())
757                         .addProperty(
758                                 new DocumentPropertyConfig.Builder("funder", "Person")
759                                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
760                                         .setShouldIndexNestedProperties(false)
761                                         .build())
762                         .build();
763 
764         SetSchemaRequest setSchemaRequest =
765                 new SetSchemaRequest.Builder().addSchemas(personSchema, organizationSchema).build();
766         ExecutionException executionException =
767                 assertThrows(
768                         ExecutionException.class,
769                         () -> mDb1.setSchemaAsync(setSchemaRequest).get());
770         assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
771         AppSearchException exception = (AppSearchException) executionException.getCause();
772         assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT);
773         assertThat(exception).hasMessageThat().containsMatch("Invalid cycle|Infinite loop");
774     }
775 
776     /** Test indexing maximum properties into a schema. */
777     @Test
testSetSchema_maxProperties()778     public void testSetSchema_maxProperties() throws Exception {
779         int maxProperties = mDb1.getFeatures().getMaxIndexedProperties();
780         AppSearchSchema.Builder schemaBuilder = new AppSearchSchema.Builder("testSchema");
781         for (int i = 0; i < maxProperties; i++) {
782             schemaBuilder.addProperty(
783                     new StringPropertyConfig.Builder("string" + i)
784                             .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
785                             .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
786                             .build());
787         }
788         AppSearchSchema maxSchema = schemaBuilder.build();
789         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(maxSchema).build()).get();
790         Set<AppSearchSchema> actual1 = mDb1.getSchemaAsync().get().getSchemas();
791         assertThat(actual1).containsExactly(maxSchema);
792 
793         schemaBuilder.addProperty(
794                 new StringPropertyConfig.Builder("toomuch")
795                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
796                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
797                         .build());
798         ExecutionException exception =
799                 assertThrows(
800                         ExecutionException.class,
801                         () ->
802                                 mDb1.setSchemaAsync(
803                                                 new SetSchemaRequest.Builder()
804                                                         .addSchemas(schemaBuilder.build())
805                                                         .setForceOverride(true)
806                                                         .build())
807                                         .get());
808         Throwable cause = exception.getCause();
809         assertThat(cause).isInstanceOf(AppSearchException.class);
810         assertThat(cause.getMessage())
811                 .isEqualTo(
812                         "Too many properties to be indexed, max "
813                                 + "number of properties allowed: "
814                                 + maxProperties);
815     }
816 
817     @Test
testSetSchema_maxProperties_nestedSchemas()818     public void testSetSchema_maxProperties_nestedSchemas() throws Exception {
819         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SET_SCHEMA_CIRCULAR_REFERENCES));
820 
821         int maxProperties = mDb1.getFeatures().getMaxIndexedProperties();
822         AppSearchSchema.Builder personSchemaBuilder = new AppSearchSchema.Builder("Person");
823         for (int i = 0; i < maxProperties / 3 + 1; i++) {
824             personSchemaBuilder.addProperty(
825                     new StringPropertyConfig.Builder("string" + i)
826                             .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
827                             .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
828                             .build());
829         }
830         personSchemaBuilder.addProperty(
831                 new DocumentPropertyConfig.Builder("worksFor", "Organization")
832                         .setShouldIndexNestedProperties(false)
833                         .build());
834         personSchemaBuilder.addProperty(
835                 new DocumentPropertyConfig.Builder("address", "Address")
836                         .setShouldIndexNestedProperties(true)
837                         .build());
838 
839         AppSearchSchema.Builder orgSchemaBuilder = new AppSearchSchema.Builder("Organization");
840         for (int i = 0; i < maxProperties / 3; i++) {
841             orgSchemaBuilder.addProperty(
842                     new StringPropertyConfig.Builder("string" + i)
843                             .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
844                             .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
845                             .build());
846         }
847         orgSchemaBuilder.addProperty(
848                 new DocumentPropertyConfig.Builder("funder", "Person")
849                         .setShouldIndexNestedProperties(true)
850                         .build());
851 
852         AppSearchSchema.Builder addressSchemaBuilder = new AppSearchSchema.Builder("Address");
853         for (int i = 0; i < maxProperties / 3; i++) {
854             addressSchemaBuilder.addProperty(
855                     new StringPropertyConfig.Builder("string" + i)
856                             .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
857                             .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
858                             .build());
859         }
860 
861         AppSearchSchema personSchema = personSchemaBuilder.build();
862         AppSearchSchema orgSchema = orgSchemaBuilder.build();
863         AppSearchSchema addressSchema = addressSchemaBuilder.build();
864         mDb1.setSchemaAsync(
865                         new SetSchemaRequest.Builder()
866                                 .addSchemas(personSchema, orgSchema, addressSchema)
867                                 .build())
868                 .get();
869         Set<AppSearchSchema> schemas = mDb1.getSchemaAsync().get().getSchemas();
870         assertThat(schemas).containsExactly(personSchema, orgSchema, addressSchema);
871 
872         // Add one more property to bring the number of sections over the max limit
873         personSchemaBuilder.addProperty(
874                 new StringPropertyConfig.Builder("toomuch")
875                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
876                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
877                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
878                         .build());
879         ExecutionException exception =
880                 assertThrows(
881                         ExecutionException.class,
882                         () ->
883                                 mDb1.setSchemaAsync(
884                                                 new SetSchemaRequest.Builder()
885                                                         .addSchemas(
886                                                                 personSchemaBuilder.build(),
887                                                                 orgSchema,
888                                                                 addressSchema)
889                                                         .setForceOverride(true)
890                                                         .build())
891                                         .get());
892         Throwable cause = exception.getCause();
893         assertThat(cause).isInstanceOf(AppSearchException.class);
894         assertThat(cause.getMessage()).contains("Too many properties to be indexed");
895     }
896 
897     @Test
testGetSchema_allPropertyTypes()898     public void testGetSchema_allPropertyTypes() throws Exception {
899         AppSearchSchema inSchema =
900                 new AppSearchSchema.Builder("Test")
901                         .addProperty(
902                                 new StringPropertyConfig.Builder("string")
903                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
904                                         .setIndexingType(
905                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
906                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
907                                         .build())
908                         .addProperty(
909                                 new AppSearchSchema.LongPropertyConfig.Builder("long")
910                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
911                                         .build())
912                         .addProperty(
913                                 new AppSearchSchema.DoublePropertyConfig.Builder("double")
914                                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
915                                         .build())
916                         .addProperty(
917                                 new AppSearchSchema.BooleanPropertyConfig.Builder("boolean")
918                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
919                                         .build())
920                         .addProperty(
921                                 new AppSearchSchema.BytesPropertyConfig.Builder("bytes")
922                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
923                                         .build())
924                         .addProperty(
925                                 new AppSearchSchema.DocumentPropertyConfig.Builder(
926                                                 "document", AppSearchEmail.SCHEMA_TYPE)
927                                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
928                                         .setShouldIndexNestedProperties(true)
929                                         .build())
930                         .build();
931 
932         // Add it to AppSearch and then obtain it again
933         mDb1.setSchemaAsync(
934                         new SetSchemaRequest.Builder()
935                                 .addSchemas(inSchema, AppSearchEmail.SCHEMA)
936                                 .build())
937                 .get();
938         GetSchemaResponse response = mDb1.getSchemaAsync().get();
939         List<AppSearchSchema> schemas = new ArrayList<>(response.getSchemas());
940         assertThat(schemas).containsExactly(inSchema, AppSearchEmail.SCHEMA);
941         AppSearchSchema outSchema;
942         if (schemas.get(0).getSchemaType().equals("Test")) {
943             outSchema = schemas.get(0);
944         } else {
945             outSchema = schemas.get(1);
946         }
947         assertThat(outSchema.getSchemaType()).isEqualTo("Test");
948         assertThat(outSchema).isNotSameInstanceAs(inSchema);
949 
950         List<PropertyConfig> properties = outSchema.getProperties();
951         assertThat(properties).hasSize(6);
952 
953         assertThat(properties.get(0).getName()).isEqualTo("string");
954         assertThat(properties.get(0).getCardinality())
955                 .isEqualTo(PropertyConfig.CARDINALITY_REQUIRED);
956         assertThat(((StringPropertyConfig) properties.get(0)).getIndexingType())
957                 .isEqualTo(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS);
958         assertThat(((StringPropertyConfig) properties.get(0)).getTokenizerType())
959                 .isEqualTo(StringPropertyConfig.TOKENIZER_TYPE_PLAIN);
960 
961         assertThat(properties.get(1).getName()).isEqualTo("long");
962         assertThat(properties.get(1).getCardinality())
963                 .isEqualTo(PropertyConfig.CARDINALITY_OPTIONAL);
964         assertThat(properties.get(1)).isInstanceOf(AppSearchSchema.LongPropertyConfig.class);
965 
966         assertThat(properties.get(2).getName()).isEqualTo("double");
967         assertThat(properties.get(2).getCardinality())
968                 .isEqualTo(PropertyConfig.CARDINALITY_REPEATED);
969         assertThat(properties.get(2)).isInstanceOf(AppSearchSchema.DoublePropertyConfig.class);
970 
971         assertThat(properties.get(3).getName()).isEqualTo("boolean");
972         assertThat(properties.get(3).getCardinality())
973                 .isEqualTo(PropertyConfig.CARDINALITY_REQUIRED);
974         assertThat(properties.get(3)).isInstanceOf(AppSearchSchema.BooleanPropertyConfig.class);
975 
976         assertThat(properties.get(4).getName()).isEqualTo("bytes");
977         assertThat(properties.get(4).getCardinality())
978                 .isEqualTo(PropertyConfig.CARDINALITY_OPTIONAL);
979         assertThat(properties.get(4)).isInstanceOf(AppSearchSchema.BytesPropertyConfig.class);
980 
981         assertThat(properties.get(5).getName()).isEqualTo("document");
982         assertThat(properties.get(5).getCardinality())
983                 .isEqualTo(PropertyConfig.CARDINALITY_REPEATED);
984         assertThat(((AppSearchSchema.DocumentPropertyConfig) properties.get(5)).getSchemaType())
985                 .isEqualTo(AppSearchEmail.SCHEMA_TYPE);
986         assertThat(
987                         ((AppSearchSchema.DocumentPropertyConfig) properties.get(5))
988                                 .shouldIndexNestedProperties())
989                 .isEqualTo(true);
990     }
991 
992     @Test
testGetSchema_visibilitySetting()993     public void testGetSchema_visibilitySetting() throws Exception {
994         assumeTrue(
995                 mDb1.getFeatures().isFeatureSupported(Features.ADD_PERMISSIONS_AND_GET_VISIBILITY));
996         AppSearchSchema emailSchema =
997                 new AppSearchSchema.Builder("Email1")
998                         .addProperty(
999                                 new StringPropertyConfig.Builder("subject")
1000                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
1001                                         .setIndexingType(
1002                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
1003                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
1004                                         .build())
1005                         .addProperty(
1006                                 new StringPropertyConfig.Builder("body")
1007                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
1008                                         .setIndexingType(
1009                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
1010                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
1011                                         .build())
1012                         .build();
1013 
1014         byte[] shar256Cert1 = new byte[32];
1015         Arrays.fill(shar256Cert1, (byte) 1);
1016         byte[] shar256Cert2 = new byte[32];
1017         Arrays.fill(shar256Cert2, (byte) 2);
1018         PackageIdentifier packageIdentifier1 = new PackageIdentifier("pkgFoo", shar256Cert1);
1019         PackageIdentifier packageIdentifier2 = new PackageIdentifier("pkgBar", shar256Cert2);
1020         SetSchemaRequest request =
1021                 new SetSchemaRequest.Builder()
1022                         .addSchemas(emailSchema)
1023                         .setSchemaTypeDisplayedBySystem("Email1", /* displayed= */ false)
1024                         .setSchemaTypeVisibilityForPackage(
1025                                 "Email1", /* visible= */ true, packageIdentifier1)
1026                         .setSchemaTypeVisibilityForPackage(
1027                                 "Email1", /* visible= */ true, packageIdentifier2)
1028                         .addRequiredPermissionsForSchemaTypeVisibility(
1029                                 "Email1",
1030                                 ImmutableSet.of(
1031                                         SetSchemaRequest.READ_SMS, SetSchemaRequest.READ_CALENDAR))
1032                         .addRequiredPermissionsForSchemaTypeVisibility(
1033                                 "Email1",
1034                                 ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA))
1035                         .build();
1036 
1037         mDb1.setSchemaAsync(request).get();
1038 
1039         GetSchemaResponse getSchemaResponse = mDb1.getSchemaAsync().get();
1040         Set<AppSearchSchema> actual = getSchemaResponse.getSchemas();
1041         assertThat(actual).hasSize(1);
1042         assertThat(actual).isEqualTo(request.getSchemas());
1043         assertThat(getSchemaResponse.getSchemaTypesNotDisplayedBySystem())
1044                 .containsExactly("Email1");
1045         assertThat(getSchemaResponse.getSchemaTypesVisibleToPackages())
1046                 .containsExactly("Email1", ImmutableSet.of(packageIdentifier1, packageIdentifier2));
1047         assertThat(getSchemaResponse.getRequiredPermissionsForSchemaTypeVisibility())
1048                 .containsExactly(
1049                         "Email1",
1050                         ImmutableSet.of(
1051                                 ImmutableSet.of(
1052                                         SetSchemaRequest.READ_SMS, SetSchemaRequest.READ_CALENDAR),
1053                                 ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA)));
1054     }
1055 
1056     @Test
testGetSchema_visibilitySetting_oneSharedSchema()1057     public void testGetSchema_visibilitySetting_oneSharedSchema() throws Exception {
1058         assumeTrue(
1059                 mDb1.getFeatures().isFeatureSupported(Features.ADD_PERMISSIONS_AND_GET_VISIBILITY));
1060 
1061         AppSearchSchema noteSchema =
1062                 new AppSearchSchema.Builder("Note")
1063                         .addProperty(new StringPropertyConfig.Builder("subject").build())
1064                         .build();
1065         SetSchemaRequest.Builder requestBuilder =
1066                 new SetSchemaRequest.Builder()
1067                         .addSchemas(AppSearchEmail.SCHEMA, noteSchema)
1068                         .setSchemaTypeDisplayedBySystem(noteSchema.getSchemaType(), false)
1069                         .setSchemaTypeVisibilityForPackage(
1070                                 noteSchema.getSchemaType(),
1071                                 true,
1072                                 new PackageIdentifier("com.some.package1", new byte[32]))
1073                         .addRequiredPermissionsForSchemaTypeVisibility(
1074                                 noteSchema.getSchemaType(),
1075                                 Collections.singleton(SetSchemaRequest.READ_SMS));
1076         if (mDb1.getFeatures()
1077                 .isFeatureSupported(Features.SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE)) {
1078             requestBuilder.setPubliclyVisibleSchema(
1079                     noteSchema.getSchemaType(),
1080                     new PackageIdentifier("com.some.package2", new byte[32]));
1081         }
1082         SetSchemaRequest request = requestBuilder.build();
1083         mDb1.setSchemaAsync(request).get();
1084 
1085         GetSchemaResponse getSchemaResponse = mDb1.getSchemaAsync().get();
1086         Set<AppSearchSchema> actual = getSchemaResponse.getSchemas();
1087         assertThat(actual).hasSize(2);
1088         assertThat(actual).isEqualTo(request.getSchemas());
1089 
1090         // Check visibility settings. Schemas without settings shouldn't appear in the result at
1091         // all, even with empty maps as values.
1092         assertThat(getSchemaResponse.getSchemaTypesNotDisplayedBySystem())
1093                 .containsExactly(noteSchema.getSchemaType());
1094         assertThat(getSchemaResponse.getSchemaTypesVisibleToPackages())
1095                 .containsExactly(
1096                         noteSchema.getSchemaType(),
1097                         ImmutableSet.of(new PackageIdentifier("com.some.package1", new byte[32])));
1098         assertThat(getSchemaResponse.getRequiredPermissionsForSchemaTypeVisibility())
1099                 .containsExactly(
1100                         noteSchema.getSchemaType(),
1101                         ImmutableSet.of(ImmutableSet.of(SetSchemaRequest.READ_SMS)));
1102         if (mDb1.getFeatures()
1103                 .isFeatureSupported(Features.SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE)) {
1104             assertThat(getSchemaResponse.getPubliclyVisibleSchemas())
1105                     .containsExactly(
1106                             noteSchema.getSchemaType(),
1107                             new PackageIdentifier("com.some.package2", new byte[32]));
1108         }
1109     }
1110 
1111     @Test
testGetSchema_visibilitySetting_notSupported()1112     public void testGetSchema_visibilitySetting_notSupported() throws Exception {
1113         assumeFalse(
1114                 mDb1.getFeatures().isFeatureSupported(Features.ADD_PERMISSIONS_AND_GET_VISIBILITY));
1115         AppSearchSchema emailSchema =
1116                 new AppSearchSchema.Builder("Email1")
1117                         .addProperty(
1118                                 new StringPropertyConfig.Builder("subject")
1119                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
1120                                         .setIndexingType(
1121                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
1122                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
1123                                         .build())
1124                         .addProperty(
1125                                 new StringPropertyConfig.Builder("body")
1126                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
1127                                         .setIndexingType(
1128                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
1129                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
1130                                         .build())
1131                         .build();
1132 
1133         byte[] shar256Cert1 = new byte[32];
1134         Arrays.fill(shar256Cert1, (byte) 1);
1135         byte[] shar256Cert2 = new byte[32];
1136         Arrays.fill(shar256Cert2, (byte) 2);
1137         PackageIdentifier packageIdentifier1 = new PackageIdentifier("pkgFoo", shar256Cert1);
1138         PackageIdentifier packageIdentifier2 = new PackageIdentifier("pkgBar", shar256Cert2);
1139         SetSchemaRequest request =
1140                 new SetSchemaRequest.Builder()
1141                         .addSchemas(emailSchema)
1142                         .setSchemaTypeDisplayedBySystem("Email1", /* displayed= */ false)
1143                         .setSchemaTypeVisibilityForPackage(
1144                                 "Email1", /* visible= */ true, packageIdentifier1)
1145                         .setSchemaTypeVisibilityForPackage(
1146                                 "Email1", /* visible= */ true, packageIdentifier2)
1147                         .build();
1148 
1149         mDb1.setSchemaAsync(request).get();
1150 
1151         GetSchemaResponse getSchemaResponse = mDb1.getSchemaAsync().get();
1152         Set<AppSearchSchema> actual = getSchemaResponse.getSchemas();
1153         assertThat(actual).hasSize(1);
1154         assertThat(actual).isEqualTo(request.getSchemas());
1155         assertThrows(
1156                 UnsupportedOperationException.class,
1157                 () -> getSchemaResponse.getSchemaTypesNotDisplayedBySystem());
1158         assertThrows(
1159                 UnsupportedOperationException.class,
1160                 () -> getSchemaResponse.getSchemaTypesVisibleToPackages());
1161         assertThrows(
1162                 UnsupportedOperationException.class,
1163                 () -> getSchemaResponse.getRequiredPermissionsForSchemaTypeVisibility());
1164     }
1165 
1166     @Test
testSetSchema_visibilitySettingPermission_notSupported()1167     public void testSetSchema_visibilitySettingPermission_notSupported() {
1168         assumeFalse(
1169                 mDb1.getFeatures().isFeatureSupported(Features.ADD_PERMISSIONS_AND_GET_VISIBILITY));
1170         AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email1").build();
1171 
1172         SetSchemaRequest request =
1173                 new SetSchemaRequest.Builder()
1174                         .addSchemas(emailSchema)
1175                         .setSchemaTypeDisplayedBySystem("Email1", /* displayed= */ false)
1176                         .addRequiredPermissionsForSchemaTypeVisibility(
1177                                 "Email1", ImmutableSet.of(SetSchemaRequest.READ_SMS))
1178                         .build();
1179 
1180         assertThrows(UnsupportedOperationException.class, () -> mDb1.setSchemaAsync(request).get());
1181     }
1182 
1183     @Test
testSetSchema_publiclyVisible()1184     public void testSetSchema_publiclyVisible() throws Exception {
1185         assumeTrue(
1186                 mDb1.getFeatures()
1187                         .isFeatureSupported(Features.SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE));
1188 
1189         PackageIdentifier pkg = new PackageIdentifier(mContext.getPackageName(), new byte[32]);
1190         SetSchemaRequest request =
1191                 new SetSchemaRequest.Builder()
1192                         .addSchemas(AppSearchEmail.SCHEMA)
1193                         .setPubliclyVisibleSchema("builtin:Email", pkg)
1194                         .build();
1195 
1196         mDb1.setSchemaAsync(request).get();
1197         GetSchemaResponse getSchemaResponse = mDb1.getSchemaAsync().get();
1198 
1199         assertThat(getSchemaResponse.getSchemas()).containsExactly(AppSearchEmail.SCHEMA);
1200         assertThat(getSchemaResponse.getPubliclyVisibleSchemas())
1201                 .isEqualTo(ImmutableMap.of("builtin:Email", pkg));
1202 
1203         AppSearchEmail email =
1204                 new AppSearchEmail.Builder("namespace", "id1")
1205                         .setSubject("testPut example")
1206                         .build();
1207 
1208         // mDb1 and mDb2 are in the same package, so we can't REALLY test out public acl. But we
1209         // can make sure they their own documents under the Public ACL.
1210         AppSearchBatchResult<String, Void> putResult =
1211                 checkIsBatchResultSuccess(
1212                         mDb1.putAsync(
1213                                 new PutDocumentsRequest.Builder()
1214                                         .addGenericDocuments(email)
1215                                         .build()));
1216         assertThat(putResult.getSuccesses()).containsExactly("id1", null);
1217         assertThat(putResult.getFailures()).isEmpty();
1218 
1219         GetByDocumentIdRequest getByDocumentIdRequest =
1220                 new GetByDocumentIdRequest.Builder("namespace").addIds("id1").build();
1221         List<GenericDocument> outDocuments = doGet(mDb1, getByDocumentIdRequest);
1222         assertThat(outDocuments).hasSize(1);
1223         assertThat(outDocuments).containsExactly(email);
1224     }
1225 
1226     @Test
testSetSchema_publiclyVisible_unsupported()1227     public void testSetSchema_publiclyVisible_unsupported() {
1228         assumeFalse(
1229                 mDb1.getFeatures()
1230                         .isFeatureSupported(Features.SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE));
1231 
1232         SetSchemaRequest request =
1233                 new SetSchemaRequest.Builder()
1234                         .addSchemas(new AppSearchSchema.Builder("Email").build())
1235                         .setPubliclyVisibleSchema(
1236                                 "Email",
1237                                 new PackageIdentifier(mContext.getPackageName(), new byte[32]))
1238                         .build();
1239         Exception e =
1240                 assertThrows(
1241                         UnsupportedOperationException.class,
1242                         () -> mDb1.setSchemaAsync(request).get());
1243         assertThat(e.getMessage())
1244                 .isEqualTo(
1245                         "Publicly visible schema are not supported on this "
1246                                 + "AppSearch implementation.");
1247     }
1248 
1249     @Test
testSetSchema_visibleToConfig()1250     public void testSetSchema_visibleToConfig() throws Exception {
1251         assumeTrue(
1252                 mDb1.getFeatures()
1253                         .isFeatureSupported(
1254                                 Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG));
1255         byte[] cert1 = new byte[32];
1256         byte[] cert2 = new byte[32];
1257         Arrays.fill(cert1, (byte) 1);
1258         Arrays.fill(cert2, (byte) 2);
1259         PackageIdentifier pkg1 = new PackageIdentifier("package1", cert1);
1260         PackageIdentifier pkg2 = new PackageIdentifier("package2", cert2);
1261         SchemaVisibilityConfig config1 =
1262                 new SchemaVisibilityConfig.Builder()
1263                         .setPubliclyVisibleTargetPackage(pkg1)
1264                         .addRequiredPermissions(ImmutableSet.of(1, 2))
1265                         .build();
1266         SchemaVisibilityConfig config2 =
1267                 new SchemaVisibilityConfig.Builder()
1268                         .setPubliclyVisibleTargetPackage(pkg2)
1269                         .addRequiredPermissions(ImmutableSet.of(3, 4))
1270                         .build();
1271         SetSchemaRequest request =
1272                 new SetSchemaRequest.Builder()
1273                         .addSchemas(AppSearchEmail.SCHEMA)
1274                         .addSchemaTypeVisibleToConfig("builtin:Email", config1)
1275                         .addSchemaTypeVisibleToConfig("builtin:Email", config2)
1276                         .build();
1277         mDb1.setSchemaAsync(request).get();
1278 
1279         GetSchemaResponse getSchemaResponse = mDb1.getSchemaAsync().get();
1280         assertThat(getSchemaResponse.getSchemas()).containsExactly(AppSearchEmail.SCHEMA);
1281         assertThat(getSchemaResponse.getSchemaTypesVisibleToConfigs())
1282                 .isEqualTo(ImmutableMap.of("builtin:Email", ImmutableSet.of(config1, config2)));
1283     }
1284 
1285     @Test
testSetSchema_visibleToConfig_unsupported()1286     public void testSetSchema_visibleToConfig_unsupported() {
1287         assumeFalse(
1288                 mDb1.getFeatures()
1289                         .isFeatureSupported(
1290                                 Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG));
1291 
1292         SchemaVisibilityConfig config =
1293                 new SchemaVisibilityConfig.Builder()
1294                         .addRequiredPermissions(ImmutableSet.of(1, 2))
1295                         .build();
1296         SetSchemaRequest request =
1297                 new SetSchemaRequest.Builder()
1298                         .addSchemas(new AppSearchSchema.Builder("Email").build())
1299                         .addSchemaTypeVisibleToConfig("Email", config)
1300                         .build();
1301         Exception e =
1302                 assertThrows(
1303                         UnsupportedOperationException.class,
1304                         () -> mDb1.setSchemaAsync(request).get());
1305         assertThat(e.getMessage())
1306                 .isEqualTo(
1307                         "Schema visible to config are not supported on"
1308                                 + " this AppSearch implementation.");
1309     }
1310 
1311     @Test
testGetSchema_longPropertyIndexingType()1312     public void testGetSchema_longPropertyIndexingType() throws Exception {
1313         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.NUMERIC_SEARCH));
1314         AppSearchSchema inSchema =
1315                 new AppSearchSchema.Builder("Test")
1316                         .addProperty(
1317                                 new LongPropertyConfig.Builder("indexableLong")
1318                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
1319                                         .setIndexingType(LongPropertyConfig.INDEXING_TYPE_RANGE)
1320                                         .build())
1321                         .addProperty(
1322                                 new LongPropertyConfig.Builder("long")
1323                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
1324                                         .setIndexingType(LongPropertyConfig.INDEXING_TYPE_NONE)
1325                                         .build())
1326                         .build();
1327 
1328         SetSchemaRequest request = new SetSchemaRequest.Builder().addSchemas(inSchema).build();
1329 
1330         mDb1.setSchemaAsync(request).get();
1331 
1332         Set<AppSearchSchema> actual = mDb1.getSchemaAsync().get().getSchemas();
1333         assertThat(actual).hasSize(1);
1334         assertThat(actual).containsExactlyElementsIn(request.getSchemas());
1335     }
1336 
1337     @Test
testGetSchema_longPropertyIndexingTypeNone_succeeds()1338     public void testGetSchema_longPropertyIndexingTypeNone_succeeds() throws Exception {
1339         AppSearchSchema inSchema =
1340                 new AppSearchSchema.Builder("Test")
1341                         .addProperty(
1342                                 new LongPropertyConfig.Builder("long")
1343                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
1344                                         .setIndexingType(LongPropertyConfig.INDEXING_TYPE_NONE)
1345                                         .build())
1346                         .build();
1347 
1348         SetSchemaRequest request = new SetSchemaRequest.Builder().addSchemas(inSchema).build();
1349 
1350         mDb1.setSchemaAsync(request).get();
1351 
1352         Set<AppSearchSchema> actual = mDb1.getSchemaAsync().get().getSchemas();
1353         assertThat(actual).hasSize(1);
1354         assertThat(actual).containsExactlyElementsIn(request.getSchemas());
1355     }
1356 
1357     @Test
testGetSchema_longPropertyIndexingTypeRange_notSupported()1358     public void testGetSchema_longPropertyIndexingTypeRange_notSupported() throws Exception {
1359         assumeFalse(mDb1.getFeatures().isFeatureSupported(Features.NUMERIC_SEARCH));
1360         AppSearchSchema inSchema =
1361                 new AppSearchSchema.Builder("Test")
1362                         .addProperty(
1363                                 new LongPropertyConfig.Builder("indexableLong")
1364                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
1365                                         .setIndexingType(LongPropertyConfig.INDEXING_TYPE_RANGE)
1366                                         .build())
1367                         .build();
1368 
1369         SetSchemaRequest request = new SetSchemaRequest.Builder().addSchemas(inSchema).build();
1370 
1371         UnsupportedOperationException e =
1372                 assertThrows(
1373                         UnsupportedOperationException.class,
1374                         () -> mDb1.setSchemaAsync(request).get());
1375         assertThat(e.getMessage())
1376                 .isEqualTo(
1377                         "LongProperty.INDEXING_TYPE_RANGE is not "
1378                                 + "supported on this AppSearch implementation.");
1379     }
1380 
1381     @Test
testGetSchema_joinableValueType()1382     public void testGetSchema_joinableValueType() throws Exception {
1383         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
1384         AppSearchSchema inSchema =
1385                 new AppSearchSchema.Builder("Test")
1386                         .addProperty(
1387                                 new StringPropertyConfig.Builder("normalStr")
1388                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
1389                                         .build())
1390                         .addProperty(
1391                                 new StringPropertyConfig.Builder("optionalQualifiedIdStr")
1392                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
1393                                         .setJoinableValueType(
1394                                                 StringPropertyConfig
1395                                                         .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
1396                                         .build())
1397                         .addProperty(
1398                                 new StringPropertyConfig.Builder("requiredQualifiedIdStr")
1399                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
1400                                         .setJoinableValueType(
1401                                                 StringPropertyConfig
1402                                                         .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
1403                                         .build())
1404                         .build();
1405 
1406         SetSchemaRequest request = new SetSchemaRequest.Builder().addSchemas(inSchema).build();
1407 
1408         mDb1.setSchemaAsync(request).get();
1409 
1410         Set<AppSearchSchema> actual = mDb1.getSchemaAsync().get().getSchemas();
1411         assertThat(actual).hasSize(1);
1412         assertThat(actual).containsExactlyElementsIn(request.getSchemas());
1413     }
1414 
1415     @Test
testGetSchema_joinableValueTypeNone_succeeds()1416     public void testGetSchema_joinableValueTypeNone_succeeds() throws Exception {
1417         AppSearchSchema inSchema =
1418                 new AppSearchSchema.Builder("Test")
1419                         .addProperty(
1420                                 new StringPropertyConfig.Builder("optionalString")
1421                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
1422                                         .setJoinableValueType(
1423                                                 StringPropertyConfig.JOINABLE_VALUE_TYPE_NONE)
1424                                         .build())
1425                         .addProperty(
1426                                 new StringPropertyConfig.Builder("requiredString")
1427                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
1428                                         .setJoinableValueType(
1429                                                 StringPropertyConfig.JOINABLE_VALUE_TYPE_NONE)
1430                                         .build())
1431                         .addProperty(
1432                                 new StringPropertyConfig.Builder("repeatedString")
1433                                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
1434                                         .setJoinableValueType(
1435                                                 StringPropertyConfig.JOINABLE_VALUE_TYPE_NONE)
1436                                         .build())
1437                         .build();
1438 
1439         SetSchemaRequest request = new SetSchemaRequest.Builder().addSchemas(inSchema).build();
1440 
1441         mDb1.setSchemaAsync(request).get();
1442 
1443         Set<AppSearchSchema> actual = mDb1.getSchemaAsync().get().getSchemas();
1444         assertThat(actual).hasSize(1);
1445         assertThat(actual).containsExactlyElementsIn(request.getSchemas());
1446     }
1447 
1448     @Test
testGetSchema_joinableValueTypeQualifiedId_notSupported()1449     public void testGetSchema_joinableValueTypeQualifiedId_notSupported() throws Exception {
1450         assumeFalse(mDb1.getFeatures().isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
1451         AppSearchSchema inSchema =
1452                 new AppSearchSchema.Builder("Test")
1453                         .addProperty(
1454                                 new StringPropertyConfig.Builder("qualifiedId")
1455                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
1456                                         .setJoinableValueType(
1457                                                 StringPropertyConfig
1458                                                         .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
1459                                         .build())
1460                         .build();
1461 
1462         SetSchemaRequest request = new SetSchemaRequest.Builder().addSchemas(inSchema).build();
1463 
1464         UnsupportedOperationException e =
1465                 assertThrows(
1466                         UnsupportedOperationException.class,
1467                         () -> mDb1.setSchemaAsync(request).get());
1468         assertThat(e.getMessage())
1469                 .isEqualTo(
1470                         "StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID is not supported on"
1471                             + " this AppSearch implementation.");
1472     }
1473 
1474     @Test
1475     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_DELETE_PROPAGATION_TYPE)
testGetSchema_deletePropagationTypePropagateFrom()1476     public void testGetSchema_deletePropagationTypePropagateFrom() throws Exception {
1477         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
1478         assumeTrue(
1479                 mDb1.getFeatures()
1480                         .isFeatureSupported(
1481                                 Features
1482                                         .SCHEMA_STRING_PROPERTY_CONFIG_DELETE_PROPAGATION_TYPE_PROPAGATE_FROM));
1483 
1484         AppSearchSchema inSchema =
1485                 new AppSearchSchema.Builder("Test")
1486                         .addProperty(
1487                                 new StringPropertyConfig.Builder("normalStr")
1488                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
1489                                         .build())
1490                         .addProperty(
1491                                 new StringPropertyConfig.Builder("qualifiedId")
1492                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
1493                                         .setJoinableValueType(
1494                                                 StringPropertyConfig
1495                                                         .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
1496                                         .setDeletePropagationType(
1497                                                 StringPropertyConfig
1498                                                         .DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
1499                                         .build())
1500                         .build();
1501 
1502         SetSchemaRequest request = new SetSchemaRequest.Builder().addSchemas(inSchema).build();
1503 
1504         mDb1.setSchemaAsync(request).get();
1505 
1506         Set<AppSearchSchema> actual = mDb1.getSchemaAsync().get().getSchemas();
1507         assertThat(actual).hasSize(1);
1508         assertThat(actual).containsExactlyElementsIn(request.getSchemas());
1509     }
1510 
1511     @Test
1512     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_DELETE_PROPAGATION_TYPE)
testGetSchema_deletePropagationTypeNoneWithNonJoinable_succeeds()1513     public void testGetSchema_deletePropagationTypeNoneWithNonJoinable_succeeds() throws Exception {
1514         AppSearchSchema inSchema =
1515                 new AppSearchSchema.Builder("Test")
1516                         .addProperty(
1517                                 new StringPropertyConfig.Builder("optionalString")
1518                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
1519                                         .setJoinableValueType(
1520                                                 StringPropertyConfig.JOINABLE_VALUE_TYPE_NONE)
1521                                         .setDeletePropagationType(
1522                                                 StringPropertyConfig.DELETE_PROPAGATION_TYPE_NONE)
1523                                         .build())
1524                         .addProperty(
1525                                 new StringPropertyConfig.Builder("requiredString")
1526                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
1527                                         .setJoinableValueType(
1528                                                 StringPropertyConfig.JOINABLE_VALUE_TYPE_NONE)
1529                                         .setDeletePropagationType(
1530                                                 StringPropertyConfig.DELETE_PROPAGATION_TYPE_NONE)
1531                                         .build())
1532                         .addProperty(
1533                                 new StringPropertyConfig.Builder("repeatedString")
1534                                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
1535                                         .setJoinableValueType(
1536                                                 StringPropertyConfig.JOINABLE_VALUE_TYPE_NONE)
1537                                         .setDeletePropagationType(
1538                                                 StringPropertyConfig.DELETE_PROPAGATION_TYPE_NONE)
1539                                         .build())
1540                         .build();
1541 
1542         SetSchemaRequest request = new SetSchemaRequest.Builder().addSchemas(inSchema).build();
1543 
1544         mDb1.setSchemaAsync(request).get();
1545 
1546         Set<AppSearchSchema> actual = mDb1.getSchemaAsync().get().getSchemas();
1547         assertThat(actual).hasSize(1);
1548         assertThat(actual).containsExactlyElementsIn(request.getSchemas());
1549     }
1550 
1551     @Test
1552     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_DELETE_PROPAGATION_TYPE)
testGetSchema_deletePropagationTypeNoneWithJoinable_succeeds()1553     public void testGetSchema_deletePropagationTypeNoneWithJoinable_succeeds() throws Exception {
1554         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
1555 
1556         AppSearchSchema inSchema =
1557                 new AppSearchSchema.Builder("Test")
1558                         .addProperty(
1559                                 new StringPropertyConfig.Builder("optionalString")
1560                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
1561                                         .setJoinableValueType(
1562                                                 StringPropertyConfig
1563                                                         .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
1564                                         .setDeletePropagationType(
1565                                                 StringPropertyConfig.DELETE_PROPAGATION_TYPE_NONE)
1566                                         .build())
1567                         .addProperty(
1568                                 new StringPropertyConfig.Builder("requiredString")
1569                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
1570                                         .setJoinableValueType(
1571                                                 StringPropertyConfig
1572                                                         .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
1573                                         .setDeletePropagationType(
1574                                                 StringPropertyConfig.DELETE_PROPAGATION_TYPE_NONE)
1575                                         .build())
1576                         .build();
1577 
1578         SetSchemaRequest request = new SetSchemaRequest.Builder().addSchemas(inSchema).build();
1579 
1580         mDb1.setSchemaAsync(request).get();
1581 
1582         Set<AppSearchSchema> actual = mDb1.getSchemaAsync().get().getSchemas();
1583         assertThat(actual).hasSize(1);
1584         assertThat(actual).containsExactlyElementsIn(request.getSchemas());
1585     }
1586 
1587     @Test
1588     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_DELETE_PROPAGATION_TYPE)
testGetSchema_deletePropagationTypePropagateFrom_notSupported()1589     public void testGetSchema_deletePropagationTypePropagateFrom_notSupported() throws Exception {
1590         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
1591         assumeFalse(
1592                 mDb1.getFeatures()
1593                         .isFeatureSupported(
1594                                 Features
1595                                         .SCHEMA_STRING_PROPERTY_CONFIG_DELETE_PROPAGATION_TYPE_PROPAGATE_FROM));
1596 
1597         AppSearchSchema inSchema =
1598                 new AppSearchSchema.Builder("Test")
1599                         .addProperty(
1600                                 new StringPropertyConfig.Builder("qualifiedId")
1601                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
1602                                         .setJoinableValueType(
1603                                                 StringPropertyConfig
1604                                                         .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
1605                                         .setDeletePropagationType(
1606                                                 StringPropertyConfig
1607                                                         .DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
1608                                         .build())
1609                         .build();
1610 
1611         SetSchemaRequest request = new SetSchemaRequest.Builder().addSchemas(inSchema).build();
1612 
1613         UnsupportedOperationException e =
1614                 assertThrows(
1615                         UnsupportedOperationException.class,
1616                         () -> mDb1.setSchemaAsync(request).get());
1617         assertThat(e.getMessage())
1618                 .isEqualTo(
1619                         "StringPropertyConfig.DELETE_PROPAGATION_TYPE_PROPAGATE_FROM is not"
1620                             + " supported on this AppSearch implementation.");
1621     }
1622 
1623     @Test
testGetNamespaces()1624     public void testGetNamespaces() throws Exception {
1625         // Schema registration
1626         mDb1.setSchemaAsync(
1627                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
1628                 .get();
1629         assertThat(mDb1.getNamespacesAsync().get()).isEmpty();
1630 
1631         // Index a document
1632         checkIsBatchResultSuccess(
1633                 mDb1.putAsync(
1634                         new PutDocumentsRequest.Builder()
1635                                 .addGenericDocuments(
1636                                         new AppSearchEmail.Builder("namespace1", "id1").build())
1637                                 .build()));
1638         assertThat(mDb1.getNamespacesAsync().get()).containsExactly("namespace1");
1639 
1640         // Index additional data
1641         checkIsBatchResultSuccess(
1642                 mDb1.putAsync(
1643                         new PutDocumentsRequest.Builder()
1644                                 .addGenericDocuments(
1645                                         new AppSearchEmail.Builder("namespace2", "id1").build(),
1646                                         new AppSearchEmail.Builder("namespace2", "id2").build(),
1647                                         new AppSearchEmail.Builder("namespace3", "id1").build())
1648                                 .build()));
1649         assertThat(mDb1.getNamespacesAsync().get())
1650                 .containsExactly("namespace1", "namespace2", "namespace3");
1651 
1652         // Remove namespace2/id2 -- namespace2 should still exist because of namespace2/id1
1653         checkIsBatchResultSuccess(
1654                 mDb1.removeAsync(
1655                         new RemoveByDocumentIdRequest.Builder("namespace2").addIds("id2").build()));
1656         assertThat(mDb1.getNamespacesAsync().get())
1657                 .containsExactly("namespace1", "namespace2", "namespace3");
1658 
1659         // Remove namespace2/id1 -- namespace2 should now be gone
1660         checkIsBatchResultSuccess(
1661                 mDb1.removeAsync(
1662                         new RemoveByDocumentIdRequest.Builder("namespace2").addIds("id1").build()));
1663         assertThat(mDb1.getNamespacesAsync().get()).containsExactly("namespace1", "namespace3");
1664 
1665         // Make sure the list of namespaces is preserved after restart
1666         mDb1.close();
1667         mDb1 = createSearchSessionAsync(DB_NAME_1).get();
1668         assertThat(mDb1.getNamespacesAsync().get()).containsExactly("namespace1", "namespace3");
1669     }
1670 
1671     @Test
testGetNamespaces_dbIsolation()1672     public void testGetNamespaces_dbIsolation() throws Exception {
1673         // Schema registration
1674         mDb1.setSchemaAsync(
1675                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
1676                 .get();
1677         mDb2.setSchemaAsync(
1678                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
1679                 .get();
1680         assertThat(mDb1.getNamespacesAsync().get()).isEmpty();
1681         assertThat(mDb2.getNamespacesAsync().get()).isEmpty();
1682 
1683         // Index documents
1684         checkIsBatchResultSuccess(
1685                 mDb1.putAsync(
1686                         new PutDocumentsRequest.Builder()
1687                                 .addGenericDocuments(
1688                                         new AppSearchEmail.Builder("namespace1_db1", "id1").build())
1689                                 .build()));
1690         checkIsBatchResultSuccess(
1691                 mDb1.putAsync(
1692                         new PutDocumentsRequest.Builder()
1693                                 .addGenericDocuments(
1694                                         new AppSearchEmail.Builder("namespace2_db1", "id1").build())
1695                                 .build()));
1696         checkIsBatchResultSuccess(
1697                 mDb2.putAsync(
1698                         new PutDocumentsRequest.Builder()
1699                                 .addGenericDocuments(
1700                                         new AppSearchEmail.Builder("namespace_db2", "id1").build())
1701                                 .build()));
1702         assertThat(mDb1.getNamespacesAsync().get())
1703                 .containsExactly("namespace1_db1", "namespace2_db1");
1704         assertThat(mDb2.getNamespacesAsync().get()).containsExactly("namespace_db2");
1705 
1706         // Make sure the list of namespaces is preserved after restart
1707         mDb1.close();
1708         mDb1 = createSearchSessionAsync(DB_NAME_1).get();
1709         assertThat(mDb1.getNamespacesAsync().get())
1710                 .containsExactly("namespace1_db1", "namespace2_db1");
1711         assertThat(mDb2.getNamespacesAsync().get()).containsExactly("namespace_db2");
1712     }
1713 
1714     @Test
testGetSchema_emptyDB()1715     public void testGetSchema_emptyDB() throws Exception {
1716         GetSchemaResponse getSchemaResponse = mDb1.getSchemaAsync().get();
1717         assertThat(getSchemaResponse.getVersion()).isEqualTo(0);
1718     }
1719 
1720     @Test
testPutDocuments()1721     public void testPutDocuments() throws Exception {
1722         // Schema registration
1723         mDb1.setSchemaAsync(
1724                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
1725                 .get();
1726 
1727         // Index a document
1728         AppSearchEmail email =
1729                 new AppSearchEmail.Builder("namespace", "id1")
1730                         .setFrom("[email protected]")
1731                         .setTo("[email protected]", "[email protected]")
1732                         .setSubject("testPut example")
1733                         .setBody("This is the body of the testPut email")
1734                         .build();
1735 
1736         AppSearchBatchResult<String, Void> result =
1737                 checkIsBatchResultSuccess(
1738                         mDb1.putAsync(
1739                                 new PutDocumentsRequest.Builder()
1740                                         .addGenericDocuments(email)
1741                                         .build()));
1742         assertThat(result.getSuccesses()).containsExactly("id1", null);
1743         assertThat(result.getFailures()).isEmpty();
1744     }
1745 
1746     @Test
testPutDocuments_emptyProperties()1747     public void testPutDocuments_emptyProperties() throws Exception {
1748         // Schema registration. Due to b/204677124 is fixed in Android T. We have different
1749         // behaviour when set empty array to bytes and documents between local and platform storage.
1750         // This test only test String, long, boolean and double, for byte array and Document will be
1751         // test in backend's specific test.
1752         AppSearchSchema schema =
1753                 new AppSearchSchema.Builder("testSchema")
1754                         .addProperty(
1755                                 new StringPropertyConfig.Builder("string")
1756                                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
1757                                         .setIndexingType(
1758                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
1759                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
1760                                         .build())
1761                         .addProperty(
1762                                 new AppSearchSchema.LongPropertyConfig.Builder("long")
1763                                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
1764                                         .build())
1765                         .addProperty(
1766                                 new AppSearchSchema.DoublePropertyConfig.Builder("double")
1767                                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
1768                                         .build())
1769                         .addProperty(
1770                                 new AppSearchSchema.BooleanPropertyConfig.Builder("boolean")
1771                                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
1772                                         .build())
1773                         .build();
1774         mDb1.setSchemaAsync(
1775                         new SetSchemaRequest.Builder()
1776                                 .addSchemas(schema, AppSearchEmail.SCHEMA)
1777                                 .build())
1778                 .get();
1779 
1780         // Index a document
1781         GenericDocument document =
1782                 new GenericDocument.Builder<>("namespace", "id1", "testSchema")
1783                         .setPropertyBoolean("boolean")
1784                         .setPropertyString("string")
1785                         .setPropertyDouble("double")
1786                         .setPropertyLong("long")
1787                         .build();
1788 
1789         AppSearchBatchResult<String, Void> result =
1790                 checkIsBatchResultSuccess(
1791                         mDb1.putAsync(
1792                                 new PutDocumentsRequest.Builder()
1793                                         .addGenericDocuments(document)
1794                                         .build()));
1795         assertThat(result.getSuccesses()).containsExactly("id1", null);
1796         assertThat(result.getFailures()).isEmpty();
1797 
1798         GetByDocumentIdRequest request =
1799                 new GetByDocumentIdRequest.Builder("namespace").addIds("id1").build();
1800         List<GenericDocument> outDocuments = doGet(mDb1, request);
1801         assertThat(outDocuments).hasSize(1);
1802         GenericDocument outDocument = outDocuments.get(0);
1803         assertThat(outDocument.getPropertyBooleanArray("boolean")).isEmpty();
1804         assertThat(outDocument.getPropertyStringArray("string")).isEmpty();
1805         assertThat(outDocument.getPropertyDoubleArray("double")).isEmpty();
1806         assertThat(outDocument.getPropertyLongArray("long")).isEmpty();
1807     }
1808 
1809     @Test
testPutLargeDocumentBatch()1810     public void testPutLargeDocumentBatch() throws Exception {
1811         // Schema registration
1812         AppSearchSchema schema =
1813                 new AppSearchSchema.Builder("Type")
1814                         .addProperty(
1815                                 new StringPropertyConfig.Builder("body")
1816                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
1817                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
1818                                         .setIndexingType(
1819                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
1820                                         .build())
1821                         .build();
1822         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
1823 
1824         // Creates a large batch of Documents, since we have max document size in Framework which is
1825         // 512KiB, we will create 1KiB * 4000 docs = 4MiB total size > 1MiB binder transaction limit
1826         char[] chars = new char[1024]; // 1KiB
1827         Arrays.fill(chars, ' ');
1828         String body = String.valueOf(chars) + "the end.";
1829         List<GenericDocument> inDocuments = new ArrayList<>();
1830         GetByDocumentIdRequest.Builder getByDocumentIdRequestBuilder =
1831                 new GetByDocumentIdRequest.Builder("namespace");
1832         for (int i = 0; i < 4000; i++) {
1833             GenericDocument inDocument =
1834                     new GenericDocument.Builder<>("namespace", "id" + i, "Type")
1835                             .setPropertyString("body", body)
1836                             .build();
1837             inDocuments.add(inDocument);
1838             getByDocumentIdRequestBuilder.addIds("id" + i);
1839         }
1840 
1841         // Index documents.
1842         AppSearchBatchResult<String, Void> result =
1843                 mDb1.putAsync(
1844                                 new PutDocumentsRequest.Builder()
1845                                         .addGenericDocuments(inDocuments)
1846                                         .build())
1847                         .get();
1848         assertThat(result.isSuccess()).isTrue();
1849 
1850         // Query those documents and verify they are same with the input. This also verify
1851         // AppSearchResult could handle large batch.
1852         SearchResultsShim searchResults =
1853                 mDb1.search(
1854                         "end",
1855                         new SearchSpec.Builder()
1856                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
1857                                 .setResultCountPerPage(4000)
1858                                 .build());
1859         List<GenericDocument> outDocuments = convertSearchResultsToDocuments(searchResults);
1860 
1861         // Create a map to assert the output is same to the input in O(n).
1862         // containsExactlyElementsIn will create two iterators and the complexity is O(n^2).
1863         Map<String, GenericDocument> outMap = new ArrayMap<>(outDocuments.size());
1864         for (int i = 0; i < outDocuments.size(); i++) {
1865             outMap.put(outDocuments.get(i).getId(), outDocuments.get(i));
1866         }
1867         for (int i = 0; i < inDocuments.size(); i++) {
1868             GenericDocument inDocument = inDocuments.get(i);
1869             assertThat(inDocument).isEqualTo(outMap.get(inDocument.getId()));
1870             outMap.remove(inDocument.getId());
1871         }
1872         assertThat(outMap).isEmpty();
1873 
1874         // Get by document ID and verify they are same with the input. This also verify
1875         // AppSearchBatchResult could handle large batch.
1876         AppSearchBatchResult<String, GenericDocument> batchResult =
1877                 mDb1.getByDocumentIdAsync(getByDocumentIdRequestBuilder.build()).get();
1878         assertThat(batchResult.isSuccess()).isTrue();
1879         for (int i = 0; i < inDocuments.size(); i++) {
1880             GenericDocument inDocument = inDocuments.get(i);
1881             assertThat(batchResult.getSuccesses().get(inDocument.getId())).isEqualTo(inDocument);
1882         }
1883     }
1884 
1885     @Test
testPutDocuments_takenActionGenericDocuments()1886     public void testPutDocuments_takenActionGenericDocuments() throws Exception {
1887         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
1888 
1889         // Schema registration
1890         AppSearchSchema searchActionSchema =
1891                 new AppSearchSchema.Builder("builtin:SearchAction")
1892                         .addProperty(
1893                                 new LongPropertyConfig.Builder("actionType")
1894                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
1895                                         .build())
1896                         .addProperty(
1897                                 new StringPropertyConfig.Builder("query")
1898                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
1899                                         .setIndexingType(
1900                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
1901                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
1902                                         .build())
1903                         .build();
1904         AppSearchSchema clickActionSchema =
1905                 new AppSearchSchema.Builder("builtin:ClickAction")
1906                         .addProperty(
1907                                 new LongPropertyConfig.Builder("actionType")
1908                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
1909                                         .build())
1910                         .addProperty(
1911                                 new StringPropertyConfig.Builder("query")
1912                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
1913                                         .setIndexingType(
1914                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
1915                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
1916                                         .build())
1917                         .addProperty(
1918                                 new StringPropertyConfig.Builder("referencedQualifiedId")
1919                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
1920                                         .setJoinableValueType(
1921                                                 StringPropertyConfig
1922                                                         .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
1923                                         .build())
1924                         .build();
1925         AppSearchSchema impressionActionSchema =
1926                 new AppSearchSchema.Builder("builtin:ImpressionAction")
1927                         .addProperty(
1928                                 new LongPropertyConfig.Builder("actionType")
1929                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
1930                                         .build())
1931                         .addProperty(
1932                                 new StringPropertyConfig.Builder("query")
1933                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
1934                                         .setIndexingType(
1935                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
1936                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
1937                                         .build())
1938                         .addProperty(
1939                                 new StringPropertyConfig.Builder("referencedQualifiedId")
1940                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
1941                                         .setJoinableValueType(
1942                                                 StringPropertyConfig
1943                                                         .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
1944                                         .build())
1945                         .build();
1946         AppSearchSchema dismissActionSchema =
1947                 new AppSearchSchema.Builder("builtin:DismissAction")
1948                         .addProperty(
1949                                 new LongPropertyConfig.Builder("actionType")
1950                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
1951                                         .build())
1952                         .addProperty(
1953                                 new StringPropertyConfig.Builder("query")
1954                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
1955                                         .setIndexingType(
1956                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
1957                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
1958                                         .build())
1959                         .addProperty(
1960                                 new StringPropertyConfig.Builder("referencedQualifiedId")
1961                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
1962                                         .setJoinableValueType(
1963                                                 StringPropertyConfig
1964                                                         .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
1965                                         .build())
1966                         .build();
1967 
1968         mDb1.setSchemaAsync(
1969                         new SetSchemaRequest.Builder()
1970                                 .addSchemas(
1971                                         searchActionSchema,
1972                                         clickActionSchema,
1973                                         impressionActionSchema,
1974                                         dismissActionSchema)
1975                                 .build())
1976                 .get();
1977 
1978         // Put search action, click action and impression action generic documents.
1979         GenericDocument searchAction =
1980                 new GenericDocument.Builder<>("namespace", "search", "builtin:SearchAction")
1981                         .setCreationTimestampMillis(1000)
1982                         .setPropertyLong("actionType", ACTION_TYPE_SEARCH)
1983                         .setPropertyString("query", "body")
1984                         .build();
1985         GenericDocument clickAction =
1986                 new GenericDocument.Builder<>("namespace", "click", "builtin:ClickAction")
1987                         .setCreationTimestampMillis(2000)
1988                         .setPropertyLong("actionType", ACTION_TYPE_CLICK)
1989                         .setPropertyString("query", "body")
1990                         .setPropertyString("referencedQualifiedId", "pkg$db/ns#refId1")
1991                         .build();
1992         GenericDocument impressionAction =
1993                 new GenericDocument.Builder<>("namespace", "impression", "builtin:ImpressionAction")
1994                         .setCreationTimestampMillis(3000)
1995                         .setPropertyLong("actionType", ACTION_TYPE_IMPRESSION)
1996                         .setPropertyString("query", "body")
1997                         .setPropertyString("referencedQualifiedId", "pkg$db/ns#refId2")
1998                         .build();
1999         GenericDocument dismissAction =
2000                 new GenericDocument.Builder<>("namespace", "dismiss", "builtin:DismissAction")
2001                         .setCreationTimestampMillis(4000)
2002                         .setPropertyLong("actionType", ACTION_TYPE_DISMISS)
2003                         .setPropertyString("query", "body")
2004                         .setPropertyString("referencedQualifiedId", "pkg$db/ns#refId3")
2005                         .build();
2006 
2007         AppSearchBatchResult<String, Void> result =
2008                 checkIsBatchResultSuccess(
2009                         mDb1.putAsync(
2010                                 new PutDocumentsRequest.Builder()
2011                                         .addTakenActionGenericDocuments(
2012                                                 searchAction,
2013                                                 clickAction,
2014                                                 impressionAction,
2015                                                 dismissAction)
2016                                         .build()));
2017         assertThat(result.getSuccesses()).containsEntry("search", null);
2018         assertThat(result.getSuccesses()).containsEntry("click", null);
2019         assertThat(result.getSuccesses()).containsEntry("impression", null);
2020         assertThat(result.getSuccesses()).containsEntry("dismiss", null);
2021         assertThat(result.getFailures()).isEmpty();
2022     }
2023 
2024     @Test
testUpdateSchema()2025     public void testUpdateSchema() throws Exception {
2026         // Schema registration
2027         AppSearchSchema oldEmailSchema =
2028                 new AppSearchSchema.Builder(AppSearchEmail.SCHEMA_TYPE)
2029                         .addProperty(
2030                                 new StringPropertyConfig.Builder("subject")
2031                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
2032                                         .setIndexingType(
2033                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
2034                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
2035                                         .build())
2036                         .build();
2037         AppSearchSchema newEmailSchema =
2038                 new AppSearchSchema.Builder(AppSearchEmail.SCHEMA_TYPE)
2039                         .addProperty(
2040                                 new StringPropertyConfig.Builder("subject")
2041                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
2042                                         .setIndexingType(
2043                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
2044                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
2045                                         .build())
2046                         .addProperty(
2047                                 new StringPropertyConfig.Builder("body")
2048                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
2049                                         .setIndexingType(
2050                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
2051                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
2052                                         .build())
2053                         .build();
2054         AppSearchSchema giftSchema =
2055                 new AppSearchSchema.Builder("Gift")
2056                         .addProperty(
2057                                 new AppSearchSchema.LongPropertyConfig.Builder("price")
2058                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
2059                                         .build())
2060                         .build();
2061         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(oldEmailSchema).build())
2062                 .get();
2063 
2064         // Try to index a gift. This should fail as it's not in the schema.
2065         GenericDocument gift =
2066                 new GenericDocument.Builder<>("namespace", "gift1", "Gift")
2067                         .setPropertyLong("price", 5)
2068                         .build();
2069         AppSearchBatchResult<String, Void> result =
2070                 mDb1.putAsync(new PutDocumentsRequest.Builder().addGenericDocuments(gift).build())
2071                         .get();
2072         assertThat(result.isSuccess()).isFalse();
2073         assertThat(result.getFailures().get("gift1").getResultCode())
2074                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
2075 
2076         // Update the schema to include the gift and update email with a new field
2077         mDb1.setSchemaAsync(
2078                         new SetSchemaRequest.Builder()
2079                                 .addSchemas(newEmailSchema, giftSchema)
2080                                 .build())
2081                 .get();
2082 
2083         // Try to index the document again, which should now work
2084         checkIsBatchResultSuccess(
2085                 mDb1.putAsync(new PutDocumentsRequest.Builder().addGenericDocuments(gift).build()));
2086 
2087         // Indexing an email with a body should also work
2088         AppSearchEmail email =
2089                 new AppSearchEmail.Builder("namespace", "email1")
2090                         .setSubject("testPut example")
2091                         .setBody("This is the body of the testPut email")
2092                         .build();
2093         checkIsBatchResultSuccess(
2094                 mDb1.putAsync(
2095                         new PutDocumentsRequest.Builder().addGenericDocuments(email).build()));
2096     }
2097 
2098     @Test
testRemoveSchema()2099     public void testRemoveSchema() throws Exception {
2100         // Schema registration
2101         AppSearchSchema emailSchema =
2102                 new AppSearchSchema.Builder(AppSearchEmail.SCHEMA_TYPE)
2103                         .addProperty(
2104                                 new StringPropertyConfig.Builder("subject")
2105                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
2106                                         .setIndexingType(
2107                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
2108                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
2109                                         .build())
2110                         .build();
2111         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(emailSchema).build()).get();
2112 
2113         // Index an email and check it present.
2114         AppSearchEmail email =
2115                 new AppSearchEmail.Builder("namespace", "email1")
2116                         .setSubject("testPut example")
2117                         .build();
2118         checkIsBatchResultSuccess(
2119                 mDb1.putAsync(
2120                         new PutDocumentsRequest.Builder().addGenericDocuments(email).build()));
2121         List<GenericDocument> outDocuments = doGet(mDb1, "namespace", "email1");
2122         assertThat(outDocuments).hasSize(1);
2123         AppSearchEmail outEmail = new AppSearchEmail(outDocuments.get(0));
2124         assertThat(outEmail).isEqualTo(email);
2125 
2126         // Try to remove the email schema. This should fail as it's an incompatible change.
2127         SetSchemaRequest setSchemaRequest = new SetSchemaRequest.Builder().build();
2128         ExecutionException executionException =
2129                 assertThrows(
2130                         ExecutionException.class,
2131                         () -> mDb1.setSchemaAsync(setSchemaRequest).get());
2132         assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
2133         AppSearchException failResult1 = (AppSearchException) executionException.getCause();
2134         assertThat(failResult1).hasMessageThat().contains("Schema is incompatible");
2135         assertThat(failResult1).hasMessageThat().contains("Deleted types: {builtin:Email}");
2136 
2137         // Try to remove the email schema again, which should now work as we set forceOverride to
2138         // be true.
2139         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
2140 
2141         // Make sure the indexed email is gone.
2142         AppSearchBatchResult<String, GenericDocument> getResult =
2143                 mDb1.getByDocumentIdAsync(
2144                                 new GetByDocumentIdRequest.Builder("namespace")
2145                                         .addIds("email1")
2146                                         .build())
2147                         .get();
2148         assertThat(getResult.isSuccess()).isFalse();
2149         assertThat(getResult.getFailures().get("email1").getResultCode())
2150                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
2151 
2152         // Try to index an email again. This should fail as the schema has been removed.
2153         AppSearchEmail email2 =
2154                 new AppSearchEmail.Builder("namespace", "email2")
2155                         .setSubject("testPut example")
2156                         .build();
2157         AppSearchBatchResult<String, Void> failResult2 =
2158                 mDb1.putAsync(new PutDocumentsRequest.Builder().addGenericDocuments(email2).build())
2159                         .get();
2160         assertThat(failResult2.isSuccess()).isFalse();
2161         assertThat(failResult2.getFailures().get("email2").getErrorMessage())
2162                 .isEqualTo(
2163                         "Schema type config '"
2164                                 + mContext.getPackageName()
2165                                 + "$"
2166                                 + DB_NAME_1
2167                                 + "/builtin:Email' not found");
2168     }
2169 
2170     @Test
testRemoveSchema_twoDatabases()2171     public void testRemoveSchema_twoDatabases() throws Exception {
2172         // Schema registration in mDb1 and mDb2
2173         AppSearchSchema emailSchema =
2174                 new AppSearchSchema.Builder(AppSearchEmail.SCHEMA_TYPE)
2175                         .addProperty(
2176                                 new StringPropertyConfig.Builder("subject")
2177                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
2178                                         .setIndexingType(
2179                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
2180                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
2181                                         .build())
2182                         .build();
2183         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(emailSchema).build()).get();
2184         mDb2.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(emailSchema).build()).get();
2185 
2186         // Index an email and check it present in database1.
2187         AppSearchEmail email1 =
2188                 new AppSearchEmail.Builder("namespace", "email1")
2189                         .setSubject("testPut example")
2190                         .build();
2191         checkIsBatchResultSuccess(
2192                 mDb1.putAsync(
2193                         new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
2194         List<GenericDocument> outDocuments = doGet(mDb1, "namespace", "email1");
2195         assertThat(outDocuments).hasSize(1);
2196         AppSearchEmail outEmail = new AppSearchEmail(outDocuments.get(0));
2197         assertThat(outEmail).isEqualTo(email1);
2198 
2199         // Index an email and check it present in database2.
2200         AppSearchEmail email2 =
2201                 new AppSearchEmail.Builder("namespace", "email2")
2202                         .setSubject("testPut example")
2203                         .build();
2204         checkIsBatchResultSuccess(
2205                 mDb2.putAsync(
2206                         new PutDocumentsRequest.Builder().addGenericDocuments(email2).build()));
2207         outDocuments = doGet(mDb2, "namespace", "email2");
2208         assertThat(outDocuments).hasSize(1);
2209         outEmail = new AppSearchEmail(outDocuments.get(0));
2210         assertThat(outEmail).isEqualTo(email2);
2211 
2212         // Try to remove the email schema in database1. This should fail as it's an incompatible
2213         // change.
2214         SetSchemaRequest setSchemaRequest = new SetSchemaRequest.Builder().build();
2215         ExecutionException executionException =
2216                 assertThrows(
2217                         ExecutionException.class,
2218                         () -> mDb1.setSchemaAsync(setSchemaRequest).get());
2219         assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
2220         AppSearchException failResult1 = (AppSearchException) executionException.getCause();
2221         assertThat(failResult1).hasMessageThat().contains("Schema is incompatible");
2222         assertThat(failResult1).hasMessageThat().contains("Deleted types: {builtin:Email}");
2223 
2224         // Try to remove the email schema again, which should now work as we set forceOverride to
2225         // be true.
2226         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
2227 
2228         // Make sure the indexed email is gone in database 1.
2229         AppSearchBatchResult<String, GenericDocument> getResult =
2230                 mDb1.getByDocumentIdAsync(
2231                                 new GetByDocumentIdRequest.Builder("namespace")
2232                                         .addIds("email1")
2233                                         .build())
2234                         .get();
2235         assertThat(getResult.isSuccess()).isFalse();
2236         assertThat(getResult.getFailures().get("email1").getResultCode())
2237                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
2238 
2239         // Try to index an email again. This should fail as the schema has been removed.
2240         AppSearchEmail email3 =
2241                 new AppSearchEmail.Builder("namespace", "email3")
2242                         .setSubject("testPut example")
2243                         .build();
2244         AppSearchBatchResult<String, Void> failResult2 =
2245                 mDb1.putAsync(new PutDocumentsRequest.Builder().addGenericDocuments(email3).build())
2246                         .get();
2247         assertThat(failResult2.isSuccess()).isFalse();
2248         assertThat(failResult2.getFailures().get("email3").getErrorMessage())
2249                 .isEqualTo(
2250                         "Schema type config '"
2251                                 + mContext.getPackageName()
2252                                 + "$"
2253                                 + DB_NAME_1
2254                                 + "/builtin:Email' not found");
2255 
2256         // Make sure email in database 2 still present.
2257         outDocuments = doGet(mDb2, "namespace", "email2");
2258         assertThat(outDocuments).hasSize(1);
2259         outEmail = new AppSearchEmail(outDocuments.get(0));
2260         assertThat(outEmail).isEqualTo(email2);
2261 
2262         // Make sure email could still be indexed in database 2.
2263         checkIsBatchResultSuccess(
2264                 mDb2.putAsync(
2265                         new PutDocumentsRequest.Builder().addGenericDocuments(email2).build()));
2266     }
2267 
2268     @Test
testGetDocuments()2269     public void testGetDocuments() throws Exception {
2270         // Schema registration
2271         mDb1.setSchemaAsync(
2272                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
2273                 .get();
2274 
2275         // Index a document
2276         AppSearchEmail inEmail =
2277                 new AppSearchEmail.Builder("namespace", "id1")
2278                         .setFrom("[email protected]")
2279                         .setTo("[email protected]", "[email protected]")
2280                         .setSubject("testPut example")
2281                         .setBody("This is the body of the testPut email")
2282                         .build();
2283         checkIsBatchResultSuccess(
2284                 mDb1.putAsync(
2285                         new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
2286 
2287         // Get the document
2288         List<GenericDocument> outDocuments = doGet(mDb1, "namespace", "id1");
2289         assertThat(outDocuments).hasSize(1);
2290         AppSearchEmail outEmail = new AppSearchEmail(outDocuments.get(0));
2291         assertThat(outEmail).isEqualTo(inEmail);
2292 
2293         // Can't get the document in the other instance.
2294         AppSearchBatchResult<String, GenericDocument> failResult =
2295                 mDb2.getByDocumentIdAsync(
2296                                 new GetByDocumentIdRequest.Builder("namespace")
2297                                         .addIds("id1")
2298                                         .build())
2299                         .get();
2300         assertThat(failResult.isSuccess()).isFalse();
2301         assertThat(failResult.getFailures().get("id1").getResultCode())
2302                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
2303     }
2304 
2305     @Test
testGetDocuments_projection()2306     public void testGetDocuments_projection() throws Exception {
2307         // Schema registration
2308         mDb1.setSchemaAsync(
2309                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
2310                 .get();
2311 
2312         // Index two documents
2313         AppSearchEmail email1 =
2314                 new AppSearchEmail.Builder("namespace", "id1")
2315                         .setCreationTimestampMillis(1000)
2316                         .setFrom("[email protected]")
2317                         .setTo("[email protected]", "[email protected]")
2318                         .setSubject("testPut example")
2319                         .setBody("This is the body of the testPut email")
2320                         .build();
2321         AppSearchEmail email2 =
2322                 new AppSearchEmail.Builder("namespace", "id2")
2323                         .setCreationTimestampMillis(1000)
2324                         .setFrom("[email protected]")
2325                         .setTo("[email protected]", "[email protected]")
2326                         .setSubject("testPut example")
2327                         .setBody("This is the body of the testPut email")
2328                         .build();
2329         checkIsBatchResultSuccess(
2330                 mDb1.putAsync(
2331                         new PutDocumentsRequest.Builder()
2332                                 .addGenericDocuments(email1, email2)
2333                                 .build()));
2334 
2335         // Get with type property paths {"Email", ["subject", "to"]}
2336         GetByDocumentIdRequest request =
2337                 new GetByDocumentIdRequest.Builder("namespace")
2338                         .addIds("id1", "id2")
2339                         .addProjection(
2340                                 AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("subject", "to"))
2341                         .build();
2342         List<GenericDocument> outDocuments = doGet(mDb1, request);
2343 
2344         // The two email documents should have been returned with only the "subject" and "to"
2345         // properties.
2346         AppSearchEmail expected1 =
2347                 new AppSearchEmail.Builder("namespace", "id2")
2348                         .setCreationTimestampMillis(1000)
2349                         .setTo("[email protected]", "[email protected]")
2350                         .setSubject("testPut example")
2351                         .build();
2352         AppSearchEmail expected2 =
2353                 new AppSearchEmail.Builder("namespace", "id1")
2354                         .setCreationTimestampMillis(1000)
2355                         .setTo("[email protected]", "[email protected]")
2356                         .setSubject("testPut example")
2357                         .build();
2358         assertThat(outDocuments).containsExactly(expected1, expected2);
2359     }
2360 
2361     @Test
testGetDocuments_projectionEmpty()2362     public void testGetDocuments_projectionEmpty() throws Exception {
2363         // Schema registration
2364         mDb1.setSchemaAsync(
2365                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
2366                 .get();
2367 
2368         // Index two documents
2369         AppSearchEmail email1 =
2370                 new AppSearchEmail.Builder("namespace", "id1")
2371                         .setCreationTimestampMillis(1000)
2372                         .setFrom("[email protected]")
2373                         .setTo("[email protected]", "[email protected]")
2374                         .setSubject("testPut example")
2375                         .setBody("This is the body of the testPut email")
2376                         .build();
2377         AppSearchEmail email2 =
2378                 new AppSearchEmail.Builder("namespace", "id2")
2379                         .setCreationTimestampMillis(1000)
2380                         .setFrom("[email protected]")
2381                         .setTo("[email protected]", "[email protected]")
2382                         .setSubject("testPut example")
2383                         .setBody("This is the body of the testPut email")
2384                         .build();
2385         checkIsBatchResultSuccess(
2386                 mDb1.putAsync(
2387                         new PutDocumentsRequest.Builder()
2388                                 .addGenericDocuments(email1, email2)
2389                                 .build()));
2390 
2391         // Get with type property paths {"Email", ["subject", "to"]}
2392         GetByDocumentIdRequest request =
2393                 new GetByDocumentIdRequest.Builder("namespace")
2394                         .addIds("id1", "id2")
2395                         .addProjection(AppSearchEmail.SCHEMA_TYPE, Collections.emptyList())
2396                         .build();
2397         List<GenericDocument> outDocuments = doGet(mDb1, request);
2398 
2399         // The two email documents should have been returned without any properties.
2400         AppSearchEmail expected1 =
2401                 new AppSearchEmail.Builder("namespace", "id2")
2402                         .setCreationTimestampMillis(1000)
2403                         .build();
2404         AppSearchEmail expected2 =
2405                 new AppSearchEmail.Builder("namespace", "id1")
2406                         .setCreationTimestampMillis(1000)
2407                         .build();
2408         assertThat(outDocuments).containsExactly(expected1, expected2);
2409     }
2410 
2411     @Test
testGetDocuments_projectionNonExistentType()2412     public void testGetDocuments_projectionNonExistentType() throws Exception {
2413         // Schema registration
2414         mDb1.setSchemaAsync(
2415                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
2416                 .get();
2417 
2418         // Index two documents
2419         AppSearchEmail email1 =
2420                 new AppSearchEmail.Builder("namespace", "id1")
2421                         .setCreationTimestampMillis(1000)
2422                         .setFrom("[email protected]")
2423                         .setTo("[email protected]", "[email protected]")
2424                         .setSubject("testPut example")
2425                         .setBody("This is the body of the testPut email")
2426                         .build();
2427         AppSearchEmail email2 =
2428                 new AppSearchEmail.Builder("namespace", "id2")
2429                         .setCreationTimestampMillis(1000)
2430                         .setFrom("[email protected]")
2431                         .setTo("[email protected]", "[email protected]")
2432                         .setSubject("testPut example")
2433                         .setBody("This is the body of the testPut email")
2434                         .build();
2435         checkIsBatchResultSuccess(
2436                 mDb1.putAsync(
2437                         new PutDocumentsRequest.Builder()
2438                                 .addGenericDocuments(email1, email2)
2439                                 .build()));
2440 
2441         // Get with type property paths {"Email", ["subject", "to"]}
2442         GetByDocumentIdRequest request =
2443                 new GetByDocumentIdRequest.Builder("namespace")
2444                         .addIds("id1", "id2")
2445                         .addProjection("NonExistentType", Collections.emptyList())
2446                         .addProjection(
2447                                 AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("subject", "to"))
2448                         .build();
2449         List<GenericDocument> outDocuments = doGet(mDb1, request);
2450 
2451         // The two email documents should have been returned with only the "subject" and "to"
2452         // properties.
2453         AppSearchEmail expected1 =
2454                 new AppSearchEmail.Builder("namespace", "id2")
2455                         .setCreationTimestampMillis(1000)
2456                         .setTo("[email protected]", "[email protected]")
2457                         .setSubject("testPut example")
2458                         .build();
2459         AppSearchEmail expected2 =
2460                 new AppSearchEmail.Builder("namespace", "id1")
2461                         .setCreationTimestampMillis(1000)
2462                         .setTo("[email protected]", "[email protected]")
2463                         .setSubject("testPut example")
2464                         .build();
2465         assertThat(outDocuments).containsExactly(expected1, expected2);
2466     }
2467 
2468     @Test
testGetDocuments_wildcardProjection()2469     public void testGetDocuments_wildcardProjection() throws Exception {
2470         // Schema registration
2471         mDb1.setSchemaAsync(
2472                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
2473                 .get();
2474 
2475         // Index two documents
2476         AppSearchEmail email1 =
2477                 new AppSearchEmail.Builder("namespace", "id1")
2478                         .setCreationTimestampMillis(1000)
2479                         .setFrom("[email protected]")
2480                         .setTo("[email protected]", "[email protected]")
2481                         .setSubject("testPut example")
2482                         .setBody("This is the body of the testPut email")
2483                         .build();
2484         AppSearchEmail email2 =
2485                 new AppSearchEmail.Builder("namespace", "id2")
2486                         .setCreationTimestampMillis(1000)
2487                         .setFrom("[email protected]")
2488                         .setTo("[email protected]", "[email protected]")
2489                         .setSubject("testPut example")
2490                         .setBody("This is the body of the testPut email")
2491                         .build();
2492         checkIsBatchResultSuccess(
2493                 mDb1.putAsync(
2494                         new PutDocumentsRequest.Builder()
2495                                 .addGenericDocuments(email1, email2)
2496                                 .build()));
2497 
2498         // Get with type property paths {"Email", ["subject", "to"]}
2499         GetByDocumentIdRequest request =
2500                 new GetByDocumentIdRequest.Builder("namespace")
2501                         .addIds("id1", "id2")
2502                         .addProjection(
2503                                 GetByDocumentIdRequest.PROJECTION_SCHEMA_TYPE_WILDCARD,
2504                                 ImmutableList.of("subject", "to"))
2505                         .build();
2506         List<GenericDocument> outDocuments = doGet(mDb1, request);
2507 
2508         // The two email documents should have been returned with only the "subject" and "to"
2509         // properties.
2510         AppSearchEmail expected1 =
2511                 new AppSearchEmail.Builder("namespace", "id2")
2512                         .setCreationTimestampMillis(1000)
2513                         .setTo("[email protected]", "[email protected]")
2514                         .setSubject("testPut example")
2515                         .build();
2516         AppSearchEmail expected2 =
2517                 new AppSearchEmail.Builder("namespace", "id1")
2518                         .setCreationTimestampMillis(1000)
2519                         .setTo("[email protected]", "[email protected]")
2520                         .setSubject("testPut example")
2521                         .build();
2522         assertThat(outDocuments).containsExactly(expected1, expected2);
2523     }
2524 
2525     @Test
testGetDocuments_wildcardProjectionEmpty()2526     public void testGetDocuments_wildcardProjectionEmpty() throws Exception {
2527         // Schema registration
2528         mDb1.setSchemaAsync(
2529                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
2530                 .get();
2531 
2532         // Index two documents
2533         AppSearchEmail email1 =
2534                 new AppSearchEmail.Builder("namespace", "id1")
2535                         .setCreationTimestampMillis(1000)
2536                         .setFrom("[email protected]")
2537                         .setTo("[email protected]", "[email protected]")
2538                         .setSubject("testPut example")
2539                         .setBody("This is the body of the testPut email")
2540                         .build();
2541         AppSearchEmail email2 =
2542                 new AppSearchEmail.Builder("namespace", "id2")
2543                         .setCreationTimestampMillis(1000)
2544                         .setFrom("[email protected]")
2545                         .setTo("[email protected]", "[email protected]")
2546                         .setSubject("testPut example")
2547                         .setBody("This is the body of the testPut email")
2548                         .build();
2549         checkIsBatchResultSuccess(
2550                 mDb1.putAsync(
2551                         new PutDocumentsRequest.Builder()
2552                                 .addGenericDocuments(email1, email2)
2553                                 .build()));
2554 
2555         // Get with type property paths {"Email", ["subject", "to"]}
2556         GetByDocumentIdRequest request =
2557                 new GetByDocumentIdRequest.Builder("namespace")
2558                         .addIds("id1", "id2")
2559                         .addProjection(
2560                                 GetByDocumentIdRequest.PROJECTION_SCHEMA_TYPE_WILDCARD,
2561                                 Collections.emptyList())
2562                         .build();
2563         List<GenericDocument> outDocuments = doGet(mDb1, request);
2564 
2565         // The two email documents should have been returned without any properties.
2566         AppSearchEmail expected1 =
2567                 new AppSearchEmail.Builder("namespace", "id2")
2568                         .setCreationTimestampMillis(1000)
2569                         .build();
2570         AppSearchEmail expected2 =
2571                 new AppSearchEmail.Builder("namespace", "id1")
2572                         .setCreationTimestampMillis(1000)
2573                         .build();
2574         assertThat(outDocuments).containsExactly(expected1, expected2);
2575     }
2576 
2577     @Test
testGetDocuments_wildcardProjectionNonExistentType()2578     public void testGetDocuments_wildcardProjectionNonExistentType() throws Exception {
2579         // Schema registration
2580         mDb1.setSchemaAsync(
2581                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
2582                 .get();
2583 
2584         // Index two documents
2585         AppSearchEmail email1 =
2586                 new AppSearchEmail.Builder("namespace", "id1")
2587                         .setCreationTimestampMillis(1000)
2588                         .setFrom("[email protected]")
2589                         .setTo("[email protected]", "[email protected]")
2590                         .setSubject("testPut example")
2591                         .setBody("This is the body of the testPut email")
2592                         .build();
2593         AppSearchEmail email2 =
2594                 new AppSearchEmail.Builder("namespace", "id2")
2595                         .setCreationTimestampMillis(1000)
2596                         .setFrom("[email protected]")
2597                         .setTo("[email protected]", "[email protected]")
2598                         .setSubject("testPut example")
2599                         .setBody("This is the body of the testPut email")
2600                         .build();
2601         checkIsBatchResultSuccess(
2602                 mDb1.putAsync(
2603                         new PutDocumentsRequest.Builder()
2604                                 .addGenericDocuments(email1, email2)
2605                                 .build()));
2606 
2607         // Get with type property paths {"Email", ["subject", "to"]}
2608         GetByDocumentIdRequest request =
2609                 new GetByDocumentIdRequest.Builder("namespace")
2610                         .addIds("id1", "id2")
2611                         .addProjection("NonExistentType", Collections.emptyList())
2612                         .addProjection(
2613                                 GetByDocumentIdRequest.PROJECTION_SCHEMA_TYPE_WILDCARD,
2614                                 ImmutableList.of("subject", "to"))
2615                         .build();
2616         List<GenericDocument> outDocuments = doGet(mDb1, request);
2617 
2618         // The two email documents should have been returned with only the "subject" and "to"
2619         // properties.
2620         AppSearchEmail expected1 =
2621                 new AppSearchEmail.Builder("namespace", "id2")
2622                         .setCreationTimestampMillis(1000)
2623                         .setTo("[email protected]", "[email protected]")
2624                         .setSubject("testPut example")
2625                         .build();
2626         AppSearchEmail expected2 =
2627                 new AppSearchEmail.Builder("namespace", "id1")
2628                         .setCreationTimestampMillis(1000)
2629                         .setTo("[email protected]", "[email protected]")
2630                         .setSubject("testPut example")
2631                         .build();
2632         assertThat(outDocuments).containsExactly(expected1, expected2);
2633     }
2634 
2635     @Test
testQuery()2636     public void testQuery() throws Exception {
2637         // Schema registration
2638         mDb1.setSchemaAsync(
2639                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
2640                 .get();
2641 
2642         // Index a document
2643         AppSearchEmail inEmail =
2644                 new AppSearchEmail.Builder("namespace", "id1")
2645                         .setFrom("[email protected]")
2646                         .setTo("[email protected]", "[email protected]")
2647                         .setSubject("testPut example")
2648                         .setBody("This is the body of the testPut email")
2649                         .build();
2650         checkIsBatchResultSuccess(
2651                 mDb1.putAsync(
2652                         new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
2653 
2654         // Query for the document
2655         SearchResultsShim searchResults =
2656                 mDb1.search(
2657                         "body",
2658                         new SearchSpec.Builder()
2659                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
2660                                 .build());
2661         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
2662         assertThat(documents).hasSize(1);
2663         assertThat(documents.get(0)).isEqualTo(inEmail);
2664 
2665         // Multi-term query
2666         searchResults =
2667                 mDb1.search(
2668                         "body email",
2669                         new SearchSpec.Builder()
2670                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
2671                                 .build());
2672         documents = convertSearchResultsToDocuments(searchResults);
2673         assertThat(documents).hasSize(1);
2674         assertThat(documents.get(0)).isEqualTo(inEmail);
2675     }
2676 
2677     @Test
testQuery_getNextPage()2678     public void testQuery_getNextPage() throws Exception {
2679         // Schema registration
2680         mDb1.setSchemaAsync(
2681                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
2682                 .get();
2683         Set<AppSearchEmail> emailSet = new HashSet<>();
2684         PutDocumentsRequest.Builder putDocumentsRequestBuilder = new PutDocumentsRequest.Builder();
2685         // Index 31 documents
2686         for (int i = 0; i < 31; i++) {
2687             AppSearchEmail inEmail =
2688                     new AppSearchEmail.Builder("namespace", "id" + i)
2689                             .setFrom("[email protected]")
2690                             .setTo("[email protected]", "[email protected]")
2691                             .setSubject("testPut example")
2692                             .setBody("This is the body of the testPut email")
2693                             .build();
2694             emailSet.add(inEmail);
2695             putDocumentsRequestBuilder.addGenericDocuments(inEmail);
2696         }
2697         checkIsBatchResultSuccess(mDb1.putAsync(putDocumentsRequestBuilder.build()));
2698 
2699         // Set number of results per page is 7.
2700         SearchResultsShim searchResults =
2701                 mDb1.search(
2702                         "body",
2703                         new SearchSpec.Builder()
2704                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
2705                                 .setResultCountPerPage(7)
2706                                 .build());
2707         List<GenericDocument> documents = new ArrayList<>();
2708 
2709         int pageNumber = 0;
2710         List<SearchResult> results;
2711 
2712         // keep loading next page until it's empty.
2713         do {
2714             results = searchResults.getNextPageAsync().get();
2715             ++pageNumber;
2716             for (SearchResult result : results) {
2717                 documents.add(result.getGenericDocument());
2718             }
2719         } while (results.size() > 0);
2720 
2721         // check all document presents
2722         assertThat(documents).containsExactlyElementsIn(emailSet);
2723         assertThat(pageNumber).isEqualTo(6); // 5 (upper(31/7)) + 1 (final empty page)
2724     }
2725 
2726     @Test
testQueryIndexableLongProperty_numericSearchEnabledSucceeds()2727     public void testQueryIndexableLongProperty_numericSearchEnabledSucceeds() throws Exception {
2728         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.NUMERIC_SEARCH));
2729 
2730         // Schema registration
2731         AppSearchSchema transactionSchema =
2732                 new AppSearchSchema.Builder("transaction")
2733                         .addProperty(
2734                                 new LongPropertyConfig.Builder("price")
2735                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
2736                                         .setIndexingType(LongPropertyConfig.INDEXING_TYPE_RANGE)
2737                                         .build())
2738                         .addProperty(
2739                                 new LongPropertyConfig.Builder("cost")
2740                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
2741                                         .setIndexingType(LongPropertyConfig.INDEXING_TYPE_RANGE)
2742                                         .build())
2743                         .build();
2744         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(transactionSchema).build())
2745                 .get();
2746 
2747         // Index some documents
2748         GenericDocument doc1 =
2749                 new GenericDocument.Builder<>("namespace", "id1", "transaction")
2750                         .setPropertyLong("price", 10)
2751                         .setCreationTimestampMillis(1000)
2752                         .build();
2753         GenericDocument doc2 =
2754                 new GenericDocument.Builder<>("namespace", "id2", "transaction")
2755                         .setPropertyLong("price", 25)
2756                         .setCreationTimestampMillis(1000)
2757                         .build();
2758         GenericDocument doc3 =
2759                 new GenericDocument.Builder<>("namespace", "id3", "transaction")
2760                         .setPropertyLong("cost", 2)
2761                         .setCreationTimestampMillis(1000)
2762                         .build();
2763         checkIsBatchResultSuccess(
2764                 mDb1.putAsync(
2765                         new PutDocumentsRequest.Builder()
2766                                 .addGenericDocuments(doc1, doc2, doc3)
2767                                 .build()));
2768 
2769         // Query for the document
2770         SearchResultsShim searchResults =
2771                 mDb1.search(
2772                         "price < 20",
2773                         new SearchSpec.Builder().setNumericSearchEnabled(true).build());
2774         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
2775         assertThat(documents).hasSize(1);
2776         assertThat(documents.get(0)).isEqualTo(doc1);
2777 
2778         searchResults =
2779                 mDb1.search(
2780                         "price == 25",
2781                         new SearchSpec.Builder().setNumericSearchEnabled(true).build());
2782         documents = convertSearchResultsToDocuments(searchResults);
2783         assertThat(documents).hasSize(1);
2784         assertThat(documents.get(0)).isEqualTo(doc2);
2785 
2786         searchResults =
2787                 mDb1.search(
2788                         "cost > 2", new SearchSpec.Builder().setNumericSearchEnabled(true).build());
2789         documents = convertSearchResultsToDocuments(searchResults);
2790         assertThat(documents).isEmpty();
2791 
2792         searchResults =
2793                 mDb1.search(
2794                         "cost >= 2",
2795                         new SearchSpec.Builder().setNumericSearchEnabled(true).build());
2796         documents = convertSearchResultsToDocuments(searchResults);
2797         assertThat(documents).hasSize(1);
2798         assertThat(documents.get(0)).isEqualTo(doc3);
2799 
2800         searchResults =
2801                 mDb1.search(
2802                         "price <= 25",
2803                         new SearchSpec.Builder().setNumericSearchEnabled(true).build());
2804         documents = convertSearchResultsToDocuments(searchResults);
2805         assertThat(documents).hasSize(2);
2806         assertThat(documents.get(0)).isEqualTo(doc2);
2807         assertThat(documents.get(1)).isEqualTo(doc1);
2808     }
2809 
2810     @Test
testQueryIndexableLongProperty_numericSearchNotEnabled()2811     public void testQueryIndexableLongProperty_numericSearchNotEnabled() throws Exception {
2812         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.NUMERIC_SEARCH));
2813 
2814         // Schema registration
2815         AppSearchSchema transactionSchema =
2816                 new AppSearchSchema.Builder("transaction")
2817                         .addProperty(
2818                                 new LongPropertyConfig.Builder("price")
2819                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
2820                                         .setIndexingType(LongPropertyConfig.INDEXING_TYPE_RANGE)
2821                                         .build())
2822                         .build();
2823         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(transactionSchema).build())
2824                 .get();
2825 
2826         // Index some documents
2827         GenericDocument doc =
2828                 new GenericDocument.Builder<>("namespace", "id1", "transaction")
2829                         .setPropertyLong("price", 10)
2830                         .setCreationTimestampMillis(1000)
2831                         .build();
2832         checkIsBatchResultSuccess(
2833                 mDb1.putAsync(new PutDocumentsRequest.Builder().addGenericDocuments(doc).build()));
2834 
2835         // Query for the document
2836         // Use advanced query but disable NUMERIC_SEARCH in the SearchSpec.
2837         SearchResultsShim searchResults =
2838                 mDb1.search(
2839                         "price < 20",
2840                         new SearchSpec.Builder().setNumericSearchEnabled(false).build());
2841 
2842         ExecutionException executionException =
2843                 assertThrows(
2844                         ExecutionException.class, () -> searchResults.getNextPageAsync().get());
2845         assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
2846         AppSearchException exception = (AppSearchException) executionException.getCause();
2847         assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT);
2848         assertThat(exception).hasMessageThat().contains("Attempted use of unenabled feature");
2849         assertThat(exception).hasMessageThat().contains(Features.NUMERIC_SEARCH);
2850     }
2851 
2852     @Test
testQuery_relevanceScoring()2853     public void testQuery_relevanceScoring() throws Exception {
2854         // Schema registration
2855         mDb1.setSchemaAsync(
2856                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
2857                 .get();
2858 
2859         // Index two documents
2860         AppSearchEmail email1 =
2861                 new AppSearchEmail.Builder("namespace", "id1")
2862                         .setCreationTimestampMillis(1000)
2863                         .setFrom("[email protected]")
2864                         .setTo("[email protected]", "[email protected]")
2865                         .setSubject("Mary had a little lamb")
2866                         .setBody("A little lamb, little lamb")
2867                         .build();
2868         AppSearchEmail email2 =
2869                 new AppSearchEmail.Builder("namespace", "id2")
2870                         .setCreationTimestampMillis(1000)
2871                         .setFrom("[email protected]")
2872                         .setTo("[email protected]", "[email protected]")
2873                         .setSubject("I'm a little teapot")
2874                         .setBody("short and stout. Here is my handle, here is my spout.")
2875                         .build();
2876         checkIsBatchResultSuccess(
2877                 mDb1.putAsync(
2878                         new PutDocumentsRequest.Builder()
2879                                 .addGenericDocuments(email1, email2)
2880                                 .build()));
2881 
2882         // Query for "little". It should match both emails.
2883         SearchResultsShim searchResults =
2884                 mDb1.search(
2885                         "little",
2886                         new SearchSpec.Builder()
2887                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
2888                                 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE)
2889                                 .build());
2890         List<SearchResult> results = retrieveAllSearchResults(searchResults);
2891 
2892         // The email1 should be ranked higher because 'little' appears three times in email1 and
2893         // only once in email2.
2894         assertThat(results).hasSize(2);
2895         assertThat(results.get(0).getGenericDocument()).isEqualTo(email1);
2896         assertThat(results.get(0).getRankingSignal())
2897                 .isGreaterThan(results.get(1).getRankingSignal());
2898         assertThat(results.get(1).getGenericDocument()).isEqualTo(email2);
2899         assertThat(results.get(1).getRankingSignal()).isGreaterThan(0);
2900 
2901         // Query for "little OR stout". It should match both emails.
2902         searchResults =
2903                 mDb1.search(
2904                         "little OR stout",
2905                         new SearchSpec.Builder()
2906                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
2907                                 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE)
2908                                 .build());
2909         results = retrieveAllSearchResults(searchResults);
2910 
2911         // The email2 should be ranked higher because 'little' appears once and "stout", which is a
2912         // rarer term, appears once. email1 only has the three 'little' appearances.
2913         assertThat(results).hasSize(2);
2914         assertThat(results.get(0).getGenericDocument()).isEqualTo(email2);
2915         assertThat(results.get(0).getRankingSignal())
2916                 .isGreaterThan(results.get(1).getRankingSignal());
2917         assertThat(results.get(1).getGenericDocument()).isEqualTo(email1);
2918         assertThat(results.get(1).getRankingSignal()).isGreaterThan(0);
2919     }
2920 
2921     @Test
testQuery_advancedRanking()2922     public void testQuery_advancedRanking() throws Exception {
2923         assumeTrue(
2924                 mDb1.getFeatures()
2925                         .isFeatureSupported(Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION));
2926 
2927         // Schema registration
2928         mDb1.setSchemaAsync(
2929                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
2930                 .get();
2931 
2932         // Index a document
2933         AppSearchEmail inEmail =
2934                 new AppSearchEmail.Builder("namespace", "id1")
2935                         .setFrom("[email protected]")
2936                         .setTo("[email protected]", "[email protected]")
2937                         .setSubject("testPut example")
2938                         .setBody("This is the body of the testPut email")
2939                         .setScore(3)
2940                         .build();
2941         checkIsBatchResultSuccess(
2942                 mDb1.putAsync(
2943                         new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
2944 
2945         // Query for the document, and set an advanced ranking expression that evaluates to 6.
2946         SearchResultsShim searchResults =
2947                 mDb1.search(
2948                         "body",
2949                         new SearchSpec.Builder()
2950                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
2951                                 // "abs(pow(2, 2) - 6)" should be evaluated to 2.
2952                                 // "this.documentScore()" should be evaluated to 3.
2953                                 .setRankingStrategy("abs(pow(2, 2) - 6) * this.documentScore()")
2954                                 .build());
2955         List<SearchResult> results = retrieveAllSearchResults(searchResults);
2956         assertThat(results).hasSize(1);
2957         assertThat(results.get(0).getGenericDocument()).isEqualTo(inEmail);
2958         assertThat(results.get(0).getRankingSignal()).isEqualTo(6);
2959     }
2960 
2961     @Test
testQuery_advancedRankingWithPropertyWeights()2962     public void testQuery_advancedRankingWithPropertyWeights() throws Exception {
2963         assumeTrue(
2964                 mDb1.getFeatures()
2965                         .isFeatureSupported(Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION));
2966         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SPEC_PROPERTY_WEIGHTS));
2967 
2968         // Schema registration
2969         mDb1.setSchemaAsync(
2970                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
2971                 .get();
2972 
2973         // Index a document
2974         AppSearchEmail inEmail =
2975                 new AppSearchEmail.Builder("namespace", "id1")
2976                         .setFrom("test from")
2977                         .setTo("test to")
2978                         .setSubject("subject")
2979                         .setBody("test body")
2980                         .build();
2981         checkIsBatchResultSuccess(
2982                 mDb1.putAsync(
2983                         new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
2984 
2985         // Query for the document, and set an advanced ranking expression that evaluates to 0.7.
2986         SearchResultsShim searchResults =
2987                 mDb1.search(
2988                         "test",
2989                         new SearchSpec.Builder()
2990                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
2991                                 .setPropertyWeights(
2992                                         AppSearchEmail.SCHEMA_TYPE,
2993                                         ImmutableMap.of(
2994                                                 "from", 0.1, "to", 0.2, "subject", 2.0, "body",
2995                                                 0.4))
2996                                 // this.propertyWeights() returns normalized property weights, in
2997                                 // which each
2998                                 // weight is divided by the maximum weight.
2999                                 // As a result, this expression will evaluates to the list {0.1 /
3000                                 // 2.0, 0.2 / 2.0,
3001                                 // 0.4 / 2.0}, since the matched properties are "from", "to" and
3002                                 // "body", and the
3003                                 // maximum weight provided is 2.0.
3004                                 // Thus, sum(this.propertyWeights()) will be evaluated to 0.05 + 0.1
3005                                 // + 0.2 = 0.35.
3006                                 .setRankingStrategy("sum(this.propertyWeights())")
3007                                 .build());
3008         List<SearchResult> results = retrieveAllSearchResults(searchResults);
3009         assertThat(results).hasSize(1);
3010         assertThat(results.get(0).getGenericDocument()).isEqualTo(inEmail);
3011         assertThat(results.get(0).getRankingSignal()).isEqualTo(0.35);
3012     }
3013 
3014     @Test
testQuery_advancedRankingWithJoin()3015     public void testQuery_advancedRankingWithJoin() throws Exception {
3016         assumeTrue(
3017                 mDb1.getFeatures()
3018                         .isFeatureSupported(Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION));
3019         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
3020 
3021         // A full example of how join might be used
3022         AppSearchSchema actionSchema =
3023                 new AppSearchSchema.Builder("ViewAction")
3024                         .addProperty(
3025                                 new StringPropertyConfig.Builder("entityId")
3026                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3027                                         .setIndexingType(
3028                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
3029                                         .setJoinableValueType(
3030                                                 StringPropertyConfig
3031                                                         .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
3032                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
3033                                         .build())
3034                         .addProperty(
3035                                 new StringPropertyConfig.Builder("note")
3036                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3037                                         .setIndexingType(
3038                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
3039                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
3040                                         .build())
3041                         .build();
3042 
3043         // Schema registration
3044         mDb1.setSchemaAsync(
3045                         new SetSchemaRequest.Builder()
3046                                 .addSchemas(AppSearchEmail.SCHEMA, actionSchema)
3047                                 .build())
3048                 .get();
3049 
3050         // Index a document
3051         AppSearchEmail inEmail =
3052                 new AppSearchEmail.Builder("namespace", "id1")
3053                         .setFrom("[email protected]")
3054                         .setTo("[email protected]", "[email protected]")
3055                         .setSubject("testPut example")
3056                         .setBody("This is the body of the testPut email")
3057                         .setScore(1)
3058                         .build();
3059 
3060         String qualifiedId =
3061                 DocumentIdUtil.createQualifiedId(
3062                         mContext.getPackageName(), DB_NAME_1, "namespace", "id1");
3063         GenericDocument viewAction1 =
3064                 new GenericDocument.Builder<>("NS", "id2", "ViewAction")
3065                         .setScore(1)
3066                         .setPropertyString("entityId", qualifiedId)
3067                         .setPropertyString("note", "Viewed email on Monday")
3068                         .build();
3069         GenericDocument viewAction2 =
3070                 new GenericDocument.Builder<>("NS", "id3", "ViewAction")
3071                         .setScore(2)
3072                         .setPropertyString("entityId", qualifiedId)
3073                         .setPropertyString("note", "Viewed email on Tuesday")
3074                         .build();
3075         checkIsBatchResultSuccess(
3076                 mDb1.putAsync(
3077                         new PutDocumentsRequest.Builder()
3078                                 .addGenericDocuments(inEmail, viewAction1, viewAction2)
3079                                 .build()));
3080 
3081         SearchSpec nestedSearchSpec =
3082                 new SearchSpec.Builder()
3083                         .setRankingStrategy("2 * this.documentScore()")
3084                         .setOrder(SearchSpec.ORDER_ASCENDING)
3085                         .build();
3086 
3087         JoinSpec js =
3088                 new JoinSpec.Builder("entityId").setNestedSearch("", nestedSearchSpec).build();
3089 
3090         SearchResultsShim searchResults =
3091                 mDb1.search(
3092                         "body email",
3093                         new SearchSpec.Builder()
3094                                 // this.childrenRankingSignals() evaluates to the list {1 * 2, 2 *
3095                                 // 2}.
3096                                 // Thus, sum(this.childrenRankingSignals()) evaluates to 6.
3097                                 .setRankingStrategy("sum(this.childrenRankingSignals())")
3098                                 .setJoinSpec(js)
3099                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
3100                                 .build());
3101 
3102         List<SearchResult> sr = searchResults.getNextPageAsync().get();
3103 
3104         assertThat(sr).hasSize(1);
3105         assertThat(sr.get(0).getGenericDocument().getId()).isEqualTo("id1");
3106         assertThat(sr.get(0).getJoinedResults()).hasSize(2);
3107         assertThat(sr.get(0).getJoinedResults().get(0).getGenericDocument()).isEqualTo(viewAction1);
3108         assertThat(sr.get(0).getJoinedResults().get(1).getGenericDocument()).isEqualTo(viewAction2);
3109         assertThat(sr.get(0).getRankingSignal()).isEqualTo(6.0);
3110     }
3111 
3112     @Test
testQueryRankByClickActions_useTakenActionGenericDocument()3113     public void testQueryRankByClickActions_useTakenActionGenericDocument() throws Exception {
3114         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
3115 
3116         AppSearchSchema searchActionSchema =
3117                 new AppSearchSchema.Builder("builtin:SearchAction")
3118                         .addProperty(
3119                                 new LongPropertyConfig.Builder("actionType")
3120                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3121                                         .build())
3122                         .addProperty(
3123                                 new StringPropertyConfig.Builder("query")
3124                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3125                                         .setIndexingType(
3126                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
3127                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
3128                                         .build())
3129                         .build();
3130         AppSearchSchema clickActionSchema =
3131                 new AppSearchSchema.Builder("builtin:ClickAction")
3132                         .addProperty(
3133                                 new LongPropertyConfig.Builder("actionType")
3134                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3135                                         .build())
3136                         .addProperty(
3137                                 new StringPropertyConfig.Builder("query")
3138                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3139                                         .setIndexingType(
3140                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
3141                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
3142                                         .build())
3143                         .addProperty(
3144                                 new StringPropertyConfig.Builder("referencedQualifiedId")
3145                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3146                                         .setJoinableValueType(
3147                                                 StringPropertyConfig
3148                                                         .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
3149                                         .build())
3150                         .build();
3151         AppSearchSchema impressionActionSchema =
3152                 new AppSearchSchema.Builder("builtin:ImpressionAction")
3153                         .addProperty(
3154                                 new LongPropertyConfig.Builder("actionType")
3155                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3156                                         .build())
3157                         .addProperty(
3158                                 new StringPropertyConfig.Builder("query")
3159                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3160                                         .setIndexingType(
3161                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
3162                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
3163                                         .build())
3164                         .addProperty(
3165                                 new StringPropertyConfig.Builder("referencedQualifiedId")
3166                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3167                                         .setJoinableValueType(
3168                                                 StringPropertyConfig
3169                                                         .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
3170                                         .build())
3171                         .build();
3172         AppSearchSchema dismissActionSchema =
3173                 new AppSearchSchema.Builder("builtin:DismissAction")
3174                         .addProperty(
3175                                 new LongPropertyConfig.Builder("actionType")
3176                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3177                                         .build())
3178                         .addProperty(
3179                                 new StringPropertyConfig.Builder("query")
3180                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3181                                         .setIndexingType(
3182                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
3183                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
3184                                         .build())
3185                         .addProperty(
3186                                 new StringPropertyConfig.Builder("referencedQualifiedId")
3187                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3188                                         .setJoinableValueType(
3189                                                 StringPropertyConfig
3190                                                         .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
3191                                         .build())
3192                         .build();
3193 
3194         // Schema registration
3195         mDb1.setSchemaAsync(
3196                         new SetSchemaRequest.Builder()
3197                                 .addSchemas(
3198                                         AppSearchEmail.SCHEMA,
3199                                         searchActionSchema,
3200                                         clickActionSchema,
3201                                         impressionActionSchema,
3202                                         dismissActionSchema)
3203                                 .build())
3204                 .get();
3205 
3206         // Index several email documents
3207         AppSearchEmail inEmail1 =
3208                 new AppSearchEmail.Builder("namespace", "email1")
3209                         .setFrom("[email protected]")
3210                         .setTo("[email protected]", "[email protected]")
3211                         .setSubject("testPut example")
3212                         .setBody("This is the body of the testPut email")
3213                         .setScore(1)
3214                         .build();
3215         AppSearchEmail inEmail2 =
3216                 new AppSearchEmail.Builder("namespace", "email2")
3217                         .setFrom("[email protected]")
3218                         .setTo("[email protected]", "[email protected]")
3219                         .setSubject("testPut example")
3220                         .setBody("This is the body of the testPut email")
3221                         .setScore(1)
3222                         .build();
3223 
3224         String qualifiedId1 =
3225                 DocumentIdUtil.createQualifiedId(mContext.getPackageName(), DB_NAME_1, inEmail1);
3226         String qualifiedId2 =
3227                 DocumentIdUtil.createQualifiedId(mContext.getPackageName(), DB_NAME_1, inEmail2);
3228 
3229         GenericDocument searchAction =
3230                 new GenericDocument.Builder<>("namespace", "search1", "builtin:SearchAction")
3231                         .setCreationTimestampMillis(1000)
3232                         .setPropertyLong("actionType", ACTION_TYPE_SEARCH)
3233                         .setPropertyString("query", "body")
3234                         .build();
3235         GenericDocument clickAction1 =
3236                 new GenericDocument.Builder<>("namespace", "click1", "builtin:ClickAction")
3237                         .setCreationTimestampMillis(2000)
3238                         .setPropertyLong("actionType", ACTION_TYPE_CLICK)
3239                         .setPropertyString("query", "body")
3240                         .setPropertyString("referencedQualifiedId", qualifiedId1)
3241                         .build();
3242         GenericDocument clickAction2 =
3243                 new GenericDocument.Builder<>("namespace", "click2", "builtin:ClickAction")
3244                         .setCreationTimestampMillis(3000)
3245                         .setPropertyLong("actionType", ACTION_TYPE_CLICK)
3246                         .setPropertyString("query", "body")
3247                         .setPropertyString("referencedQualifiedId", qualifiedId2)
3248                         .build();
3249         GenericDocument clickAction3 =
3250                 new GenericDocument.Builder<>("namespace", "click3", "builtin:ClickAction")
3251                         .setCreationTimestampMillis(4000)
3252                         .setPropertyLong("actionType", ACTION_TYPE_CLICK)
3253                         .setPropertyString("query", "body")
3254                         .setPropertyString("referencedQualifiedId", qualifiedId1)
3255                         .build();
3256         GenericDocument impressionAction1 =
3257                 new GenericDocument.Builder<>(
3258                                 "namespace", "impression1", "builtin:ImpressionAction")
3259                         .setCreationTimestampMillis(5000)
3260                         .setPropertyLong("actionType", ACTION_TYPE_IMPRESSION)
3261                         .setPropertyString("query", "body")
3262                         .setPropertyString("referencedQualifiedId", qualifiedId2)
3263                         .build();
3264         GenericDocument dismissAction1 =
3265                 new GenericDocument.Builder<>("namespace", "dismiss1", "builtin:DismissAction")
3266                         .setCreationTimestampMillis(6000)
3267                         .setPropertyLong("actionType", ACTION_TYPE_DISMISS)
3268                         .setPropertyString("query", "body")
3269                         .setPropertyString("referencedQualifiedId", qualifiedId2)
3270                         .build();
3271 
3272         checkIsBatchResultSuccess(
3273                 mDb1.putAsync(
3274                         new PutDocumentsRequest.Builder()
3275                                 .addGenericDocuments(inEmail1, inEmail2)
3276                                 .addTakenActionGenericDocuments(
3277                                         searchAction,
3278                                         clickAction1,
3279                                         clickAction2,
3280                                         clickAction3,
3281                                         impressionAction1,
3282                                         dismissAction1)
3283                                 .build()));
3284 
3285         SearchSpec nestedSearchSpec =
3286                 new SearchSpec.Builder()
3287                         .setRankingStrategy(SearchSpec.RANKING_STRATEGY_DOCUMENT_SCORE)
3288                         .setOrder(SearchSpec.ORDER_DESCENDING)
3289                         .addFilterSchemas("builtin:ClickAction")
3290                         .build();
3291 
3292         // Note: SearchSpec.Builder#setMaxJoinedResultCount only limits the number of child
3293         // documents returned. It does not affect the number of child documents that are scored.
3294         JoinSpec js =
3295                 new JoinSpec.Builder("referencedQualifiedId")
3296                         .setNestedSearch("query:body", nestedSearchSpec)
3297                         .setAggregationScoringStrategy(JoinSpec.AGGREGATION_SCORING_RESULT_COUNT)
3298                         .setMaxJoinedResultCount(0)
3299                         .build();
3300 
3301         // Search "body" for AppSearchEmail documents, ranking by ClickAction signals with
3302         // query = "body".
3303         SearchResultsShim searchResults =
3304                 mDb1.search(
3305                         "body",
3306                         new SearchSpec.Builder()
3307                                 .setRankingStrategy(
3308                                         SearchSpec.RANKING_STRATEGY_JOIN_AGGREGATE_SCORE)
3309                                 .setOrder(SearchSpec.ORDER_DESCENDING)
3310                                 .setJoinSpec(js)
3311                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
3312                                 .addFilterSchemas(AppSearchEmail.SCHEMA_TYPE)
3313                                 .build());
3314 
3315         List<SearchResult> sr = searchResults.getNextPageAsync().get();
3316 
3317         assertThat(sr).hasSize(2);
3318         assertThat(sr.get(0).getGenericDocument().getId()).isEqualTo("email1");
3319         assertThat(sr.get(0).getRankingSignal()).isEqualTo(2.0);
3320         assertThat(sr.get(1).getGenericDocument().getId()).isEqualTo("email2");
3321         assertThat(sr.get(1).getRankingSignal()).isEqualTo(1.0);
3322     }
3323 
3324     @Test
testQueryRankByImpressionActions_useTakenActionGenericDocument()3325     public void testQueryRankByImpressionActions_useTakenActionGenericDocument() throws Exception {
3326         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
3327 
3328         AppSearchSchema searchActionSchema =
3329                 new AppSearchSchema.Builder("builtin:SearchAction")
3330                         .addProperty(
3331                                 new LongPropertyConfig.Builder("actionType")
3332                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3333                                         .build())
3334                         .addProperty(
3335                                 new StringPropertyConfig.Builder("query")
3336                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3337                                         .setIndexingType(
3338                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
3339                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
3340                                         .build())
3341                         .build();
3342         AppSearchSchema clickActionSchema =
3343                 new AppSearchSchema.Builder("builtin:ClickAction")
3344                         .addProperty(
3345                                 new LongPropertyConfig.Builder("actionType")
3346                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3347                                         .build())
3348                         .addProperty(
3349                                 new StringPropertyConfig.Builder("query")
3350                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3351                                         .setIndexingType(
3352                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
3353                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
3354                                         .build())
3355                         .addProperty(
3356                                 new StringPropertyConfig.Builder("referencedQualifiedId")
3357                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3358                                         .setJoinableValueType(
3359                                                 StringPropertyConfig
3360                                                         .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
3361                                         .build())
3362                         .build();
3363         AppSearchSchema impressionActionSchema =
3364                 new AppSearchSchema.Builder("builtin:ImpressionAction")
3365                         .addProperty(
3366                                 new LongPropertyConfig.Builder("actionType")
3367                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3368                                         .build())
3369                         .addProperty(
3370                                 new StringPropertyConfig.Builder("query")
3371                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3372                                         .setIndexingType(
3373                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
3374                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
3375                                         .build())
3376                         .addProperty(
3377                                 new StringPropertyConfig.Builder("referencedQualifiedId")
3378                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3379                                         .setJoinableValueType(
3380                                                 StringPropertyConfig
3381                                                         .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
3382                                         .build())
3383                         .build();
3384         AppSearchSchema dismissActionSchema =
3385                 new AppSearchSchema.Builder("builtin:DismissAction")
3386                         .addProperty(
3387                                 new LongPropertyConfig.Builder("actionType")
3388                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3389                                         .build())
3390                         .addProperty(
3391                                 new StringPropertyConfig.Builder("query")
3392                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3393                                         .setIndexingType(
3394                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
3395                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
3396                                         .build())
3397                         .addProperty(
3398                                 new StringPropertyConfig.Builder("referencedQualifiedId")
3399                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3400                                         .setJoinableValueType(
3401                                                 StringPropertyConfig
3402                                                         .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
3403                                         .build())
3404                         .build();
3405 
3406         // Schema registration
3407         mDb1.setSchemaAsync(
3408                         new SetSchemaRequest.Builder()
3409                                 .addSchemas(
3410                                         AppSearchEmail.SCHEMA,
3411                                         searchActionSchema,
3412                                         clickActionSchema,
3413                                         impressionActionSchema,
3414                                         dismissActionSchema)
3415                                 .build())
3416                 .get();
3417 
3418         // Index several email documents
3419         AppSearchEmail inEmail1 =
3420                 new AppSearchEmail.Builder("namespace", "email1")
3421                         .setFrom("[email protected]")
3422                         .setTo("[email protected]", "[email protected]")
3423                         .setSubject("testPut example")
3424                         .setBody("This is the body of the testPut email")
3425                         .setScore(1)
3426                         .build();
3427         AppSearchEmail inEmail2 =
3428                 new AppSearchEmail.Builder("namespace", "email2")
3429                         .setFrom("[email protected]")
3430                         .setTo("[email protected]", "[email protected]")
3431                         .setSubject("testPut example")
3432                         .setBody("This is the body of the testPut email")
3433                         .setScore(1)
3434                         .build();
3435 
3436         String qualifiedId1 =
3437                 DocumentIdUtil.createQualifiedId(mContext.getPackageName(), DB_NAME_1, inEmail1);
3438         String qualifiedId2 =
3439                 DocumentIdUtil.createQualifiedId(mContext.getPackageName(), DB_NAME_1, inEmail2);
3440 
3441         GenericDocument searchAction =
3442                 new GenericDocument.Builder<>("namespace", "search1", "builtin:SearchAction")
3443                         .setCreationTimestampMillis(1000)
3444                         .setPropertyLong("actionType", ACTION_TYPE_SEARCH)
3445                         .setPropertyString("query", "body")
3446                         .build();
3447         GenericDocument clickAction1 =
3448                 new GenericDocument.Builder<>("namespace", "click1", "builtin:ClickAction")
3449                         .setCreationTimestampMillis(2000)
3450                         .setPropertyLong("actionType", ACTION_TYPE_CLICK)
3451                         .setPropertyString("query", "body")
3452                         .setPropertyString("referencedQualifiedId", qualifiedId1)
3453                         .build();
3454         GenericDocument clickAction2 =
3455                 new GenericDocument.Builder<>("namespace", "click2", "builtin:ClickAction")
3456                         .setCreationTimestampMillis(3000)
3457                         .setPropertyLong("actionType", ACTION_TYPE_CLICK)
3458                         .setPropertyString("query", "body")
3459                         .setPropertyString("referencedQualifiedId", qualifiedId2)
3460                         .build();
3461         GenericDocument clickAction3 =
3462                 new GenericDocument.Builder<>("namespace", "click3", "builtin:ClickAction")
3463                         .setCreationTimestampMillis(4000)
3464                         .setPropertyLong("actionType", ACTION_TYPE_CLICK)
3465                         .setPropertyString("query", "body")
3466                         .setPropertyString("referencedQualifiedId", qualifiedId1)
3467                         .build();
3468         GenericDocument impressionAction1 =
3469                 new GenericDocument.Builder<>(
3470                                 "namespace", "impression1", "builtin:ImpressionAction")
3471                         .setCreationTimestampMillis(5000)
3472                         .setPropertyLong("actionType", ACTION_TYPE_IMPRESSION)
3473                         .setPropertyString("query", "body")
3474                         .setPropertyString("referencedQualifiedId", qualifiedId2)
3475                         .build();
3476         GenericDocument dismissAction1 =
3477                 new GenericDocument.Builder<>("namespace", "dismiss1", "builtin:DismissAction")
3478                         .setCreationTimestampMillis(6000)
3479                         .setPropertyLong("actionType", ACTION_TYPE_DISMISS)
3480                         .setPropertyString("query", "body")
3481                         .setPropertyString("referencedQualifiedId", qualifiedId2)
3482                         .build();
3483 
3484         checkIsBatchResultSuccess(
3485                 mDb1.putAsync(
3486                         new PutDocumentsRequest.Builder()
3487                                 .addGenericDocuments(inEmail1, inEmail2)
3488                                 .addTakenActionGenericDocuments(
3489                                         searchAction,
3490                                         clickAction1,
3491                                         clickAction2,
3492                                         clickAction3,
3493                                         impressionAction1,
3494                                         dismissAction1)
3495                                 .build()));
3496 
3497         SearchSpec nestedSearchSpec =
3498                 new SearchSpec.Builder()
3499                         .setRankingStrategy(SearchSpec.RANKING_STRATEGY_DOCUMENT_SCORE)
3500                         .setOrder(SearchSpec.ORDER_DESCENDING)
3501                         .addFilterSchemas("builtin:ImpressionAction")
3502                         .build();
3503 
3504         // Note: SearchSpec.Builder#setMaxJoinedResultCount only limits the number of child
3505         // documents returned. It does not affect the number of child documents that are scored.
3506         JoinSpec js =
3507                 new JoinSpec.Builder("referencedQualifiedId")
3508                         .setNestedSearch("query:body", nestedSearchSpec)
3509                         .setAggregationScoringStrategy(JoinSpec.AGGREGATION_SCORING_RESULT_COUNT)
3510                         .setMaxJoinedResultCount(0)
3511                         .build();
3512 
3513         // Search "body" for AppSearchEmail documents, ranking by ImpressionAction signals with
3514         // query = "body".
3515         SearchResultsShim searchResults =
3516                 mDb1.search(
3517                         "body",
3518                         new SearchSpec.Builder()
3519                                 .setRankingStrategy(
3520                                         SearchSpec.RANKING_STRATEGY_JOIN_AGGREGATE_SCORE)
3521                                 .setOrder(SearchSpec.ORDER_DESCENDING)
3522                                 .setJoinSpec(js)
3523                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
3524                                 .addFilterSchemas(AppSearchEmail.SCHEMA_TYPE)
3525                                 .build());
3526 
3527         List<SearchResult> sr = searchResults.getNextPageAsync().get();
3528 
3529         assertThat(sr).hasSize(2);
3530         assertThat(sr.get(0).getGenericDocument().getId()).isEqualTo("email2");
3531         assertThat(sr.get(0).getRankingSignal()).isEqualTo(1.0);
3532         assertThat(sr.get(1).getGenericDocument().getId()).isEqualTo("email1");
3533         assertThat(sr.get(1).getRankingSignal()).isEqualTo(0.0);
3534     }
3535 
3536     @Test
testQueryRankByDismissActions_useTakenActionGenericDocument()3537     public void testQueryRankByDismissActions_useTakenActionGenericDocument() throws Exception {
3538         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
3539 
3540         AppSearchSchema searchActionSchema =
3541                 new AppSearchSchema.Builder("builtin:SearchAction")
3542                         .addProperty(
3543                                 new LongPropertyConfig.Builder("actionType")
3544                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3545                                         .build())
3546                         .addProperty(
3547                                 new StringPropertyConfig.Builder("query")
3548                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3549                                         .setIndexingType(
3550                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
3551                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
3552                                         .build())
3553                         .build();
3554         AppSearchSchema clickActionSchema =
3555                 new AppSearchSchema.Builder("builtin:ClickAction")
3556                         .addProperty(
3557                                 new LongPropertyConfig.Builder("actionType")
3558                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3559                                         .build())
3560                         .addProperty(
3561                                 new StringPropertyConfig.Builder("query")
3562                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3563                                         .setIndexingType(
3564                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
3565                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
3566                                         .build())
3567                         .addProperty(
3568                                 new StringPropertyConfig.Builder("referencedQualifiedId")
3569                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3570                                         .setJoinableValueType(
3571                                                 StringPropertyConfig
3572                                                         .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
3573                                         .build())
3574                         .build();
3575         AppSearchSchema impressionActionSchema =
3576                 new AppSearchSchema.Builder("builtin:ImpressionAction")
3577                         .addProperty(
3578                                 new LongPropertyConfig.Builder("actionType")
3579                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3580                                         .build())
3581                         .addProperty(
3582                                 new StringPropertyConfig.Builder("query")
3583                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3584                                         .setIndexingType(
3585                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
3586                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
3587                                         .build())
3588                         .addProperty(
3589                                 new StringPropertyConfig.Builder("referencedQualifiedId")
3590                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3591                                         .setJoinableValueType(
3592                                                 StringPropertyConfig
3593                                                         .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
3594                                         .build())
3595                         .build();
3596         AppSearchSchema dismissActionSchema =
3597                 new AppSearchSchema.Builder("builtin:DismissAction")
3598                         .addProperty(
3599                                 new LongPropertyConfig.Builder("actionType")
3600                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3601                                         .build())
3602                         .addProperty(
3603                                 new StringPropertyConfig.Builder("query")
3604                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3605                                         .setIndexingType(
3606                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
3607                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
3608                                         .build())
3609                         .addProperty(
3610                                 new StringPropertyConfig.Builder("referencedQualifiedId")
3611                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3612                                         .setJoinableValueType(
3613                                                 StringPropertyConfig
3614                                                         .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
3615                                         .build())
3616                         .build();
3617 
3618         // Schema registration
3619         mDb1.setSchemaAsync(
3620                         new SetSchemaRequest.Builder()
3621                                 .addSchemas(
3622                                         AppSearchEmail.SCHEMA,
3623                                         searchActionSchema,
3624                                         clickActionSchema,
3625                                         impressionActionSchema,
3626                                         dismissActionSchema)
3627                                 .build())
3628                 .get();
3629 
3630         // Index several email documents
3631         AppSearchEmail inEmail1 =
3632                 new AppSearchEmail.Builder("namespace", "email1")
3633                         .setFrom("[email protected]")
3634                         .setTo("[email protected]", "[email protected]")
3635                         .setSubject("testPut example")
3636                         .setBody("This is the body of the testPut email")
3637                         .setScore(1)
3638                         .build();
3639         AppSearchEmail inEmail2 =
3640                 new AppSearchEmail.Builder("namespace", "email2")
3641                         .setFrom("[email protected]")
3642                         .setTo("[email protected]", "[email protected]")
3643                         .setSubject("testPut example")
3644                         .setBody("This is the body of the testPut email")
3645                         .setScore(1)
3646                         .build();
3647 
3648         String qualifiedId1 =
3649                 DocumentIdUtil.createQualifiedId(mContext.getPackageName(), DB_NAME_1, inEmail1);
3650         String qualifiedId2 =
3651                 DocumentIdUtil.createQualifiedId(mContext.getPackageName(), DB_NAME_1, inEmail2);
3652 
3653         GenericDocument searchAction =
3654                 new GenericDocument.Builder<>("namespace", "search1", "builtin:SearchAction")
3655                         .setCreationTimestampMillis(1000)
3656                         .setPropertyLong("actionType", ACTION_TYPE_SEARCH)
3657                         .setPropertyString("query", "body")
3658                         .build();
3659         GenericDocument clickAction1 =
3660                 new GenericDocument.Builder<>("namespace", "click1", "builtin:ClickAction")
3661                         .setCreationTimestampMillis(2000)
3662                         .setPropertyLong("actionType", ACTION_TYPE_CLICK)
3663                         .setPropertyString("query", "body")
3664                         .setPropertyString("referencedQualifiedId", qualifiedId1)
3665                         .build();
3666         GenericDocument clickAction2 =
3667                 new GenericDocument.Builder<>("namespace", "click2", "builtin:ClickAction")
3668                         .setCreationTimestampMillis(3000)
3669                         .setPropertyLong("actionType", ACTION_TYPE_CLICK)
3670                         .setPropertyString("query", "body")
3671                         .setPropertyString("referencedQualifiedId", qualifiedId2)
3672                         .build();
3673         GenericDocument clickAction3 =
3674                 new GenericDocument.Builder<>("namespace", "click3", "builtin:ClickAction")
3675                         .setCreationTimestampMillis(4000)
3676                         .setPropertyLong("actionType", ACTION_TYPE_CLICK)
3677                         .setPropertyString("query", "body")
3678                         .setPropertyString("referencedQualifiedId", qualifiedId1)
3679                         .build();
3680         GenericDocument impressionAction1 =
3681                 new GenericDocument.Builder<>(
3682                                 "namespace", "impression1", "builtin:ImpressionAction")
3683                         .setCreationTimestampMillis(5000)
3684                         .setPropertyLong("actionType", ACTION_TYPE_IMPRESSION)
3685                         .setPropertyString("query", "body")
3686                         .setPropertyString("referencedQualifiedId", qualifiedId2)
3687                         .build();
3688         GenericDocument dismissAction1 =
3689                 new GenericDocument.Builder<>("namespace", "dismiss1", "builtin:DismissAction")
3690                         .setCreationTimestampMillis(6000)
3691                         .setPropertyLong("actionType", ACTION_TYPE_DISMISS)
3692                         .setPropertyString("query", "body")
3693                         .setPropertyString("referencedQualifiedId", qualifiedId2)
3694                         .build();
3695         GenericDocument dismissAction2 =
3696                 new GenericDocument.Builder<>("namespace", "dismiss2", "builtin:DismissAction")
3697                         .setCreationTimestampMillis(7000)
3698                         .setPropertyLong("actionType", ACTION_TYPE_DISMISS)
3699                         .setPropertyString("query", "body")
3700                         .setPropertyString("referencedQualifiedId", qualifiedId2)
3701                         .build();
3702         GenericDocument dismissAction3 =
3703                 new GenericDocument.Builder<>("namespace", "dismiss3", "builtin:DismissAction")
3704                         .setCreationTimestampMillis(8000)
3705                         .setPropertyLong("actionType", ACTION_TYPE_DISMISS)
3706                         .setPropertyString("query", "body")
3707                         .setPropertyString("referencedQualifiedId", qualifiedId2)
3708                         .build();
3709 
3710         checkIsBatchResultSuccess(
3711                 mDb1.putAsync(
3712                         new PutDocumentsRequest.Builder()
3713                                 .addGenericDocuments(inEmail1, inEmail2)
3714                                 .addTakenActionGenericDocuments(
3715                                         searchAction,
3716                                         clickAction1,
3717                                         clickAction2,
3718                                         clickAction3,
3719                                         impressionAction1,
3720                                         dismissAction1,
3721                                         dismissAction2,
3722                                         dismissAction3)
3723                                 .build()));
3724 
3725         SearchSpec nestedSearchSpec =
3726                 new SearchSpec.Builder()
3727                         .setRankingStrategy(SearchSpec.RANKING_STRATEGY_DOCUMENT_SCORE)
3728                         .setOrder(SearchSpec.ORDER_DESCENDING)
3729                         .addFilterSchemas("builtin:DismissAction")
3730                         .build();
3731 
3732         // Note: SearchSpec.Builder#setMaxJoinedResultCount only limits the number of child
3733         // documents returned. It does not affect the number of child documents that are scored.
3734         JoinSpec js =
3735                 new JoinSpec.Builder("referencedQualifiedId")
3736                         .setNestedSearch("query:body", nestedSearchSpec)
3737                         .setMaxJoinedResultCount(0)
3738                         .build();
3739 
3740         // Search "body" for AppSearchEmail documents, ranking by DismissAction signals with
3741         // query = "body" via advanced scoring language syntax to assign negative weights.
3742         SearchResultsShim searchResults =
3743                 mDb1.search(
3744                         "body",
3745                         new SearchSpec.Builder()
3746                                 .setRankingStrategy("-len(this.childrenRankingSignals())")
3747                                 .setOrder(SearchSpec.ORDER_DESCENDING)
3748                                 .setJoinSpec(js)
3749                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
3750                                 .addFilterSchemas(AppSearchEmail.SCHEMA_TYPE)
3751                                 .build());
3752 
3753         List<SearchResult> sr = searchResults.getNextPageAsync().get();
3754 
3755         assertThat(sr).hasSize(2);
3756         assertThat(sr.get(0).getGenericDocument().getId()).isEqualTo("email1");
3757         assertThat(sr.get(0).getRankingSignal()).isEqualTo(-0.0);
3758         assertThat(sr.get(1).getGenericDocument().getId()).isEqualTo("email2");
3759         assertThat(sr.get(1).getRankingSignal()).isEqualTo(-3.0);
3760     }
3761 
3762     @Test
testQuery_invalidAdvancedRanking()3763     public void testQuery_invalidAdvancedRanking() throws Exception {
3764         assumeTrue(
3765                 mDb1.getFeatures()
3766                         .isFeatureSupported(Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION));
3767 
3768         // Schema registration
3769         mDb1.setSchemaAsync(
3770                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
3771                 .get();
3772 
3773         // Index a document
3774         AppSearchEmail inEmail =
3775                 new AppSearchEmail.Builder("namespace", "id1")
3776                         .setFrom("[email protected]")
3777                         .setTo("[email protected]", "[email protected]")
3778                         .setSubject("testPut example")
3779                         .setBody("This is the body of the testPut email")
3780                         .build();
3781         checkIsBatchResultSuccess(
3782                 mDb1.putAsync(
3783                         new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
3784 
3785         // Query for the document, but set an invalid advanced ranking expression.
3786         SearchResultsShim searchResults =
3787                 mDb1.search(
3788                         "body",
3789                         new SearchSpec.Builder()
3790                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
3791                                 .setRankingStrategy("sqrt()")
3792                                 .build());
3793         ExecutionException executionException =
3794                 assertThrows(
3795                         ExecutionException.class, () -> searchResults.getNextPageAsync().get());
3796         assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
3797         AppSearchException exception = (AppSearchException) executionException.getCause();
3798         assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT);
3799         assertThat(exception)
3800                 .hasMessageThat()
3801                 .contains("Math functions must have at least one argument.");
3802     }
3803 
3804     @Test
testQuery_invalidAdvancedRankingWithChildrenRankingSignals()3805     public void testQuery_invalidAdvancedRankingWithChildrenRankingSignals() throws Exception {
3806         assumeTrue(
3807                 mDb1.getFeatures()
3808                         .isFeatureSupported(Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION));
3809         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
3810 
3811         // Schema registration
3812         mDb1.setSchemaAsync(
3813                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
3814                 .get();
3815 
3816         // Index a document
3817         AppSearchEmail inEmail =
3818                 new AppSearchEmail.Builder("namespace", "id1")
3819                         .setFrom("[email protected]")
3820                         .setTo("[email protected]", "[email protected]")
3821                         .setSubject("testPut example")
3822                         .setBody("This is the body of the testPut email")
3823                         .build();
3824         checkIsBatchResultSuccess(
3825                 mDb1.putAsync(
3826                         new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
3827 
3828         SearchResultsShim searchResults =
3829                 mDb1.search(
3830                         "body",
3831                         new SearchSpec.Builder()
3832                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
3833                                 // Using this.childrenRankingSignals() without the context of a join
3834                                 // is invalid.
3835                                 .setRankingStrategy("sum(this.childrenRankingSignals())")
3836                                 .build());
3837         ExecutionException executionException =
3838                 assertThrows(
3839                         ExecutionException.class, () -> searchResults.getNextPageAsync().get());
3840         assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
3841         AppSearchException exception = (AppSearchException) executionException.getCause();
3842         assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT);
3843         assertThat(exception)
3844                 .hasMessageThat()
3845                 .contains("childrenRankingSignals must only be used with join");
3846     }
3847 
3848     @Test
testQuery_unsupportedAdvancedRanking()3849     public void testQuery_unsupportedAdvancedRanking() throws Exception {
3850         // Assume that advanced ranking has not been supported.
3851         assumeFalse(
3852                 mDb1.getFeatures()
3853                         .isFeatureSupported(Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION));
3854 
3855         // Schema registration
3856         mDb1.setSchemaAsync(
3857                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
3858                 .get();
3859 
3860         // Index a document
3861         AppSearchEmail inEmail =
3862                 new AppSearchEmail.Builder("namespace", "id1")
3863                         .setFrom("[email protected]")
3864                         .setTo("[email protected]", "[email protected]")
3865                         .setSubject("testPut example")
3866                         .setBody("This is the body of the testPut email")
3867                         .build();
3868         checkIsBatchResultSuccess(
3869                 mDb1.putAsync(
3870                         new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
3871 
3872         // Query for the document, and set a valid advanced ranking expression.
3873         SearchSpec searchSpec =
3874                 new SearchSpec.Builder()
3875                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
3876                         .setRankingStrategy("sqrt(4)")
3877                         .build();
3878         UnsupportedOperationException e =
3879                 assertThrows(
3880                         UnsupportedOperationException.class, () -> mDb1.search("body", searchSpec));
3881         assertThat(e)
3882                 .hasMessageThat()
3883                 .contains(
3884                         Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION
3885                                 + " is not available on this "
3886                                 + "AppSearch implementation.");
3887     }
3888 
3889     @Test
testQuery_typeFilter()3890     public void testQuery_typeFilter() throws Exception {
3891         // Schema registration
3892         AppSearchSchema genericSchema =
3893                 new AppSearchSchema.Builder("Generic")
3894                         .addProperty(
3895                                 new StringPropertyConfig.Builder("foo")
3896                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
3897                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
3898                                         .setIndexingType(
3899                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
3900                                         .build())
3901                         .build();
3902         mDb1.setSchemaAsync(
3903                         new SetSchemaRequest.Builder()
3904                                 .addSchemas(AppSearchEmail.SCHEMA)
3905                                 .addSchemas(genericSchema)
3906                                 .build())
3907                 .get();
3908 
3909         // Index a document
3910         AppSearchEmail inEmail =
3911                 new AppSearchEmail.Builder("namespace", "id1")
3912                         .setFrom("[email protected]")
3913                         .setTo("[email protected]", "[email protected]")
3914                         .setSubject("testPut example")
3915                         .setBody("This is the body of the testPut email")
3916                         .build();
3917         GenericDocument inDoc =
3918                 new GenericDocument.Builder<>("namespace", "id2", "Generic")
3919                         .setPropertyString("foo", "body")
3920                         .build();
3921         checkIsBatchResultSuccess(
3922                 mDb1.putAsync(
3923                         new PutDocumentsRequest.Builder()
3924                                 .addGenericDocuments(inEmail, inDoc)
3925                                 .build()));
3926 
3927         // Query for the documents
3928         SearchResultsShim searchResults =
3929                 mDb1.search(
3930                         "body",
3931                         new SearchSpec.Builder()
3932                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
3933                                 .build());
3934         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
3935         assertThat(documents).hasSize(2);
3936         assertThat(documents).containsExactly(inEmail, inDoc);
3937 
3938         // Query only for Document
3939         searchResults =
3940                 mDb1.search(
3941                         "body",
3942                         new SearchSpec.Builder()
3943                                 .addFilterSchemas(
3944                                         "Generic",
3945                                         "Generic") // duplicate type in filter won't matter.
3946                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
3947                                 .build());
3948         documents = convertSearchResultsToDocuments(searchResults);
3949         assertThat(documents).hasSize(1);
3950         assertThat(documents).containsExactly(inDoc);
3951 
3952         // Query only for non-existent type
3953         searchResults =
3954                 mDb1.search(
3955                         "body",
3956                         new SearchSpec.Builder()
3957                                 .addFilterSchemas("nonExistType")
3958                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
3959                                 .build());
3960         documents = convertSearchResultsToDocuments(searchResults);
3961         assertThat(documents).isEmpty();
3962     }
3963 
3964     @Test
testQuery_packageFilter()3965     public void testQuery_packageFilter() throws Exception {
3966         // Schema registration
3967         mDb1.setSchemaAsync(
3968                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
3969                 .get();
3970 
3971         // Index documents
3972         AppSearchEmail email =
3973                 new AppSearchEmail.Builder("namespace", "id1")
3974                         .setFrom("[email protected]")
3975                         .setTo("[email protected]", "[email protected]")
3976                         .setSubject("foo")
3977                         .setBody("This is the body of the testPut email")
3978                         .build();
3979         checkIsBatchResultSuccess(
3980                 mDb1.putAsync(
3981                         new PutDocumentsRequest.Builder().addGenericDocuments(email).build()));
3982 
3983         // Query for the document within our package
3984         SearchResultsShim searchResults =
3985                 mDb1.search(
3986                         "foo",
3987                         new SearchSpec.Builder()
3988                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
3989                                 .addFilterPackageNames(
3990                                         ApplicationProvider.getApplicationContext()
3991                                                 .getPackageName())
3992                                 .build());
3993         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
3994         assertThat(documents).containsExactly(email);
3995 
3996         // Query for the document in some other package, which won't exist
3997         searchResults =
3998                 mDb1.search(
3999                         "foo",
4000                         new SearchSpec.Builder()
4001                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
4002                                 .addFilterPackageNames("some.other.package")
4003                                 .build());
4004         List<SearchResult> results = searchResults.getNextPageAsync().get();
4005         assertThat(results).isEmpty();
4006     }
4007 
4008     @Test
testQuery_namespaceFilter()4009     public void testQuery_namespaceFilter() throws Exception {
4010         // Schema registration
4011         mDb1.setSchemaAsync(
4012                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
4013                 .get();
4014 
4015         // Index two documents
4016         AppSearchEmail expectedEmail =
4017                 new AppSearchEmail.Builder("expectedNamespace", "id1")
4018                         .setFrom("[email protected]")
4019                         .setTo("[email protected]", "[email protected]")
4020                         .setSubject("testPut example")
4021                         .setBody("This is the body of the testPut email")
4022                         .build();
4023         AppSearchEmail unexpectedEmail =
4024                 new AppSearchEmail.Builder("unexpectedNamespace", "id1")
4025                         .setFrom("[email protected]")
4026                         .setTo("[email protected]", "[email protected]")
4027                         .setSubject("testPut example")
4028                         .setBody("This is the body of the testPut email")
4029                         .build();
4030         checkIsBatchResultSuccess(
4031                 mDb1.putAsync(
4032                         new PutDocumentsRequest.Builder()
4033                                 .addGenericDocuments(expectedEmail, unexpectedEmail)
4034                                 .build()));
4035 
4036         // Query for all namespaces
4037         SearchResultsShim searchResults =
4038                 mDb1.search(
4039                         "body",
4040                         new SearchSpec.Builder()
4041                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
4042                                 .build());
4043         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
4044         assertThat(documents).hasSize(2);
4045         assertThat(documents).containsExactly(expectedEmail, unexpectedEmail);
4046 
4047         // Query only for expectedNamespace
4048         searchResults =
4049                 mDb1.search(
4050                         "body",
4051                         new SearchSpec.Builder()
4052                                 .addFilterNamespaces("expectedNamespace")
4053                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
4054                                 .build());
4055         documents = convertSearchResultsToDocuments(searchResults);
4056         assertThat(documents).hasSize(1);
4057         assertThat(documents).containsExactly(expectedEmail);
4058 
4059         // Query only for non-existent namespace
4060         searchResults =
4061                 mDb1.search(
4062                         "body",
4063                         new SearchSpec.Builder()
4064                                 .addFilterNamespaces("nonExistNamespace")
4065                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
4066                                 .build());
4067         documents = convertSearchResultsToDocuments(searchResults);
4068         assertThat(documents).isEmpty();
4069     }
4070 
4071     @Test
testQuery_getPackageName()4072     public void testQuery_getPackageName() throws Exception {
4073         // Schema registration
4074         mDb1.setSchemaAsync(
4075                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
4076                 .get();
4077 
4078         // Index a document
4079         AppSearchEmail inEmail =
4080                 new AppSearchEmail.Builder("namespace", "id1")
4081                         .setFrom("[email protected]")
4082                         .setTo("[email protected]", "[email protected]")
4083                         .setSubject("testPut example")
4084                         .setBody("This is the body of the testPut email")
4085                         .build();
4086         checkIsBatchResultSuccess(
4087                 mDb1.putAsync(
4088                         new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
4089 
4090         // Query for the document
4091         SearchResultsShim searchResults =
4092                 mDb1.search(
4093                         "body",
4094                         new SearchSpec.Builder()
4095                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
4096                                 .build());
4097 
4098         List<SearchResult> results;
4099         List<GenericDocument> documents = new ArrayList<>();
4100         // keep loading next page until it's empty.
4101         do {
4102             results = searchResults.getNextPageAsync().get();
4103             for (SearchResult result : results) {
4104                 assertThat(result.getGenericDocument()).isEqualTo(inEmail);
4105                 assertThat(result.getPackageName())
4106                         .isEqualTo(ApplicationProvider.getApplicationContext().getPackageName());
4107                 documents.add(result.getGenericDocument());
4108             }
4109         } while (results.size() > 0);
4110         assertThat(documents).hasSize(1);
4111     }
4112 
4113     @Test
testQuery_getDatabaseName()4114     public void testQuery_getDatabaseName() throws Exception {
4115         // Schema registration
4116         mDb1.setSchemaAsync(
4117                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
4118                 .get();
4119 
4120         // Index a document
4121         AppSearchEmail inEmail =
4122                 new AppSearchEmail.Builder("namespace", "id1")
4123                         .setFrom("[email protected]")
4124                         .setTo("[email protected]", "[email protected]")
4125                         .setSubject("testPut example")
4126                         .setBody("This is the body of the testPut email")
4127                         .build();
4128         checkIsBatchResultSuccess(
4129                 mDb1.putAsync(
4130                         new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
4131 
4132         // Query for the document
4133         SearchResultsShim searchResults =
4134                 mDb1.search(
4135                         "body",
4136                         new SearchSpec.Builder()
4137                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
4138                                 .build());
4139 
4140         List<SearchResult> results;
4141         List<GenericDocument> documents = new ArrayList<>();
4142         // keep loading next page until it's empty.
4143         do {
4144             results = searchResults.getNextPageAsync().get();
4145             for (SearchResult result : results) {
4146                 assertThat(result.getGenericDocument()).isEqualTo(inEmail);
4147                 assertThat(result.getDatabaseName()).isEqualTo(DB_NAME_1);
4148                 documents.add(result.getGenericDocument());
4149             }
4150         } while (results.size() > 0);
4151         assertThat(documents).hasSize(1);
4152 
4153         // Schema registration for another database
4154         mDb2.setSchemaAsync(
4155                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
4156                 .get();
4157 
4158         checkIsBatchResultSuccess(
4159                 mDb2.putAsync(
4160                         new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
4161 
4162         // Query for the document
4163         searchResults =
4164                 mDb2.search(
4165                         "body",
4166                         new SearchSpec.Builder()
4167                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
4168                                 .build());
4169 
4170         documents = new ArrayList<>();
4171         // keep loading next page until it's empty.
4172         do {
4173             results = searchResults.getNextPageAsync().get();
4174             for (SearchResult result : results) {
4175                 assertThat(result.getGenericDocument()).isEqualTo(inEmail);
4176                 assertThat(result.getDatabaseName()).isEqualTo(DB_NAME_2);
4177                 documents.add(result.getGenericDocument());
4178             }
4179         } while (results.size() > 0);
4180         assertThat(documents).hasSize(1);
4181     }
4182 
4183     @Test
testQuery_projection()4184     public void testQuery_projection() throws Exception {
4185         // Schema registration
4186         mDb1.setSchemaAsync(
4187                         new SetSchemaRequest.Builder()
4188                                 .addSchemas(AppSearchEmail.SCHEMA)
4189                                 .addSchemas(
4190                                         new AppSearchSchema.Builder("Note")
4191                                                 .addProperty(
4192                                                         new StringPropertyConfig.Builder("title")
4193                                                                 .setCardinality(
4194                                                                         PropertyConfig
4195                                                                                 .CARDINALITY_REQUIRED)
4196                                                                 .setIndexingType(
4197                                                                         StringPropertyConfig
4198                                                                                 .INDEXING_TYPE_EXACT_TERMS)
4199                                                                 .setTokenizerType(
4200                                                                         StringPropertyConfig
4201                                                                                 .TOKENIZER_TYPE_PLAIN)
4202                                                                 .build())
4203                                                 .addProperty(
4204                                                         new StringPropertyConfig.Builder("body")
4205                                                                 .setCardinality(
4206                                                                         PropertyConfig
4207                                                                                 .CARDINALITY_REQUIRED)
4208                                                                 .setIndexingType(
4209                                                                         StringPropertyConfig
4210                                                                                 .INDEXING_TYPE_EXACT_TERMS)
4211                                                                 .setTokenizerType(
4212                                                                         StringPropertyConfig
4213                                                                                 .TOKENIZER_TYPE_PLAIN)
4214                                                                 .build())
4215                                                 .build())
4216                                 .build())
4217                 .get();
4218 
4219         // Index two documents
4220         AppSearchEmail email =
4221                 new AppSearchEmail.Builder("namespace", "id1")
4222                         .setCreationTimestampMillis(1000)
4223                         .setFrom("[email protected]")
4224                         .setTo("[email protected]", "[email protected]")
4225                         .setSubject("testPut example")
4226                         .setBody("This is the body of the testPut email")
4227                         .build();
4228         GenericDocument note =
4229                 new GenericDocument.Builder<>("namespace", "id2", "Note")
4230                         .setCreationTimestampMillis(1000)
4231                         .setPropertyString("title", "Note title")
4232                         .setPropertyString("body", "Note body")
4233                         .build();
4234         checkIsBatchResultSuccess(
4235                 mDb1.putAsync(
4236                         new PutDocumentsRequest.Builder()
4237                                 .addGenericDocuments(email, note)
4238                                 .build()));
4239 
4240         // Query with type property paths {"Email", ["body", "to"]}
4241         SearchResultsShim searchResults =
4242                 mDb1.search(
4243                         "body",
4244                         new SearchSpec.Builder()
4245                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
4246                                 .addProjection(
4247                                         AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("body", "to"))
4248                                 .build());
4249         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
4250 
4251         // The email document should have been returned with only the "body" and "to"
4252         // properties. The note document should have been returned with all of its properties.
4253         AppSearchEmail expectedEmail =
4254                 new AppSearchEmail.Builder("namespace", "id1")
4255                         .setCreationTimestampMillis(1000)
4256                         .setTo("[email protected]", "[email protected]")
4257                         .setBody("This is the body of the testPut email")
4258                         .build();
4259         GenericDocument expectedNote =
4260                 new GenericDocument.Builder<>("namespace", "id2", "Note")
4261                         .setCreationTimestampMillis(1000)
4262                         .setPropertyString("title", "Note title")
4263                         .setPropertyString("body", "Note body")
4264                         .build();
4265         assertThat(documents).containsExactly(expectedNote, expectedEmail);
4266     }
4267 
4268     @Test
testQuery_projectionEmpty()4269     public void testQuery_projectionEmpty() throws Exception {
4270         // Schema registration
4271         mDb1.setSchemaAsync(
4272                         new SetSchemaRequest.Builder()
4273                                 .addSchemas(AppSearchEmail.SCHEMA)
4274                                 .addSchemas(
4275                                         new AppSearchSchema.Builder("Note")
4276                                                 .addProperty(
4277                                                         new StringPropertyConfig.Builder("title")
4278                                                                 .setCardinality(
4279                                                                         PropertyConfig
4280                                                                                 .CARDINALITY_REQUIRED)
4281                                                                 .setIndexingType(
4282                                                                         StringPropertyConfig
4283                                                                                 .INDEXING_TYPE_EXACT_TERMS)
4284                                                                 .setTokenizerType(
4285                                                                         StringPropertyConfig
4286                                                                                 .TOKENIZER_TYPE_PLAIN)
4287                                                                 .build())
4288                                                 .addProperty(
4289                                                         new StringPropertyConfig.Builder("body")
4290                                                                 .setCardinality(
4291                                                                         PropertyConfig
4292                                                                                 .CARDINALITY_REQUIRED)
4293                                                                 .setIndexingType(
4294                                                                         StringPropertyConfig
4295                                                                                 .INDEXING_TYPE_EXACT_TERMS)
4296                                                                 .setTokenizerType(
4297                                                                         StringPropertyConfig
4298                                                                                 .TOKENIZER_TYPE_PLAIN)
4299                                                                 .build())
4300                                                 .build())
4301                                 .build())
4302                 .get();
4303 
4304         // Index two documents
4305         AppSearchEmail email =
4306                 new AppSearchEmail.Builder("namespace", "id1")
4307                         .setCreationTimestampMillis(1000)
4308                         .setFrom("[email protected]")
4309                         .setTo("[email protected]", "[email protected]")
4310                         .setSubject("testPut example")
4311                         .setBody("This is the body of the testPut email")
4312                         .build();
4313         GenericDocument note =
4314                 new GenericDocument.Builder<>("namespace", "id2", "Note")
4315                         .setCreationTimestampMillis(1000)
4316                         .setPropertyString("title", "Note title")
4317                         .setPropertyString("body", "Note body")
4318                         .build();
4319         checkIsBatchResultSuccess(
4320                 mDb1.putAsync(
4321                         new PutDocumentsRequest.Builder()
4322                                 .addGenericDocuments(email, note)
4323                                 .build()));
4324 
4325         // Query with type property paths {"Email", []}
4326         SearchResultsShim searchResults =
4327                 mDb1.search(
4328                         "body",
4329                         new SearchSpec.Builder()
4330                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
4331                                 .addProjection(AppSearchEmail.SCHEMA_TYPE, Collections.emptyList())
4332                                 .build());
4333         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
4334 
4335         // The email document should have been returned without any properties. The note document
4336         // should have been returned with all of its properties.
4337         AppSearchEmail expectedEmail =
4338                 new AppSearchEmail.Builder("namespace", "id1")
4339                         .setCreationTimestampMillis(1000)
4340                         .build();
4341         GenericDocument expectedNote =
4342                 new GenericDocument.Builder<>("namespace", "id2", "Note")
4343                         .setCreationTimestampMillis(1000)
4344                         .setPropertyString("title", "Note title")
4345                         .setPropertyString("body", "Note body")
4346                         .build();
4347         assertThat(documents).containsExactly(expectedNote, expectedEmail);
4348     }
4349 
4350     @Test
testQuery_projectionNonExistentType()4351     public void testQuery_projectionNonExistentType() throws Exception {
4352         // Schema registration
4353         mDb1.setSchemaAsync(
4354                         new SetSchemaRequest.Builder()
4355                                 .addSchemas(AppSearchEmail.SCHEMA)
4356                                 .addSchemas(
4357                                         new AppSearchSchema.Builder("Note")
4358                                                 .addProperty(
4359                                                         new StringPropertyConfig.Builder("title")
4360                                                                 .setCardinality(
4361                                                                         PropertyConfig
4362                                                                                 .CARDINALITY_REQUIRED)
4363                                                                 .setIndexingType(
4364                                                                         StringPropertyConfig
4365                                                                                 .INDEXING_TYPE_EXACT_TERMS)
4366                                                                 .setTokenizerType(
4367                                                                         StringPropertyConfig
4368                                                                                 .TOKENIZER_TYPE_PLAIN)
4369                                                                 .build())
4370                                                 .addProperty(
4371                                                         new StringPropertyConfig.Builder("body")
4372                                                                 .setCardinality(
4373                                                                         PropertyConfig
4374                                                                                 .CARDINALITY_REQUIRED)
4375                                                                 .setIndexingType(
4376                                                                         StringPropertyConfig
4377                                                                                 .INDEXING_TYPE_EXACT_TERMS)
4378                                                                 .setTokenizerType(
4379                                                                         StringPropertyConfig
4380                                                                                 .TOKENIZER_TYPE_PLAIN)
4381                                                                 .build())
4382                                                 .build())
4383                                 .build())
4384                 .get();
4385 
4386         // Index two documents
4387         AppSearchEmail email =
4388                 new AppSearchEmail.Builder("namespace", "id1")
4389                         .setCreationTimestampMillis(1000)
4390                         .setFrom("[email protected]")
4391                         .setTo("[email protected]", "[email protected]")
4392                         .setSubject("testPut example")
4393                         .setBody("This is the body of the testPut email")
4394                         .build();
4395         GenericDocument note =
4396                 new GenericDocument.Builder<>("namespace", "id2", "Note")
4397                         .setCreationTimestampMillis(1000)
4398                         .setPropertyString("title", "Note title")
4399                         .setPropertyString("body", "Note body")
4400                         .build();
4401         checkIsBatchResultSuccess(
4402                 mDb1.putAsync(
4403                         new PutDocumentsRequest.Builder()
4404                                 .addGenericDocuments(email, note)
4405                                 .build()));
4406 
4407         // Query with type property paths {"NonExistentType", []}, {"Email", ["body", "to"]}
4408         SearchResultsShim searchResults =
4409                 mDb1.search(
4410                         "body",
4411                         new SearchSpec.Builder()
4412                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
4413                                 .addProjection("NonExistentType", Collections.emptyList())
4414                                 .addProjection(
4415                                         AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("body", "to"))
4416                                 .build());
4417         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
4418 
4419         // The email document should have been returned with only the "body" and "to" properties.
4420         // The note document should have been returned with all of its properties.
4421         AppSearchEmail expectedEmail =
4422                 new AppSearchEmail.Builder("namespace", "id1")
4423                         .setCreationTimestampMillis(1000)
4424                         .setTo("[email protected]", "[email protected]")
4425                         .setBody("This is the body of the testPut email")
4426                         .build();
4427         GenericDocument expectedNote =
4428                 new GenericDocument.Builder<>("namespace", "id2", "Note")
4429                         .setCreationTimestampMillis(1000)
4430                         .setPropertyString("title", "Note title")
4431                         .setPropertyString("body", "Note body")
4432                         .build();
4433         assertThat(documents).containsExactly(expectedNote, expectedEmail);
4434     }
4435 
4436     @Test
testQuery_wildcardProjection()4437     public void testQuery_wildcardProjection() throws Exception {
4438         // Schema registration
4439         mDb1.setSchemaAsync(
4440                         new SetSchemaRequest.Builder()
4441                                 .addSchemas(AppSearchEmail.SCHEMA)
4442                                 .addSchemas(
4443                                         new AppSearchSchema.Builder("Note")
4444                                                 .addProperty(
4445                                                         new StringPropertyConfig.Builder("title")
4446                                                                 .setCardinality(
4447                                                                         PropertyConfig
4448                                                                                 .CARDINALITY_REQUIRED)
4449                                                                 .setIndexingType(
4450                                                                         StringPropertyConfig
4451                                                                                 .INDEXING_TYPE_EXACT_TERMS)
4452                                                                 .setTokenizerType(
4453                                                                         StringPropertyConfig
4454                                                                                 .TOKENIZER_TYPE_PLAIN)
4455                                                                 .build())
4456                                                 .addProperty(
4457                                                         new StringPropertyConfig.Builder("body")
4458                                                                 .setCardinality(
4459                                                                         PropertyConfig
4460                                                                                 .CARDINALITY_REQUIRED)
4461                                                                 .setIndexingType(
4462                                                                         StringPropertyConfig
4463                                                                                 .INDEXING_TYPE_EXACT_TERMS)
4464                                                                 .setTokenizerType(
4465                                                                         StringPropertyConfig
4466                                                                                 .TOKENIZER_TYPE_PLAIN)
4467                                                                 .build())
4468                                                 .build())
4469                                 .build())
4470                 .get();
4471 
4472         // Index two documents
4473         AppSearchEmail email =
4474                 new AppSearchEmail.Builder("namespace", "id1")
4475                         .setCreationTimestampMillis(1000)
4476                         .setFrom("[email protected]")
4477                         .setTo("[email protected]", "[email protected]")
4478                         .setSubject("testPut example")
4479                         .setBody("This is the body of the testPut email")
4480                         .build();
4481         GenericDocument note =
4482                 new GenericDocument.Builder<>("namespace", "id2", "Note")
4483                         .setCreationTimestampMillis(1000)
4484                         .setPropertyString("title", "Note title")
4485                         .setPropertyString("body", "Note body")
4486                         .build();
4487         checkIsBatchResultSuccess(
4488                 mDb1.putAsync(
4489                         new PutDocumentsRequest.Builder()
4490                                 .addGenericDocuments(email, note)
4491                                 .build()));
4492 
4493         // Query with type property paths {"*", ["body", "to"]}
4494         SearchResultsShim searchResults =
4495                 mDb1.search(
4496                         "body",
4497                         new SearchSpec.Builder()
4498                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
4499                                 .addProjection(
4500                                         SearchSpec.SCHEMA_TYPE_WILDCARD,
4501                                         ImmutableList.of("body", "to"))
4502                                 .build());
4503         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
4504 
4505         // The email document should have been returned with only the "body" and "to"
4506         // properties. The note document should have been returned with only the "body" property.
4507         AppSearchEmail expectedEmail =
4508                 new AppSearchEmail.Builder("namespace", "id1")
4509                         .setCreationTimestampMillis(1000)
4510                         .setTo("[email protected]", "[email protected]")
4511                         .setBody("This is the body of the testPut email")
4512                         .build();
4513         GenericDocument expectedNote =
4514                 new GenericDocument.Builder<>("namespace", "id2", "Note")
4515                         .setCreationTimestampMillis(1000)
4516                         .setPropertyString("body", "Note body")
4517                         .build();
4518         assertThat(documents).containsExactly(expectedNote, expectedEmail);
4519     }
4520 
4521     @Test
testQuery_wildcardProjectionEmpty()4522     public void testQuery_wildcardProjectionEmpty() throws Exception {
4523         // Schema registration
4524         mDb1.setSchemaAsync(
4525                         new SetSchemaRequest.Builder()
4526                                 .addSchemas(AppSearchEmail.SCHEMA)
4527                                 .addSchemas(
4528                                         new AppSearchSchema.Builder("Note")
4529                                                 .addProperty(
4530                                                         new StringPropertyConfig.Builder("title")
4531                                                                 .setCardinality(
4532                                                                         PropertyConfig
4533                                                                                 .CARDINALITY_REQUIRED)
4534                                                                 .setIndexingType(
4535                                                                         StringPropertyConfig
4536                                                                                 .INDEXING_TYPE_EXACT_TERMS)
4537                                                                 .setTokenizerType(
4538                                                                         StringPropertyConfig
4539                                                                                 .TOKENIZER_TYPE_PLAIN)
4540                                                                 .build())
4541                                                 .addProperty(
4542                                                         new StringPropertyConfig.Builder("body")
4543                                                                 .setCardinality(
4544                                                                         PropertyConfig
4545                                                                                 .CARDINALITY_REQUIRED)
4546                                                                 .setIndexingType(
4547                                                                         StringPropertyConfig
4548                                                                                 .INDEXING_TYPE_EXACT_TERMS)
4549                                                                 .setTokenizerType(
4550                                                                         StringPropertyConfig
4551                                                                                 .TOKENIZER_TYPE_PLAIN)
4552                                                                 .build())
4553                                                 .build())
4554                                 .build())
4555                 .get();
4556 
4557         // Index two documents
4558         AppSearchEmail email =
4559                 new AppSearchEmail.Builder("namespace", "id1")
4560                         .setCreationTimestampMillis(1000)
4561                         .setFrom("[email protected]")
4562                         .setTo("[email protected]", "[email protected]")
4563                         .setSubject("testPut example")
4564                         .setBody("This is the body of the testPut email")
4565                         .build();
4566         GenericDocument note =
4567                 new GenericDocument.Builder<>("namespace", "id2", "Note")
4568                         .setCreationTimestampMillis(1000)
4569                         .setPropertyString("title", "Note title")
4570                         .setPropertyString("body", "Note body")
4571                         .build();
4572         checkIsBatchResultSuccess(
4573                 mDb1.putAsync(
4574                         new PutDocumentsRequest.Builder()
4575                                 .addGenericDocuments(email, note)
4576                                 .build()));
4577 
4578         // Query with type property paths {"*", []}
4579         SearchResultsShim searchResults =
4580                 mDb1.search(
4581                         "body",
4582                         new SearchSpec.Builder()
4583                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
4584                                 .addProjection(
4585                                         SearchSpec.SCHEMA_TYPE_WILDCARD, Collections.emptyList())
4586                                 .build());
4587         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
4588 
4589         // The email and note documents should have been returned without any properties.
4590         AppSearchEmail expectedEmail =
4591                 new AppSearchEmail.Builder("namespace", "id1")
4592                         .setCreationTimestampMillis(1000)
4593                         .build();
4594         GenericDocument expectedNote =
4595                 new GenericDocument.Builder<>("namespace", "id2", "Note")
4596                         .setCreationTimestampMillis(1000)
4597                         .build();
4598         assertThat(documents).containsExactly(expectedNote, expectedEmail);
4599     }
4600 
4601     @Test
testQuery_wildcardProjectionNonExistentType()4602     public void testQuery_wildcardProjectionNonExistentType() throws Exception {
4603         // Schema registration
4604         mDb1.setSchemaAsync(
4605                         new SetSchemaRequest.Builder()
4606                                 .addSchemas(AppSearchEmail.SCHEMA)
4607                                 .addSchemas(
4608                                         new AppSearchSchema.Builder("Note")
4609                                                 .addProperty(
4610                                                         new StringPropertyConfig.Builder("title")
4611                                                                 .setCardinality(
4612                                                                         PropertyConfig
4613                                                                                 .CARDINALITY_REQUIRED)
4614                                                                 .setIndexingType(
4615                                                                         StringPropertyConfig
4616                                                                                 .INDEXING_TYPE_EXACT_TERMS)
4617                                                                 .setTokenizerType(
4618                                                                         StringPropertyConfig
4619                                                                                 .TOKENIZER_TYPE_PLAIN)
4620                                                                 .build())
4621                                                 .addProperty(
4622                                                         new StringPropertyConfig.Builder("body")
4623                                                                 .setCardinality(
4624                                                                         PropertyConfig
4625                                                                                 .CARDINALITY_REQUIRED)
4626                                                                 .setIndexingType(
4627                                                                         StringPropertyConfig
4628                                                                                 .INDEXING_TYPE_EXACT_TERMS)
4629                                                                 .setTokenizerType(
4630                                                                         StringPropertyConfig
4631                                                                                 .TOKENIZER_TYPE_PLAIN)
4632                                                                 .build())
4633                                                 .build())
4634                                 .build())
4635                 .get();
4636 
4637         // Index two documents
4638         AppSearchEmail email =
4639                 new AppSearchEmail.Builder("namespace", "id1")
4640                         .setCreationTimestampMillis(1000)
4641                         .setFrom("[email protected]")
4642                         .setTo("[email protected]", "[email protected]")
4643                         .setSubject("testPut example")
4644                         .setBody("This is the body of the testPut email")
4645                         .build();
4646         GenericDocument note =
4647                 new GenericDocument.Builder<>("namespace", "id2", "Note")
4648                         .setCreationTimestampMillis(1000)
4649                         .setPropertyString("title", "Note title")
4650                         .setPropertyString("body", "Note body")
4651                         .build();
4652         checkIsBatchResultSuccess(
4653                 mDb1.putAsync(
4654                         new PutDocumentsRequest.Builder()
4655                                 .addGenericDocuments(email, note)
4656                                 .build()));
4657 
4658         // Query with type property paths {"NonExistentType", []}, {"*", ["body", "to"]}
4659         SearchResultsShim searchResults =
4660                 mDb1.search(
4661                         "body",
4662                         new SearchSpec.Builder()
4663                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
4664                                 .addProjection("NonExistentType", Collections.emptyList())
4665                                 .addProjection(
4666                                         SearchSpec.SCHEMA_TYPE_WILDCARD,
4667                                         ImmutableList.of("body", "to"))
4668                                 .build());
4669         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
4670 
4671         // The email document should have been returned with only the "body" and "to"
4672         // properties. The note document should have been returned with only the "body" property.
4673         AppSearchEmail expectedEmail =
4674                 new AppSearchEmail.Builder("namespace", "id1")
4675                         .setCreationTimestampMillis(1000)
4676                         .setTo("[email protected]", "[email protected]")
4677                         .setBody("This is the body of the testPut email")
4678                         .build();
4679         GenericDocument expectedNote =
4680                 new GenericDocument.Builder<>("namespace", "id2", "Note")
4681                         .setCreationTimestampMillis(1000)
4682                         .setPropertyString("body", "Note body")
4683                         .build();
4684         assertThat(documents).containsExactly(expectedNote, expectedEmail);
4685     }
4686 
4687     @Test
4688     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SEARCH_SPEC_FILTER_DOCUMENT_IDS)
testQuery_documentIdFilter()4689     public void testQuery_documentIdFilter() throws Exception {
4690         assumeTrue(
4691                 mDb1.getFeatures()
4692                         .isFeatureSupported(Features.SEARCH_SPEC_ADD_FILTER_DOCUMENT_IDS));
4693 
4694         // Schema registration
4695         mDb1.setSchemaAsync(
4696                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
4697                 .get();
4698 
4699         // Index 3 documents
4700         AppSearchEmail email1 =
4701                 new AppSearchEmail.Builder("namespace", "id1")
4702                         .setFrom("[email protected]")
4703                         .setTo("[email protected]", "[email protected]")
4704                         .setSubject("testPut example")
4705                         .setBody("This is the body of the testPut email")
4706                         .build();
4707         AppSearchEmail email2 =
4708                 new AppSearchEmail.Builder("namespace", "id2")
4709                         .setFrom("[email protected]")
4710                         .setTo("[email protected]", "[email protected]")
4711                         .setSubject("testPut example")
4712                         .setBody("This is the body of the testPut email")
4713                         .build();
4714         AppSearchEmail email3 =
4715                 new AppSearchEmail.Builder("namespace", "id3")
4716                         .setFrom("[email protected]")
4717                         .setTo("[email protected]", "[email protected]")
4718                         .setSubject("testPut example")
4719                         .setBody("This is the body of the testPut email")
4720                         .build();
4721         checkIsBatchResultSuccess(
4722                 mDb1.putAsync(
4723                         new PutDocumentsRequest.Builder()
4724                                 .addGenericDocuments(email1, email2, email3)
4725                                 .build()));
4726 
4727         // Query for all ids by an empty document id filter.
4728         SearchResultsShim searchResults =
4729                 mDb1.search(
4730                         "body",
4731                         new SearchSpec.Builder()
4732                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
4733                                 .build());
4734         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
4735         assertThat(documents).hasSize(3);
4736         assertThat(documents).containsExactly(email1, email2, email3);
4737 
4738         // Query for all ids by explicitly specifying them.
4739         searchResults =
4740                 mDb1.search(
4741                         "body",
4742                         new SearchSpec.Builder()
4743                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
4744                                 .addFilterDocumentIds(ImmutableSet.of("id1", "id2", "id3"))
4745                                 .build());
4746         documents = convertSearchResultsToDocuments(searchResults);
4747         assertThat(documents).hasSize(3);
4748         assertThat(documents).containsExactly(email1, email2, email3);
4749 
4750         // Query only for id1
4751         searchResults =
4752                 mDb1.search(
4753                         "body",
4754                         new SearchSpec.Builder()
4755                                 .addFilterDocumentIds(ImmutableSet.of("id1"))
4756                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
4757                                 .build());
4758         documents = convertSearchResultsToDocuments(searchResults);
4759         assertThat(documents).hasSize(1);
4760         assertThat(documents).containsExactly(email1);
4761 
4762         // Query only for id1 and id3
4763         searchResults =
4764                 mDb1.search(
4765                         "body",
4766                         new SearchSpec.Builder()
4767                                 .addFilterDocumentIds(ImmutableSet.of("id1"))
4768                                 .addFilterDocumentIds(ImmutableSet.of("id3"))
4769                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
4770                                 .build());
4771         documents = convertSearchResultsToDocuments(searchResults);
4772         assertThat(documents).hasSize(2);
4773         assertThat(documents).containsExactly(email1, email3);
4774     }
4775 
4776     @Test
4777     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SEARCH_SPEC_FILTER_DOCUMENT_IDS)
testQuery_documentIdFilter_withNamespaceFilter()4778     public void testQuery_documentIdFilter_withNamespaceFilter() throws Exception {
4779         assumeTrue(
4780                 mDb1.getFeatures()
4781                         .isFeatureSupported(Features.SEARCH_SPEC_ADD_FILTER_DOCUMENT_IDS));
4782 
4783         // Schema registration
4784         mDb1.setSchemaAsync(
4785                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
4786                 .get();
4787 
4788         // Index 3 documents with "id1" in 2 different namespaces
4789         AppSearchEmail email1 =
4790                 new AppSearchEmail.Builder("namespace1", "id1")
4791                         .setFrom("[email protected]")
4792                         .setTo("[email protected]", "[email protected]")
4793                         .setSubject("testPut example")
4794                         .setBody("This is the body of the testPut email")
4795                         .build();
4796         AppSearchEmail email2 =
4797                 new AppSearchEmail.Builder("namespace1", "id2")
4798                         .setFrom("[email protected]")
4799                         .setTo("[email protected]", "[email protected]")
4800                         .setSubject("testPut example")
4801                         .setBody("This is the body of the testPut email")
4802                         .build();
4803         AppSearchEmail email3 =
4804                 new AppSearchEmail.Builder("namespace2", "id1")
4805                         .setFrom("[email protected]")
4806                         .setTo("[email protected]", "[email protected]")
4807                         .setSubject("testPut example")
4808                         .setBody("This is the body of the testPut email")
4809                         .build();
4810         checkIsBatchResultSuccess(
4811                 mDb1.putAsync(
4812                         new PutDocumentsRequest.Builder()
4813                                 .addGenericDocuments(email1, email2, email3)
4814                                 .build()));
4815 
4816         // Query for id1
4817         SearchResultsShim searchResults =
4818                 mDb1.search(
4819                         "body",
4820                         new SearchSpec.Builder()
4821                                 .addFilterDocumentIds(ImmutableSet.of("id1"))
4822                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
4823                                 .build());
4824         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
4825         assertThat(documents).hasSize(2);
4826         assertThat(documents).containsExactly(email1, email3);
4827 
4828         // Query only for id1 in namespace1
4829         searchResults =
4830                 mDb1.search(
4831                         "body",
4832                         new SearchSpec.Builder()
4833                                 .addFilterDocumentIds(ImmutableSet.of("id1"))
4834                                 .addFilterNamespaces("namespace1")
4835                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
4836                                 .build());
4837         documents = convertSearchResultsToDocuments(searchResults);
4838         assertThat(documents).hasSize(1);
4839         assertThat(documents).containsExactly(email1);
4840     }
4841 
4842     @Test
4843     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SEARCH_SPEC_FILTER_DOCUMENT_IDS)
testQuery_documentIdFilter_nonExistentId()4844     public void testQuery_documentIdFilter_nonExistentId() throws Exception {
4845         assumeTrue(
4846                 mDb1.getFeatures()
4847                         .isFeatureSupported(Features.SEARCH_SPEC_ADD_FILTER_DOCUMENT_IDS));
4848 
4849         // Schema registration
4850         mDb1.setSchemaAsync(
4851                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
4852                 .get();
4853 
4854         // Index 3 documents
4855         AppSearchEmail email1 =
4856                 new AppSearchEmail.Builder("namespace", "id1")
4857                         .setFrom("[email protected]")
4858                         .setTo("[email protected]", "[email protected]")
4859                         .setSubject("testPut example")
4860                         .setBody("This is the body of the testPut email")
4861                         .build();
4862         AppSearchEmail email2 =
4863                 new AppSearchEmail.Builder("namespace", "id2")
4864                         .setFrom("[email protected]")
4865                         .setTo("[email protected]", "[email protected]")
4866                         .setSubject("testPut example")
4867                         .setBody("This is the body of the testPut email")
4868                         .build();
4869         AppSearchEmail email3 =
4870                 new AppSearchEmail.Builder("namespace", "id3")
4871                         .setFrom("[email protected]")
4872                         .setTo("[email protected]", "[email protected]")
4873                         .setSubject("testPut example")
4874                         .setBody("This is the body of the testPut email")
4875                         .build();
4876         checkIsBatchResultSuccess(
4877                 mDb1.putAsync(
4878                         new PutDocumentsRequest.Builder()
4879                                 .addGenericDocuments(email1, email2, email3)
4880                                 .build()));
4881 
4882         // Query for a non-existent id, which should return nothing.
4883         SearchResultsShim searchResults =
4884                 mDb1.search(
4885                         "body",
4886                         new SearchSpec.Builder()
4887                                 .addFilterDocumentIds(ImmutableSet.of("nonExistId"))
4888                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
4889                                 .build());
4890         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
4891         assertThat(documents).isEmpty();
4892     }
4893 
4894     @Test
4895     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SEARCH_SPEC_FILTER_DOCUMENT_IDS)
testQuery_documentIdFilter_notSupported()4896     public void testQuery_documentIdFilter_notSupported() throws Exception {
4897         assumeFalse(
4898                 mDb1.getFeatures()
4899                         .isFeatureSupported(Features.SEARCH_SPEC_ADD_FILTER_DOCUMENT_IDS));
4900 
4901         UnsupportedOperationException exception =
4902                 assertThrows(
4903                         UnsupportedOperationException.class,
4904                         () ->
4905                                 mDb1.search(
4906                                         "body",
4907                                         new SearchSpec.Builder()
4908                                                 .addFilterDocumentIds(ImmutableSet.of("id1"))
4909                                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
4910                                                 .build()));
4911         assertThat(exception)
4912                 .hasMessageThat()
4913                 .contains(
4914                         Features.SEARCH_SPEC_ADD_FILTER_DOCUMENT_IDS
4915                                 + " is not available on this AppSearch implementation.");
4916     }
4917 
4918     @Test
testSearchSpec_setSourceTag_notSupported()4919     public void testSearchSpec_setSourceTag_notSupported() {
4920         assumeFalse(
4921                 mDb1.getFeatures()
4922                         .isFeatureSupported(Features.SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG));
4923         // UnsupportedOperationException will be thrown with these queries so no need to
4924         // define a schema and index document.
4925         SearchSpec.Builder builder = new SearchSpec.Builder();
4926         SearchSpec searchSpec = builder.setSearchSourceLogTag("tag").build();
4927 
4928         UnsupportedOperationException exception =
4929                 assertThrows(
4930                         UnsupportedOperationException.class,
4931                         () -> mDb1.search("\"Hello, world!\"", searchSpec));
4932         assertThat(exception)
4933                 .hasMessageThat()
4934                 .contains(
4935                         Features.SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG
4936                                 + " is not available on this AppSearch implementation.");
4937     }
4938 
4939     @Test
testQuery_twoInstances()4940     public void testQuery_twoInstances() throws Exception {
4941         // Schema registration
4942         mDb1.setSchemaAsync(
4943                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
4944                 .get();
4945         mDb2.setSchemaAsync(
4946                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
4947                 .get();
4948 
4949         // Index a document to instance 1.
4950         AppSearchEmail inEmail1 =
4951                 new AppSearchEmail.Builder("namespace", "id1")
4952                         .setFrom("[email protected]")
4953                         .setTo("[email protected]", "[email protected]")
4954                         .setSubject("testPut example")
4955                         .setBody("This is the body of the testPut email")
4956                         .build();
4957         checkIsBatchResultSuccess(
4958                 mDb1.putAsync(
4959                         new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build()));
4960 
4961         // Index a document to instance 2.
4962         AppSearchEmail inEmail2 =
4963                 new AppSearchEmail.Builder("namespace", "id2")
4964                         .setFrom("[email protected]")
4965                         .setTo("[email protected]", "[email protected]")
4966                         .setSubject("testPut example")
4967                         .setBody("This is the body of the testPut email")
4968                         .build();
4969         checkIsBatchResultSuccess(
4970                 mDb2.putAsync(
4971                         new PutDocumentsRequest.Builder().addGenericDocuments(inEmail2).build()));
4972 
4973         // Query for instance 1.
4974         SearchResultsShim searchResults =
4975                 mDb1.search(
4976                         "body",
4977                         new SearchSpec.Builder()
4978                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
4979                                 .build());
4980         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
4981         assertThat(documents).hasSize(1);
4982         assertThat(documents).containsExactly(inEmail1);
4983 
4984         // Query for instance 2.
4985         searchResults =
4986                 mDb2.search(
4987                         "body",
4988                         new SearchSpec.Builder()
4989                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
4990                                 .build());
4991         documents = convertSearchResultsToDocuments(searchResults);
4992         assertThat(documents).hasSize(1);
4993         assertThat(documents).containsExactly(inEmail2);
4994     }
4995 
4996     @Test
testQuery_typePropertyFilters()4997     public void testQuery_typePropertyFilters() throws Exception {
4998         assumeTrue(
4999                 mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
5000         // Schema registration
5001         mDb1.setSchemaAsync(
5002                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
5003                 .get();
5004 
5005         // Index two documents
5006         AppSearchEmail email1 =
5007                 new AppSearchEmail.Builder("namespace", "id1")
5008                         .setCreationTimestampMillis(1000)
5009                         .setFrom("[email protected]")
5010                         .setTo("[email protected]", "[email protected]")
5011                         .setSubject("testPut example")
5012                         .setBody("This is the body of the testPut email")
5013                         .build();
5014         AppSearchEmail email2 =
5015                 new AppSearchEmail.Builder("namespace", "id2")
5016                         .setCreationTimestampMillis(1000)
5017                         .setFrom("[email protected]")
5018                         .setTo("[email protected]", "[email protected]")
5019                         .setSubject("testPut example subject with some body")
5020                         .setBody("This is the body of the testPut email")
5021                         .build();
5022         checkIsBatchResultSuccess(
5023                 mDb1.putAsync(
5024                         new PutDocumentsRequest.Builder()
5025                                 .addGenericDocuments(email1, email2)
5026                                 .build()));
5027 
5028         // Query with type property filters {"Email", ["subject", "to"]}
5029         SearchResultsShim searchResults =
5030                 mDb1.search(
5031                         "body",
5032                         new SearchSpec.Builder()
5033                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
5034                                 .addFilterProperties(
5035                                         AppSearchEmail.SCHEMA_TYPE,
5036                                         ImmutableList.of("subject", "to"))
5037                                 .build());
5038         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
5039         // Only email2 should be returned because email1 doesn't have the term "body" in subject
5040         // or to fields
5041         assertThat(documents).containsExactly(email2);
5042     }
5043 
5044     @Test
testQuery_typePropertyFiltersWithDifferentSchemaTypes()5045     public void testQuery_typePropertyFiltersWithDifferentSchemaTypes() throws Exception {
5046         assumeTrue(
5047                 mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
5048         // Schema registration
5049         mDb1.setSchemaAsync(
5050                         new SetSchemaRequest.Builder()
5051                                 .addSchemas(AppSearchEmail.SCHEMA)
5052                                 .addSchemas(
5053                                         new AppSearchSchema.Builder("Note")
5054                                                 .addProperty(
5055                                                         new StringPropertyConfig.Builder("title")
5056                                                                 .setCardinality(
5057                                                                         PropertyConfig
5058                                                                                 .CARDINALITY_REQUIRED)
5059                                                                 .setIndexingType(
5060                                                                         StringPropertyConfig
5061                                                                                 .INDEXING_TYPE_EXACT_TERMS)
5062                                                                 .setTokenizerType(
5063                                                                         StringPropertyConfig
5064                                                                                 .TOKENIZER_TYPE_PLAIN)
5065                                                                 .build())
5066                                                 .addProperty(
5067                                                         new StringPropertyConfig.Builder("body")
5068                                                                 .setCardinality(
5069                                                                         PropertyConfig
5070                                                                                 .CARDINALITY_REQUIRED)
5071                                                                 .setIndexingType(
5072                                                                         StringPropertyConfig
5073                                                                                 .INDEXING_TYPE_EXACT_TERMS)
5074                                                                 .setTokenizerType(
5075                                                                         StringPropertyConfig
5076                                                                                 .TOKENIZER_TYPE_PLAIN)
5077                                                                 .build())
5078                                                 .build())
5079                                 .build())
5080                 .get();
5081 
5082         // Index two documents
5083         AppSearchEmail email =
5084                 new AppSearchEmail.Builder("namespace", "id1")
5085                         .setCreationTimestampMillis(1000)
5086                         .setFrom("[email protected]")
5087                         .setTo("[email protected]", "[email protected]")
5088                         .setSubject("testPut example")
5089                         .setBody("This is the body of the testPut email")
5090                         .build();
5091         GenericDocument note =
5092                 new GenericDocument.Builder<>("namespace", "id2", "Note")
5093                         .setCreationTimestampMillis(1000)
5094                         .setPropertyString("title", "Note title")
5095                         .setPropertyString("body", "Note body")
5096                         .build();
5097         checkIsBatchResultSuccess(
5098                 mDb1.putAsync(
5099                         new PutDocumentsRequest.Builder()
5100                                 .addGenericDocuments(email, note)
5101                                 .build()));
5102 
5103         // Query with type property paths {"Email": ["subject", "to"], "Note": ["body"]}. Note
5104         // schema has body in its property filter but Email schema doesn't.
5105         SearchResultsShim searchResults =
5106                 mDb1.search(
5107                         "body",
5108                         new SearchSpec.Builder()
5109                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
5110                                 .addFilterProperties(
5111                                         AppSearchEmail.SCHEMA_TYPE,
5112                                         ImmutableList.of("subject", "to"))
5113                                 .addFilterProperties("Note", ImmutableList.of("body"))
5114                                 .build());
5115         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
5116         // Only the note document should be returned because the email property filter doesn't
5117         // allow searching in the body.
5118         assertThat(documents).containsExactly(note);
5119     }
5120 
5121     @Test
testQuery_typePropertyFiltersWithWildcard()5122     public void testQuery_typePropertyFiltersWithWildcard() throws Exception {
5123         assumeTrue(
5124                 mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
5125         // Schema registration
5126         mDb1.setSchemaAsync(
5127                         new SetSchemaRequest.Builder()
5128                                 .addSchemas(AppSearchEmail.SCHEMA)
5129                                 .addSchemas(
5130                                         new AppSearchSchema.Builder("Note")
5131                                                 .addProperty(
5132                                                         new StringPropertyConfig.Builder("title")
5133                                                                 .setCardinality(
5134                                                                         PropertyConfig
5135                                                                                 .CARDINALITY_REQUIRED)
5136                                                                 .setIndexingType(
5137                                                                         StringPropertyConfig
5138                                                                                 .INDEXING_TYPE_EXACT_TERMS)
5139                                                                 .setTokenizerType(
5140                                                                         StringPropertyConfig
5141                                                                                 .TOKENIZER_TYPE_PLAIN)
5142                                                                 .build())
5143                                                 .addProperty(
5144                                                         new StringPropertyConfig.Builder("body")
5145                                                                 .setCardinality(
5146                                                                         PropertyConfig
5147                                                                                 .CARDINALITY_REQUIRED)
5148                                                                 .setIndexingType(
5149                                                                         StringPropertyConfig
5150                                                                                 .INDEXING_TYPE_EXACT_TERMS)
5151                                                                 .setTokenizerType(
5152                                                                         StringPropertyConfig
5153                                                                                 .TOKENIZER_TYPE_PLAIN)
5154                                                                 .build())
5155                                                 .build())
5156                                 .build())
5157                 .get();
5158 
5159         // Index two documents
5160         AppSearchEmail email =
5161                 new AppSearchEmail.Builder("namespace", "id1")
5162                         .setCreationTimestampMillis(1000)
5163                         .setFrom("[email protected]")
5164                         .setTo("[email protected]", "[email protected]")
5165                         .setSubject("testPut example subject with some body")
5166                         .setBody("This is the body of the testPut email")
5167                         .build();
5168         GenericDocument note =
5169                 new GenericDocument.Builder<>("namespace", "id2", "Note")
5170                         .setCreationTimestampMillis(1000)
5171                         .setPropertyString("title", "Note title")
5172                         .setPropertyString("body", "Note body")
5173                         .build();
5174         checkIsBatchResultSuccess(
5175                 mDb1.putAsync(
5176                         new PutDocumentsRequest.Builder()
5177                                 .addGenericDocuments(email, note)
5178                                 .build()));
5179 
5180         // Query with type property paths {"*": ["subject", "title"]}
5181         SearchResultsShim searchResults =
5182                 mDb1.search(
5183                         "body",
5184                         new SearchSpec.Builder()
5185                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
5186                                 .addFilterProperties(
5187                                         SearchSpec.SCHEMA_TYPE_WILDCARD,
5188                                         ImmutableList.of("subject", "title"))
5189                                 .build());
5190         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
5191         // The wildcard property filter will apply to both the Email and Note schema. The email
5192         // document should be returned since it has the term "body" in its subject property. The
5193         // note document should not be returned since it doesn't have the term "body" in the title
5194         // property (subject property is not applicable for Note schema)
5195         assertThat(documents).containsExactly(email);
5196     }
5197 
5198     @Test
testQuery_typePropertyFiltersWithWildcardAndExplicitSchema()5199     public void testQuery_typePropertyFiltersWithWildcardAndExplicitSchema() throws Exception {
5200         assumeTrue(
5201                 mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
5202         // Schema registration
5203         mDb1.setSchemaAsync(
5204                         new SetSchemaRequest.Builder()
5205                                 .addSchemas(AppSearchEmail.SCHEMA)
5206                                 .addSchemas(
5207                                         new AppSearchSchema.Builder("Note")
5208                                                 .addProperty(
5209                                                         new StringPropertyConfig.Builder("title")
5210                                                                 .setCardinality(
5211                                                                         PropertyConfig
5212                                                                                 .CARDINALITY_REQUIRED)
5213                                                                 .setIndexingType(
5214                                                                         StringPropertyConfig
5215                                                                                 .INDEXING_TYPE_EXACT_TERMS)
5216                                                                 .setTokenizerType(
5217                                                                         StringPropertyConfig
5218                                                                                 .TOKENIZER_TYPE_PLAIN)
5219                                                                 .build())
5220                                                 .addProperty(
5221                                                         new StringPropertyConfig.Builder("body")
5222                                                                 .setCardinality(
5223                                                                         PropertyConfig
5224                                                                                 .CARDINALITY_REQUIRED)
5225                                                                 .setIndexingType(
5226                                                                         StringPropertyConfig
5227                                                                                 .INDEXING_TYPE_EXACT_TERMS)
5228                                                                 .setTokenizerType(
5229                                                                         StringPropertyConfig
5230                                                                                 .TOKENIZER_TYPE_PLAIN)
5231                                                                 .build())
5232                                                 .build())
5233                                 .build())
5234                 .get();
5235 
5236         // Index two documents
5237         AppSearchEmail email =
5238                 new AppSearchEmail.Builder("namespace", "id1")
5239                         .setCreationTimestampMillis(1000)
5240                         .setFrom("[email protected]")
5241                         .setTo("[email protected]", "[email protected]")
5242                         .setSubject("testPut example subject with some body")
5243                         .setBody("This is the body of the testPut email")
5244                         .build();
5245         GenericDocument note =
5246                 new GenericDocument.Builder<>("namespace", "id2", "Note")
5247                         .setCreationTimestampMillis(1000)
5248                         .setPropertyString("title", "Note title")
5249                         .setPropertyString("body", "Note body")
5250                         .build();
5251         checkIsBatchResultSuccess(
5252                 mDb1.putAsync(
5253                         new PutDocumentsRequest.Builder()
5254                                 .addGenericDocuments(email, note)
5255                                 .build()));
5256 
5257         // Query with type property paths {"*": ["subject", "title"], "Note": ["body"]}
5258         SearchResultsShim searchResults =
5259                 mDb1.search(
5260                         "body",
5261                         new SearchSpec.Builder()
5262                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
5263                                 .addFilterProperties(
5264                                         SearchSpec.SCHEMA_TYPE_WILDCARD,
5265                                         ImmutableList.of("subject", "title"))
5266                                 .addFilterProperties("Note", ImmutableList.of("body"))
5267                                 .build());
5268         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
5269         // The wildcard property filter will only apply to the Email schema since Note schema has
5270         // its own explicit property filter specified. The email document should be returned since
5271         // it has the term "body" in its subject property. The note document should also be returned
5272         // since it has the term "body" in the body property.
5273         assertThat(documents).containsExactly(email, note);
5274     }
5275 
5276     @Test
testQuery_typePropertyFiltersNonExistentType()5277     public void testQuery_typePropertyFiltersNonExistentType() throws Exception {
5278         assumeTrue(
5279                 mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
5280         // Schema registration
5281         mDb1.setSchemaAsync(
5282                         new SetSchemaRequest.Builder()
5283                                 .addSchemas(AppSearchEmail.SCHEMA)
5284                                 .addSchemas(
5285                                         new AppSearchSchema.Builder("Note")
5286                                                 .addProperty(
5287                                                         new StringPropertyConfig.Builder("title")
5288                                                                 .setCardinality(
5289                                                                         PropertyConfig
5290                                                                                 .CARDINALITY_REQUIRED)
5291                                                                 .setIndexingType(
5292                                                                         StringPropertyConfig
5293                                                                                 .INDEXING_TYPE_EXACT_TERMS)
5294                                                                 .setTokenizerType(
5295                                                                         StringPropertyConfig
5296                                                                                 .TOKENIZER_TYPE_PLAIN)
5297                                                                 .build())
5298                                                 .addProperty(
5299                                                         new StringPropertyConfig.Builder("body")
5300                                                                 .setCardinality(
5301                                                                         PropertyConfig
5302                                                                                 .CARDINALITY_REQUIRED)
5303                                                                 .setIndexingType(
5304                                                                         StringPropertyConfig
5305                                                                                 .INDEXING_TYPE_EXACT_TERMS)
5306                                                                 .setTokenizerType(
5307                                                                         StringPropertyConfig
5308                                                                                 .TOKENIZER_TYPE_PLAIN)
5309                                                                 .build())
5310                                                 .build())
5311                                 .build())
5312                 .get();
5313 
5314         // Index two documents
5315         AppSearchEmail email =
5316                 new AppSearchEmail.Builder("namespace", "id1")
5317                         .setCreationTimestampMillis(1000)
5318                         .setFrom("[email protected]")
5319                         .setTo("[email protected]", "[email protected]")
5320                         .setSubject("testPut example subject with some body")
5321                         .setBody("This is the body of the testPut email")
5322                         .build();
5323         GenericDocument note =
5324                 new GenericDocument.Builder<>("namespace", "id2", "Note")
5325                         .setCreationTimestampMillis(1000)
5326                         .setPropertyString("title", "Note title")
5327                         .setPropertyString("body", "Note body")
5328                         .build();
5329         checkIsBatchResultSuccess(
5330                 mDb1.putAsync(
5331                         new PutDocumentsRequest.Builder()
5332                                 .addGenericDocuments(email, note)
5333                                 .build()));
5334 
5335         // Query with type property paths {"NonExistentType": ["to", "title"]}
5336         SearchResultsShim searchResults =
5337                 mDb1.search(
5338                         "body",
5339                         new SearchSpec.Builder()
5340                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
5341                                 .addFilterProperties(
5342                                         "NonExistentType", ImmutableList.of("to", "title"))
5343                                 .build());
5344         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
5345         // The supplied property filters don't apply to either schema types. Both the documents
5346         // should be returned since the term "body" is present in at least one of their properties.
5347         assertThat(documents).containsExactly(email, note);
5348     }
5349 
5350     @Test
testQuery_typePropertyFiltersEmpty()5351     public void testQuery_typePropertyFiltersEmpty() throws Exception {
5352         assumeTrue(
5353                 mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
5354         // Schema registration
5355         mDb1.setSchemaAsync(
5356                         new SetSchemaRequest.Builder()
5357                                 .addSchemas(AppSearchEmail.SCHEMA)
5358                                 .addSchemas(
5359                                         new AppSearchSchema.Builder("Note")
5360                                                 .addProperty(
5361                                                         new StringPropertyConfig.Builder("title")
5362                                                                 .setCardinality(
5363                                                                         PropertyConfig
5364                                                                                 .CARDINALITY_REQUIRED)
5365                                                                 .setIndexingType(
5366                                                                         StringPropertyConfig
5367                                                                                 .INDEXING_TYPE_EXACT_TERMS)
5368                                                                 .setTokenizerType(
5369                                                                         StringPropertyConfig
5370                                                                                 .TOKENIZER_TYPE_PLAIN)
5371                                                                 .build())
5372                                                 .addProperty(
5373                                                         new StringPropertyConfig.Builder("body")
5374                                                                 .setCardinality(
5375                                                                         PropertyConfig
5376                                                                                 .CARDINALITY_REQUIRED)
5377                                                                 .setIndexingType(
5378                                                                         StringPropertyConfig
5379                                                                                 .INDEXING_TYPE_EXACT_TERMS)
5380                                                                 .setTokenizerType(
5381                                                                         StringPropertyConfig
5382                                                                                 .TOKENIZER_TYPE_PLAIN)
5383                                                                 .build())
5384                                                 .build())
5385                                 .build())
5386                 .get();
5387 
5388         // Index two documents
5389         AppSearchEmail email =
5390                 new AppSearchEmail.Builder("namespace", "id1")
5391                         .setCreationTimestampMillis(1000)
5392                         .setFrom("[email protected]")
5393                         .setTo("[email protected]", "[email protected]")
5394                         .setSubject("testPut example")
5395                         .setBody("This is the body of the testPut email")
5396                         .build();
5397         GenericDocument note =
5398                 new GenericDocument.Builder<>("namespace", "id2", "Note")
5399                         .setCreationTimestampMillis(1000)
5400                         .setPropertyString("title", "Note title")
5401                         .setPropertyString("body", "Note body")
5402                         .build();
5403         checkIsBatchResultSuccess(
5404                 mDb1.putAsync(
5405                         new PutDocumentsRequest.Builder()
5406                                 .addGenericDocuments(email, note)
5407                                 .build()));
5408 
5409         // Query with type property paths {"email": []}
5410         SearchResultsShim searchResults =
5411                 mDb1.search(
5412                         "body",
5413                         new SearchSpec.Builder()
5414                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
5415                                 .addFilterProperties(
5416                                         AppSearchEmail.SCHEMA_TYPE, Collections.emptyList())
5417                                 .build());
5418         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
5419         // The email document should not be returned since the property filter doesn't allow
5420         // searching any property.
5421         assertThat(documents).containsExactly(note);
5422     }
5423 
5424     @Test
testSnippet()5425     public void testSnippet() throws Exception {
5426         // Schema registration
5427         AppSearchSchema genericSchema =
5428                 new AppSearchSchema.Builder("Generic")
5429                         .addProperty(
5430                                 new StringPropertyConfig.Builder("subject")
5431                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
5432                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
5433                                         .setIndexingType(
5434                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
5435                                         .build())
5436                         .build();
5437         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(genericSchema).build()).get();
5438 
5439         // Index a document
5440         GenericDocument document =
5441                 new GenericDocument.Builder<>("namespace", "id", "Generic")
5442                         .setPropertyString(
5443                                 "subject",
5444                                 "A commonly used fake word is foo. "
5445                                         + "Another nonsense word that’s used a lot is bar")
5446                         .build();
5447         checkIsBatchResultSuccess(
5448                 mDb1.putAsync(
5449                         new PutDocumentsRequest.Builder().addGenericDocuments(document).build()));
5450 
5451         // Query for the document
5452         SearchResultsShim searchResults =
5453                 mDb1.search(
5454                         "fo",
5455                         new SearchSpec.Builder()
5456                                 .addFilterSchemas("Generic")
5457                                 .setSnippetCount(1)
5458                                 .setSnippetCountPerProperty(1)
5459                                 .setMaxSnippetSize(10)
5460                                 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
5461                                 .build());
5462         List<SearchResult> results = searchResults.getNextPageAsync().get();
5463         assertThat(results).hasSize(1);
5464 
5465         List<SearchResult.MatchInfo> matchInfos = results.get(0).getMatchInfos();
5466         assertThat(matchInfos).isNotNull();
5467         assertThat(matchInfos).hasSize(1);
5468         SearchResult.MatchInfo matchInfo = matchInfos.get(0);
5469         assertThat(matchInfo.getFullText())
5470                 .isEqualTo(
5471                         "A commonly used fake word is foo. "
5472                                 + "Another nonsense word that’s used a lot is bar");
5473         assertThat(matchInfo.getExactMatchRange())
5474                 .isEqualTo(new SearchResult.MatchRange(/* start= */ 29, /* end= */ 32));
5475         assertThat(matchInfo.getExactMatch()).isEqualTo("foo");
5476         assertThat(matchInfo.getSnippetRange())
5477                 .isEqualTo(new SearchResult.MatchRange(/* start= */ 26, /* end= */ 33));
5478         assertThat(matchInfo.getSnippet()).isEqualTo("is foo.");
5479 
5480         if (!mDb1.getFeatures().isFeatureSupported(Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH)) {
5481             assertThrows(UnsupportedOperationException.class, matchInfo::getSubmatchRange);
5482             assertThrows(UnsupportedOperationException.class, matchInfo::getSubmatch);
5483         } else {
5484             assertThat(matchInfo.getSubmatchRange())
5485                     .isEqualTo(new SearchResult.MatchRange(/* start= */ 29, /* end= */ 31));
5486             assertThat(matchInfo.getSubmatch()).isEqualTo("fo");
5487         }
5488     }
5489 
5490     @Test
testSetSnippetCount()5491     public void testSetSnippetCount() throws Exception {
5492         // Schema registration
5493         AppSearchSchema genericSchema =
5494                 new AppSearchSchema.Builder("Generic")
5495                         .addProperty(
5496                                 new StringPropertyConfig.Builder("subject")
5497                                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
5498                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
5499                                         .setIndexingType(
5500                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
5501                                         .build())
5502                         .build();
5503         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(genericSchema).build()).get();
5504 
5505         // Index documents
5506         checkIsBatchResultSuccess(
5507                 mDb1.putAsync(
5508                         new PutDocumentsRequest.Builder()
5509                                 .addGenericDocuments(
5510                                         new GenericDocument.Builder<>("namespace", "id1", "Generic")
5511                                                 .setPropertyString(
5512                                                         "subject",
5513                                                         "I like cats",
5514                                                         "I like dogs",
5515                                                         "I like birds",
5516                                                         "I like fish")
5517                                                 .setScore(10)
5518                                                 .build(),
5519                                         new GenericDocument.Builder<>("namespace", "id2", "Generic")
5520                                                 .setPropertyString(
5521                                                         "subject",
5522                                                         "I like red",
5523                                                         "I like green",
5524                                                         "I like blue",
5525                                                         "I like yellow")
5526                                                 .setScore(20)
5527                                                 .build(),
5528                                         new GenericDocument.Builder<>("namespace", "id3", "Generic")
5529                                                 .setPropertyString(
5530                                                         "subject",
5531                                                         "I like cupcakes",
5532                                                         "I like donuts",
5533                                                         "I like eclairs",
5534                                                         "I like froyo")
5535                                                 .setScore(5)
5536                                                 .build())
5537                                 .build()));
5538 
5539         // Query for the document
5540         SearchResultsShim searchResults =
5541                 mDb1.search(
5542                         "like",
5543                         new SearchSpec.Builder()
5544                                 .addFilterSchemas("Generic")
5545                                 .setSnippetCount(2)
5546                                 .setSnippetCountPerProperty(3)
5547                                 .setMaxSnippetSize(11)
5548                                 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_DOCUMENT_SCORE)
5549                                 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
5550                                 .build());
5551 
5552         // Check result 1
5553         List<SearchResult> results = searchResults.getNextPageAsync().get();
5554         assertThat(results).hasSize(3);
5555 
5556         assertThat(results.get(0).getGenericDocument().getId()).isEqualTo("id2");
5557         List<SearchResult.MatchInfo> matchInfos = results.get(0).getMatchInfos();
5558         assertThat(matchInfos).hasSize(3);
5559         assertThat(matchInfos.get(0).getSnippet()).isEqualTo("I like red");
5560         assertThat(matchInfos.get(1).getSnippet()).isEqualTo("I like");
5561         assertThat(matchInfos.get(2).getSnippet()).isEqualTo("I like blue");
5562 
5563         // Check result 2
5564         assertThat(results.get(1).getGenericDocument().getId()).isEqualTo("id1");
5565         matchInfos = results.get(1).getMatchInfos();
5566         assertThat(matchInfos).hasSize(3);
5567         assertThat(matchInfos.get(0).getSnippet()).isEqualTo("I like cats");
5568         assertThat(matchInfos.get(1).getSnippet()).isEqualTo("I like dogs");
5569         assertThat(matchInfos.get(2).getSnippet()).isEqualTo("I like");
5570 
5571         // Check result 2
5572         assertThat(results.get(2).getGenericDocument().getId()).isEqualTo("id3");
5573         matchInfos = results.get(2).getMatchInfos();
5574         assertThat(matchInfos).isEmpty();
5575     }
5576 
5577     @Test
testCJKSnippet()5578     public void testCJKSnippet() throws Exception {
5579         // Schema registration
5580         AppSearchSchema genericSchema =
5581                 new AppSearchSchema.Builder("Generic")
5582                         .addProperty(
5583                                 new StringPropertyConfig.Builder("subject")
5584                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
5585                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
5586                                         .setIndexingType(
5587                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
5588                                         .build())
5589                         .build();
5590         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(genericSchema).build()).get();
5591 
5592         String japanese =
5593                 "差し出されたのが今日ランドセルでした普通の子であれば満面の笑みで俺を言うでしょうしかし私は赤いランド"
5594                         + "セルを見て笑うことができませんでしたどうしたのと心配そうな仕事ガラスながら渋い顔する私書いたこと言"
5595                         + "うんじゃないのカードとなる声を聞きたい私は目から涙をこぼしながらおじいちゃんの近くにかけおり頭をポ"
5596                         + "ンポンと叩きピンクが良かったんだもん";
5597         // Index a document
5598         GenericDocument document =
5599                 new GenericDocument.Builder<>("namespace", "id", "Generic")
5600                         .setPropertyString("subject", japanese)
5601                         .build();
5602         checkIsBatchResultSuccess(
5603                 mDb1.putAsync(
5604                         new PutDocumentsRequest.Builder().addGenericDocuments(document).build()));
5605 
5606         // Query for the document
5607         SearchResultsShim searchResults =
5608                 mDb1.search(
5609                         "は",
5610                         new SearchSpec.Builder()
5611                                 .addFilterSchemas("Generic")
5612                                 .setSnippetCount(1)
5613                                 .setSnippetCountPerProperty(1)
5614                                 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
5615                                 .build());
5616         List<SearchResult> results = searchResults.getNextPageAsync().get();
5617         assertThat(results).hasSize(1);
5618 
5619         List<SearchResult.MatchInfo> matchInfos = results.get(0).getMatchInfos();
5620         assertThat(matchInfos).isNotNull();
5621         assertThat(matchInfos).hasSize(1);
5622         SearchResult.MatchInfo matchInfo = matchInfos.get(0);
5623         assertThat(matchInfo.getFullText()).isEqualTo(japanese);
5624         assertThat(matchInfo.getExactMatchRange())
5625                 .isEqualTo(new SearchResult.MatchRange(/* start= */ 44, /* end= */ 45));
5626         assertThat(matchInfo.getExactMatch()).isEqualTo("は");
5627 
5628         if (!mDb1.getFeatures().isFeatureSupported(Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH)) {
5629             assertThrows(UnsupportedOperationException.class, matchInfo::getSubmatchRange);
5630             assertThrows(UnsupportedOperationException.class, matchInfo::getSubmatch);
5631         } else {
5632             assertThat(matchInfo.getSubmatchRange())
5633                     .isEqualTo(new SearchResult.MatchRange(/* start= */ 44, /* end= */ 45));
5634             assertThat(matchInfo.getSubmatch()).isEqualTo("は");
5635         }
5636     }
5637 
5638     @Test
testRemove()5639     public void testRemove() throws Exception {
5640         // Schema registration
5641         mDb1.setSchemaAsync(
5642                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
5643                 .get();
5644 
5645         // Index documents
5646         AppSearchEmail email1 =
5647                 new AppSearchEmail.Builder("namespace", "id1")
5648                         .setFrom("[email protected]")
5649                         .setTo("[email protected]", "[email protected]")
5650                         .setSubject("testPut example")
5651                         .setBody("This is the body of the testPut email")
5652                         .build();
5653         AppSearchEmail email2 =
5654                 new AppSearchEmail.Builder("namespace", "id2")
5655                         .setFrom("[email protected]")
5656                         .setTo("[email protected]", "[email protected]")
5657                         .setSubject("testPut example 2")
5658                         .setBody("This is the body of the testPut second email")
5659                         .build();
5660         checkIsBatchResultSuccess(
5661                 mDb1.putAsync(
5662                         new PutDocumentsRequest.Builder()
5663                                 .addGenericDocuments(email1, email2)
5664                                 .build()));
5665 
5666         // Check the presence of the documents
5667         assertThat(doGet(mDb1, "namespace", "id1")).hasSize(1);
5668         assertThat(doGet(mDb1, "namespace", "id2")).hasSize(1);
5669 
5670         // Delete the document
5671         checkIsBatchResultSuccess(
5672                 mDb1.removeAsync(
5673                         new RemoveByDocumentIdRequest.Builder("namespace").addIds("id1").build()));
5674 
5675         // Make sure it's really gone
5676         AppSearchBatchResult<String, GenericDocument> getResult =
5677                 mDb1.getByDocumentIdAsync(
5678                                 new GetByDocumentIdRequest.Builder("namespace")
5679                                         .addIds("id1", "id2")
5680                                         .build())
5681                         .get();
5682         assertThat(getResult.isSuccess()).isFalse();
5683         assertThat(getResult.getFailures().get("id1").getResultCode())
5684                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
5685         assertThat(getResult.getSuccesses().get("id2")).isEqualTo(email2);
5686 
5687         // Test if we delete a nonexistent id.
5688         AppSearchBatchResult<String, Void> deleteResult =
5689                 mDb1.removeAsync(
5690                                 new RemoveByDocumentIdRequest.Builder("namespace")
5691                                         .addIds("id1")
5692                                         .build())
5693                         .get();
5694 
5695         assertThat(deleteResult.getFailures().get("id1").getResultCode())
5696                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
5697     }
5698 
5699     @Test
testRemove_multipleIds()5700     public void testRemove_multipleIds() throws Exception {
5701         // Schema registration
5702         mDb1.setSchemaAsync(
5703                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
5704                 .get();
5705 
5706         // Index documents
5707         AppSearchEmail email1 =
5708                 new AppSearchEmail.Builder("namespace", "id1")
5709                         .setFrom("[email protected]")
5710                         .setTo("[email protected]", "[email protected]")
5711                         .setSubject("testPut example")
5712                         .setBody("This is the body of the testPut email")
5713                         .build();
5714         AppSearchEmail email2 =
5715                 new AppSearchEmail.Builder("namespace", "id2")
5716                         .setFrom("[email protected]")
5717                         .setTo("[email protected]", "[email protected]")
5718                         .setSubject("testPut example 2")
5719                         .setBody("This is the body of the testPut second email")
5720                         .build();
5721         checkIsBatchResultSuccess(
5722                 mDb1.putAsync(
5723                         new PutDocumentsRequest.Builder()
5724                                 .addGenericDocuments(email1, email2)
5725                                 .build()));
5726 
5727         // Check the presence of the documents
5728         assertThat(doGet(mDb1, "namespace", "id1")).hasSize(1);
5729         assertThat(doGet(mDb1, "namespace", "id2")).hasSize(1);
5730 
5731         // Delete the document
5732         checkIsBatchResultSuccess(
5733                 mDb1.removeAsync(
5734                         new RemoveByDocumentIdRequest.Builder("namespace")
5735                                 .addIds("id1", "id2")
5736                                 .build()));
5737 
5738         // Make sure it's really gone
5739         AppSearchBatchResult<String, GenericDocument> getResult =
5740                 mDb1.getByDocumentIdAsync(
5741                                 new GetByDocumentIdRequest.Builder("namespace")
5742                                         .addIds("id1", "id2")
5743                                         .build())
5744                         .get();
5745         assertThat(getResult.isSuccess()).isFalse();
5746         assertThat(getResult.getFailures().get("id1").getResultCode())
5747                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
5748         assertThat(getResult.getFailures().get("id2").getResultCode())
5749                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
5750     }
5751 
5752     @Test
5753     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_DELETE_PROPAGATION_TYPE)
testRemove_withDeletePropagationFromParentToChildren()5754     public void testRemove_withDeletePropagationFromParentToChildren() throws Exception {
5755         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
5756         assumeTrue(
5757                 mDb1.getFeatures()
5758                         .isFeatureSupported(
5759                                 Features
5760                                         .SCHEMA_STRING_PROPERTY_CONFIG_DELETE_PROPAGATION_TYPE_PROPAGATE_FROM));
5761 
5762         // Person (parent) schema.
5763         AppSearchSchema personSchema =
5764                 new AppSearchSchema.Builder("Person")
5765                         .addProperty(
5766                                 new StringPropertyConfig.Builder("name")
5767                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
5768                                         .setIndexingType(
5769                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
5770                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
5771                                         .build())
5772                         .build();
5773         // Email (child) schema: "sender" has delete propagation type PROPAGATE_FROM, and "receiver"
5774         // doesn't have delete propagation.
5775         AppSearchSchema emailSchema =
5776                 new AppSearchSchema.Builder("Email")
5777                         .addProperty(
5778                                 new StringPropertyConfig.Builder("subject")
5779                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
5780                                         .setIndexingType(
5781                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
5782                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
5783                                         .build())
5784                         .addProperty(
5785                                 new StringPropertyConfig.Builder("sender")
5786                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
5787                                         .setJoinableValueType(
5788                                                 StringPropertyConfig
5789                                                         .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
5790                                         .setDeletePropagationType(
5791                                                 StringPropertyConfig
5792                                                         .DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
5793                                         .build())
5794                         .addProperty(
5795                                 new StringPropertyConfig.Builder("receiver")
5796                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
5797                                         .setJoinableValueType(
5798                                                 StringPropertyConfig
5799                                                         .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
5800                                         .build())
5801                         .build();
5802 
5803         // Schema registration
5804         mDb1.setSchemaAsync(
5805                         new SetSchemaRequest.Builder()
5806                                 .addSchemas(personSchema, emailSchema)
5807                                 .build())
5808                 .get();
5809 
5810         // Put 1 person and 2 email documents.
5811         GenericDocument person =
5812                 new GenericDocument.Builder<>("namespace", "person", "Person")
5813                         .setPropertyString("name", "test person")
5814                         .build();
5815         String personQualifiedId =
5816                 DocumentIdUtil.createQualifiedId(
5817                         mContext.getPackageName(), DB_NAME_1, "namespace", "person");
5818         GenericDocument email1 =
5819                 new GenericDocument.Builder<>("namespace", "email1", "Email")
5820                         .setPropertyString("subject", "test email subject")
5821                         .setPropertyString("sender", personQualifiedId)
5822                         .build();
5823         GenericDocument email2 =
5824                 new GenericDocument.Builder<>("namespace", "email2", "Email")
5825                         .setPropertyString("subject", "test email subject")
5826                         .setPropertyString("receiver", personQualifiedId)
5827                         .build();
5828         checkIsBatchResultSuccess(
5829                 mDb1.putAsync(
5830                         new PutDocumentsRequest.Builder()
5831                                 .addGenericDocuments(person, email1, email2)
5832                                 .build()));
5833 
5834         // Check the presence of the documents
5835         assertThat(doGet(mDb1, "namespace", "person")).hasSize(1);
5836         assertThat(doGet(mDb1, "namespace", "email1")).hasSize(1);
5837         assertThat(doGet(mDb1, "namespace", "email2")).hasSize(1);
5838 
5839         // Delete the person (parent) document
5840         checkIsBatchResultSuccess(
5841                 mDb1.removeAsync(
5842                         new RemoveByDocumentIdRequest.Builder("namespace")
5843                                 .addIds("person")
5844                                 .build()));
5845 
5846         // Verify that:
5847         // - Person document is deleted.
5848         // - Email1 document is also deleted due to the delete propagation via "sender".
5849         // - Email2 document is still present since "receiver" does not have delete propagation.
5850         AppSearchBatchResult<String, GenericDocument> getResult1 =
5851                 mDb1.getByDocumentIdAsync(
5852                                 new GetByDocumentIdRequest.Builder("namespace")
5853                                         .addIds("person", "email1")
5854                                         .build())
5855                         .get();
5856         assertThat(getResult1.isSuccess()).isFalse();
5857         assertThat(getResult1.getFailures()).hasSize(2);
5858         assertThat(getResult1.getFailures().get("person").getResultCode())
5859                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
5860         assertThat(getResult1.getFailures().get("email1").getResultCode())
5861                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
5862 
5863         AppSearchBatchResult<String, GenericDocument> getResult2 =
5864                 mDb1.getByDocumentIdAsync(
5865                                 new GetByDocumentIdRequest.Builder("namespace")
5866                                         .addIds("email2")
5867                                         .build())
5868                         .get();
5869         assertThat(getResult2.isSuccess()).isTrue();
5870         assertThat(getResult2.getSuccesses()).hasSize(1);
5871         assertThat(getResult2.getSuccesses().get("email2")).isEqualTo(email2);
5872     }
5873 
5874     @Test
5875     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_DELETE_PROPAGATION_TYPE)
testRemove_withDeletePropagationFromParentToGrandchildren()5876     public void testRemove_withDeletePropagationFromParentToGrandchildren() throws Exception {
5877         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
5878         assumeTrue(
5879                 mDb1.getFeatures()
5880                         .isFeatureSupported(
5881                                 Features
5882                                         .SCHEMA_STRING_PROPERTY_CONFIG_DELETE_PROPAGATION_TYPE_PROPAGATE_FROM));
5883 
5884         // Person (parent) schema.
5885         AppSearchSchema personSchema =
5886                 new AppSearchSchema.Builder("Person")
5887                         .addProperty(
5888                                 new StringPropertyConfig.Builder("name")
5889                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
5890                                         .setIndexingType(
5891                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
5892                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
5893                                         .build())
5894                         .build();
5895         // Email (child) schema: "sender" has delete propagation type PROPAGATE_FROM, and "receiver"
5896         // doesn't have delete propagation.
5897         AppSearchSchema emailSchema =
5898                 new AppSearchSchema.Builder("Email")
5899                         .addProperty(
5900                                 new StringPropertyConfig.Builder("subject")
5901                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
5902                                         .setIndexingType(
5903                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
5904                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
5905                                         .build())
5906                         .addProperty(
5907                                 new StringPropertyConfig.Builder("sender")
5908                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
5909                                         .setJoinableValueType(
5910                                                 StringPropertyConfig
5911                                                         .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
5912                                         .setDeletePropagationType(
5913                                                 StringPropertyConfig
5914                                                         .DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
5915                                         .build())
5916                         .addProperty(
5917                                 new StringPropertyConfig.Builder("receiver")
5918                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
5919                                         .setJoinableValueType(
5920                                                 StringPropertyConfig
5921                                                         .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
5922                                         .build())
5923                         .build();
5924 
5925         // Label (grandchild) schema: "object" has delete propagation type PROPAGATE_FROM, and
5926         // "softLink" doesn't have delete propagation.
5927         AppSearchSchema labelSchema =
5928                 new AppSearchSchema.Builder("Label")
5929                         .addProperty(
5930                                 new StringPropertyConfig.Builder("text")
5931                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
5932                                         .setIndexingType(
5933                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
5934                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
5935                                         .build())
5936                         .addProperty(
5937                                 new StringPropertyConfig.Builder("object")
5938                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
5939                                         .setJoinableValueType(
5940                                                 StringPropertyConfig
5941                                                         .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
5942                                         .setDeletePropagationType(
5943                                                 StringPropertyConfig
5944                                                         .DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
5945                                         .build())
5946                         .addProperty(
5947                                 new StringPropertyConfig.Builder("softLink")
5948                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
5949                                         .setJoinableValueType(
5950                                                 StringPropertyConfig
5951                                                         .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
5952                                         .build())
5953                         .build();
5954 
5955         // Schema registration
5956         mDb1.setSchemaAsync(
5957                         new SetSchemaRequest.Builder()
5958                                 .addSchemas(personSchema, emailSchema, labelSchema)
5959                                 .build())
5960                 .get();
5961 
5962         // Put 1 person, 2 email, and 4 label documents with the following relations:
5963         //
5964         //                           ("object") - label1
5965         //                         /
5966         //               email1 <-
5967         //             /           \
5968         //       ("sender")          ("softLink") - label2
5969         //           /
5970         // person <-
5971         //           \
5972         //       ("receiver")        ("object") - label3
5973         //             \           /
5974         //               email2 <-
5975         //                         \
5976         //                           ("softLink") - label4
5977         GenericDocument person =
5978                 new GenericDocument.Builder<>("namespace", "person", "Person")
5979                         .setPropertyString("name", "test person")
5980                         .build();
5981         String personQualifiedId =
5982                 DocumentIdUtil.createQualifiedId(
5983                         mContext.getPackageName(), DB_NAME_1, "namespace", "person");
5984 
5985         GenericDocument email1 =
5986                 new GenericDocument.Builder<>("namespace", "email1", "Email")
5987                         .setPropertyString("subject", "test email subject")
5988                         .setPropertyString("sender", personQualifiedId)
5989                         .build();
5990         GenericDocument email2 =
5991                 new GenericDocument.Builder<>("namespace", "email2", "Email")
5992                         .setPropertyString("subject", "test email subject")
5993                         .setPropertyString("receiver", personQualifiedId)
5994                         .build();
5995         String emailQualifiedId1 =
5996                 DocumentIdUtil.createQualifiedId(
5997                         mContext.getPackageName(), DB_NAME_1, "namespace", "email1");
5998         String emailQualifiedId2 =
5999                 DocumentIdUtil.createQualifiedId(
6000                         mContext.getPackageName(), DB_NAME_1, "namespace", "email2");
6001 
6002         GenericDocument label1 =
6003                 new GenericDocument.Builder<>("namespace", "label1", "Label")
6004                         .setPropertyString("text", "label1")
6005                         .setPropertyString("object", emailQualifiedId1)
6006                         .build();
6007         GenericDocument label2 =
6008                 new GenericDocument.Builder<>("namespace", "label2", "Label")
6009                         .setPropertyString("text", "label2")
6010                         .setPropertyString("softLink", emailQualifiedId1)
6011                         .build();
6012         GenericDocument label3 =
6013                 new GenericDocument.Builder<>("namespace", "label3", "Label")
6014                         .setPropertyString("text", "label3")
6015                         .setPropertyString("object", emailQualifiedId2)
6016                         .build();
6017         GenericDocument label4 =
6018                 new GenericDocument.Builder<>("namespace", "label4", "Label")
6019                         .setPropertyString("text", "label4")
6020                         .setPropertyString("softLink", emailQualifiedId2)
6021                         .build();
6022 
6023         checkIsBatchResultSuccess(
6024                 mDb1.putAsync(
6025                         new PutDocumentsRequest.Builder()
6026                                 .addGenericDocuments(
6027                                         person, email1, email2, label1, label2, label3, label4)
6028                                 .build()));
6029 
6030         // Check the presence of the documents
6031         assertThat(doGet(mDb1, "namespace", "person")).hasSize(1);
6032         assertThat(doGet(mDb1, "namespace", "email1")).hasSize(1);
6033         assertThat(doGet(mDb1, "namespace", "email2")).hasSize(1);
6034         assertThat(doGet(mDb1, "namespace", "label1")).hasSize(1);
6035         assertThat(doGet(mDb1, "namespace", "label2")).hasSize(1);
6036         assertThat(doGet(mDb1, "namespace", "label3")).hasSize(1);
6037         assertThat(doGet(mDb1, "namespace", "label4")).hasSize(1);
6038 
6039         // Delete the person (parent) document
6040         checkIsBatchResultSuccess(
6041                 mDb1.removeAsync(
6042                         new RemoveByDocumentIdRequest.Builder("namespace")
6043                                 .addIds("person")
6044                                 .build()));
6045 
6046         // Verify that:
6047         // - Person document is deleted.
6048         // - Email1 document is also deleted due to the delete propagation via "sender".
6049         // - Label1 document is also deleted due to the delete propagation via "object".
6050         // - Label2 document is still present since "softLink" does not have delete propagation.
6051         // - Email2 document is still present since "receiver" does not have delete propagation.
6052         // - Label3 document is still present since Email2 is not deleted.
6053         // - Label4 document is still present since Email2 is not deleted.
6054         AppSearchBatchResult<String, GenericDocument> getResult1 =
6055                 mDb1.getByDocumentIdAsync(
6056                                 new GetByDocumentIdRequest.Builder("namespace")
6057                                         .addIds("person", "email1", "label1")
6058                                         .build())
6059                         .get();
6060         assertThat(getResult1.isSuccess()).isFalse();
6061         assertThat(getResult1.getFailures()).hasSize(3);
6062         assertThat(getResult1.getFailures().get("person").getResultCode())
6063                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
6064         assertThat(getResult1.getFailures().get("email1").getResultCode())
6065                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
6066         assertThat(getResult1.getFailures().get("label1").getResultCode())
6067                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
6068 
6069         AppSearchBatchResult<String, GenericDocument> getResult2 =
6070                 mDb1.getByDocumentIdAsync(
6071                                 new GetByDocumentIdRequest.Builder("namespace")
6072                                         .addIds("email2", "label2", "label3", "label4")
6073                                         .build())
6074                         .get();
6075         assertThat(getResult2.isSuccess()).isTrue();
6076         assertThat(getResult2.getSuccesses()).hasSize(4);
6077         assertThat(getResult2.getSuccesses().get("email2")).isEqualTo(email2);
6078         assertThat(getResult2.getSuccesses().get("label2")).isEqualTo(label2);
6079         assertThat(getResult2.getSuccesses().get("label3")).isEqualTo(label3);
6080         assertThat(getResult2.getSuccesses().get("label4")).isEqualTo(label4);
6081     }
6082 
6083     @Test
6084     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_DELETE_PROPAGATION_TYPE)
testRemove_withDeletePropagationFromParentToChildren_fromMultipleProperties()6085     public void testRemove_withDeletePropagationFromParentToChildren_fromMultipleProperties()
6086             throws Exception {
6087         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
6088         assumeTrue(
6089                 mDb1.getFeatures()
6090                         .isFeatureSupported(
6091                                 Features
6092                                         .SCHEMA_STRING_PROPERTY_CONFIG_DELETE_PROPAGATION_TYPE_PROPAGATE_FROM));
6093 
6094         // Person (parent) schema.
6095         AppSearchSchema personSchema =
6096                 new AppSearchSchema.Builder("Person")
6097                         .addProperty(
6098                                 new StringPropertyConfig.Builder("name")
6099                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
6100                                         .setIndexingType(
6101                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
6102                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
6103                                         .build())
6104                         .build();
6105         // Email (child) schema: "sender" has delete propagation type PROPAGATE_FROM, and "receiver"
6106         // doesn't have delete propagation.
6107         AppSearchSchema emailSchema =
6108                 new AppSearchSchema.Builder("Email")
6109                         .addProperty(
6110                                 new StringPropertyConfig.Builder("subject")
6111                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
6112                                         .setIndexingType(
6113                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
6114                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
6115                                         .build())
6116                         .addProperty(
6117                                 new StringPropertyConfig.Builder("sender")
6118                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
6119                                         .setJoinableValueType(
6120                                                 StringPropertyConfig
6121                                                         .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
6122                                         .setDeletePropagationType(
6123                                                 StringPropertyConfig
6124                                                         .DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
6125                                         .build())
6126                         .addProperty(
6127                                 new StringPropertyConfig.Builder("receiver")
6128                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
6129                                         .setJoinableValueType(
6130                                                 StringPropertyConfig
6131                                                         .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
6132                                         .build())
6133                         .build();
6134 
6135         // Schema registration
6136         mDb1.setSchemaAsync(
6137                         new SetSchemaRequest.Builder()
6138                                 .addSchemas(personSchema, emailSchema)
6139                                 .build())
6140                 .get();
6141 
6142         // Put 1 person and 1 email document.
6143         // Email document has both "sender" and "receiver" referring to the person document.
6144         GenericDocument person =
6145                 new GenericDocument.Builder<>("namespace", "person", "Person")
6146                         .setPropertyString("name", "test person")
6147                         .build();
6148         String personQualifiedId =
6149                 DocumentIdUtil.createQualifiedId(
6150                         mContext.getPackageName(), DB_NAME_1, "namespace", "person");
6151         GenericDocument email =
6152                 new GenericDocument.Builder<>("namespace", "email", "Email")
6153                         .setPropertyString("subject", "test email subject")
6154                         .setPropertyString("sender", personQualifiedId)
6155                         .setPropertyString("receiver", personQualifiedId)
6156                         .build();
6157         checkIsBatchResultSuccess(
6158                 mDb1.putAsync(
6159                         new PutDocumentsRequest.Builder()
6160                                 .addGenericDocuments(person, email)
6161                                 .build()));
6162 
6163         // Check the presence of the documents
6164         assertThat(doGet(mDb1, "namespace", "person")).hasSize(1);
6165         assertThat(doGet(mDb1, "namespace", "email")).hasSize(1);
6166 
6167         // Delete the person (parent) document
6168         checkIsBatchResultSuccess(
6169                 mDb1.removeAsync(
6170                         new RemoveByDocumentIdRequest.Builder("namespace")
6171                                 .addIds("person")
6172                                 .build()));
6173 
6174         // Verify that:
6175         // - Person document is deleted.
6176         // - Email document is also deleted since there is at least one property ("sender") with
6177         //   DELETE_PROPAGATION_TYPE_PROPAGATE_FROM.
6178         AppSearchBatchResult<String, GenericDocument> getResult1 =
6179                 mDb1.getByDocumentIdAsync(
6180                                 new GetByDocumentIdRequest.Builder("namespace")
6181                                         .addIds("person", "email")
6182                                         .build())
6183                         .get();
6184         assertThat(getResult1.isSuccess()).isFalse();
6185         assertThat(getResult1.getFailures()).hasSize(2);
6186         assertThat(getResult1.getFailures().get("person").getResultCode())
6187                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
6188         assertThat(getResult1.getFailures().get("email").getResultCode())
6189                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
6190     }
6191 
6192     @Test
testRemoveByQuery()6193     public void testRemoveByQuery() throws Exception {
6194         // Schema registration
6195         mDb1.setSchemaAsync(
6196                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
6197                 .get();
6198 
6199         // Index documents
6200         AppSearchEmail email1 =
6201                 new AppSearchEmail.Builder("namespace", "id1")
6202                         .setFrom("[email protected]")
6203                         .setTo("[email protected]", "[email protected]")
6204                         .setSubject("foo")
6205                         .setBody("This is the body of the testPut email")
6206                         .build();
6207         AppSearchEmail email2 =
6208                 new AppSearchEmail.Builder("namespace", "id2")
6209                         .setFrom("[email protected]")
6210                         .setTo("[email protected]", "[email protected]")
6211                         .setSubject("bar")
6212                         .setBody("This is the body of the testPut second email")
6213                         .build();
6214         checkIsBatchResultSuccess(
6215                 mDb1.putAsync(
6216                         new PutDocumentsRequest.Builder()
6217                                 .addGenericDocuments(email1, email2)
6218                                 .build()));
6219 
6220         // Check the presence of the documents
6221         assertThat(doGet(mDb1, "namespace", "id1")).hasSize(1);
6222         assertThat(doGet(mDb1, "namespace", "id2")).hasSize(1);
6223 
6224         // Delete the email 1 by query "foo"
6225         mDb1.removeAsync(
6226                         "foo",
6227                         new SearchSpec.Builder().setTermMatch(SearchSpec.TERM_MATCH_PREFIX).build())
6228                 .get();
6229         AppSearchBatchResult<String, GenericDocument> getResult =
6230                 mDb1.getByDocumentIdAsync(
6231                                 new GetByDocumentIdRequest.Builder("namespace")
6232                                         .addIds("id1", "id2")
6233                                         .build())
6234                         .get();
6235         assertThat(getResult.isSuccess()).isFalse();
6236         assertThat(getResult.getFailures().get("id1").getResultCode())
6237                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
6238         assertThat(getResult.getSuccesses().get("id2")).isEqualTo(email2);
6239 
6240         // Delete the email 2 by query "bar"
6241         mDb1.removeAsync(
6242                         "bar",
6243                         new SearchSpec.Builder().setTermMatch(SearchSpec.TERM_MATCH_PREFIX).build())
6244                 .get();
6245         getResult =
6246                 mDb1.getByDocumentIdAsync(
6247                                 new GetByDocumentIdRequest.Builder("namespace")
6248                                         .addIds("id2")
6249                                         .build())
6250                         .get();
6251         assertThat(getResult.isSuccess()).isFalse();
6252         assertThat(getResult.getFailures().get("id2").getResultCode())
6253                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
6254     }
6255 
6256     @Test
testRemoveByQuery_nonExistNamespace()6257     public void testRemoveByQuery_nonExistNamespace() throws Exception {
6258         // Schema registration
6259         mDb1.setSchemaAsync(
6260                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
6261                 .get();
6262 
6263         // Index documents
6264         AppSearchEmail email1 =
6265                 new AppSearchEmail.Builder("namespace1", "id1")
6266                         .setFrom("[email protected]")
6267                         .setTo("[email protected]", "[email protected]")
6268                         .setSubject("foo")
6269                         .setBody("This is the body of the testPut email")
6270                         .build();
6271         AppSearchEmail email2 =
6272                 new AppSearchEmail.Builder("namespace2", "id2")
6273                         .setFrom("[email protected]")
6274                         .setTo("[email protected]", "[email protected]")
6275                         .setSubject("bar")
6276                         .setBody("This is the body of the testPut second email")
6277                         .build();
6278         checkIsBatchResultSuccess(
6279                 mDb1.putAsync(
6280                         new PutDocumentsRequest.Builder()
6281                                 .addGenericDocuments(email1, email2)
6282                                 .build()));
6283 
6284         // Check the presence of the documents
6285         assertThat(doGet(mDb1, "namespace1", "id1")).hasSize(1);
6286         assertThat(doGet(mDb1, "namespace2", "id2")).hasSize(1);
6287 
6288         // Delete the email by nonExist namespace.
6289         mDb1.removeAsync(
6290                         "",
6291                         new SearchSpec.Builder()
6292                                 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
6293                                 .addFilterNamespaces("nonExistNamespace")
6294                                 .build())
6295                 .get();
6296         // None of these emails will be deleted.
6297         assertThat(doGet(mDb1, "namespace1", "id1")).hasSize(1);
6298         assertThat(doGet(mDb1, "namespace2", "id2")).hasSize(1);
6299     }
6300 
6301     @Test
testRemoveByQuery_packageFilter()6302     public void testRemoveByQuery_packageFilter() throws Exception {
6303         // Schema registration
6304         mDb1.setSchemaAsync(
6305                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
6306                 .get();
6307 
6308         // Index documents
6309         AppSearchEmail email =
6310                 new AppSearchEmail.Builder("namespace", "id1")
6311                         .setFrom("[email protected]")
6312                         .setTo("[email protected]", "[email protected]")
6313                         .setSubject("foo")
6314                         .setBody("This is the body of the testPut email")
6315                         .build();
6316         checkIsBatchResultSuccess(
6317                 mDb1.putAsync(
6318                         new PutDocumentsRequest.Builder().addGenericDocuments(email).build()));
6319 
6320         // Check the presence of the documents
6321         assertThat(doGet(mDb1, "namespace", "id1")).hasSize(1);
6322 
6323         // Try to delete email with query "foo", but restricted to a different package name.
6324         // Won't work and email will still exist.
6325         mDb1.removeAsync(
6326                         "foo",
6327                         new SearchSpec.Builder()
6328                                 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
6329                                 .addFilterPackageNames("some.other.package")
6330                                 .build())
6331                 .get();
6332         assertThat(doGet(mDb1, "namespace", "id1")).hasSize(1);
6333 
6334         // Delete the email by query "foo", restricted to the correct package this time.
6335         mDb1.removeAsync(
6336                         "foo",
6337                         new SearchSpec.Builder()
6338                                 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
6339                                 .addFilterPackageNames(
6340                                         ApplicationProvider.getApplicationContext()
6341                                                 .getPackageName())
6342                                 .build())
6343                 .get();
6344         AppSearchBatchResult<String, GenericDocument> getResult =
6345                 mDb1.getByDocumentIdAsync(
6346                                 new GetByDocumentIdRequest.Builder("namespace")
6347                                         .addIds("id1", "id2")
6348                                         .build())
6349                         .get();
6350         assertThat(getResult.isSuccess()).isFalse();
6351         assertThat(getResult.getFailures().get("id1").getResultCode())
6352                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
6353     }
6354 
6355     @Test
testRemove_twoInstances()6356     public void testRemove_twoInstances() throws Exception {
6357         // Schema registration
6358         mDb1.setSchemaAsync(
6359                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
6360                 .get();
6361 
6362         // Index documents
6363         AppSearchEmail email1 =
6364                 new AppSearchEmail.Builder("namespace", "id1")
6365                         .setFrom("[email protected]")
6366                         .setTo("[email protected]", "[email protected]")
6367                         .setSubject("testPut example")
6368                         .setBody("This is the body of the testPut email")
6369                         .build();
6370         checkIsBatchResultSuccess(
6371                 mDb1.putAsync(
6372                         new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
6373 
6374         // Check the presence of the documents
6375         assertThat(doGet(mDb1, "namespace", "id1")).hasSize(1);
6376 
6377         // Can't delete in the other instance.
6378         AppSearchBatchResult<String, Void> deleteResult =
6379                 mDb2.removeAsync(
6380                                 new RemoveByDocumentIdRequest.Builder("namespace")
6381                                         .addIds("id1")
6382                                         .build())
6383                         .get();
6384         assertThat(deleteResult.getFailures().get("id1").getResultCode())
6385                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
6386         assertThat(doGet(mDb1, "namespace", "id1")).hasSize(1);
6387 
6388         // Delete the document
6389         checkIsBatchResultSuccess(
6390                 mDb1.removeAsync(
6391                         new RemoveByDocumentIdRequest.Builder("namespace").addIds("id1").build()));
6392 
6393         // Make sure it's really gone
6394         AppSearchBatchResult<String, GenericDocument> getResult =
6395                 mDb1.getByDocumentIdAsync(
6396                                 new GetByDocumentIdRequest.Builder("namespace")
6397                                         .addIds("id1")
6398                                         .build())
6399                         .get();
6400         assertThat(getResult.isSuccess()).isFalse();
6401         assertThat(getResult.getFailures().get("id1").getResultCode())
6402                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
6403 
6404         // Test if we delete a nonexistent id.
6405         deleteResult =
6406                 mDb1.removeAsync(
6407                                 new RemoveByDocumentIdRequest.Builder("namespace")
6408                                         .addIds("id1")
6409                                         .build())
6410                         .get();
6411         assertThat(deleteResult.getFailures().get("id1").getResultCode())
6412                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
6413     }
6414 
6415     @Test
testRemoveByTypes()6416     public void testRemoveByTypes() throws Exception {
6417         // Schema registration
6418         AppSearchSchema genericSchema = new AppSearchSchema.Builder("Generic").build();
6419         mDb1.setSchemaAsync(
6420                         new SetSchemaRequest.Builder()
6421                                 .addSchemas(AppSearchEmail.SCHEMA)
6422                                 .addSchemas(genericSchema)
6423                                 .build())
6424                 .get();
6425 
6426         // Index documents
6427         AppSearchEmail email1 =
6428                 new AppSearchEmail.Builder("namespace", "id1")
6429                         .setFrom("[email protected]")
6430                         .setTo("[email protected]", "[email protected]")
6431                         .setSubject("testPut example")
6432                         .setBody("This is the body of the testPut email")
6433                         .build();
6434         AppSearchEmail email2 =
6435                 new AppSearchEmail.Builder("namespace", "id2")
6436                         .setFrom("[email protected]")
6437                         .setTo("[email protected]", "[email protected]")
6438                         .setSubject("testPut example 2")
6439                         .setBody("This is the body of the testPut second email")
6440                         .build();
6441         GenericDocument document1 =
6442                 new GenericDocument.Builder<>("namespace", "id3", "Generic").build();
6443         checkIsBatchResultSuccess(
6444                 mDb1.putAsync(
6445                         new PutDocumentsRequest.Builder()
6446                                 .addGenericDocuments(email1, email2, document1)
6447                                 .build()));
6448 
6449         // Check the presence of the documents
6450         assertThat(doGet(mDb1, "namespace", "id1", "id2", "id3")).hasSize(3);
6451 
6452         // Delete the email type
6453         mDb1.removeAsync(
6454                         "",
6455                         new SearchSpec.Builder()
6456                                 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
6457                                 .addFilterSchemas(AppSearchEmail.SCHEMA_TYPE)
6458                                 .build())
6459                 .get();
6460 
6461         // Make sure it's really gone
6462         AppSearchBatchResult<String, GenericDocument> getResult =
6463                 mDb1.getByDocumentIdAsync(
6464                                 new GetByDocumentIdRequest.Builder("namespace")
6465                                         .addIds("id1", "id2", "id3")
6466                                         .build())
6467                         .get();
6468         assertThat(getResult.isSuccess()).isFalse();
6469         assertThat(getResult.getFailures().get("id1").getResultCode())
6470                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
6471         assertThat(getResult.getFailures().get("id2").getResultCode())
6472                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
6473         assertThat(getResult.getSuccesses().get("id3")).isEqualTo(document1);
6474     }
6475 
6476     @Test
testRemoveByTypes_twoInstances()6477     public void testRemoveByTypes_twoInstances() throws Exception {
6478         // Schema registration
6479         mDb1.setSchemaAsync(
6480                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
6481                 .get();
6482         mDb2.setSchemaAsync(
6483                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
6484                 .get();
6485 
6486         // Index documents
6487         AppSearchEmail email1 =
6488                 new AppSearchEmail.Builder("namespace", "id1")
6489                         .setFrom("[email protected]")
6490                         .setTo("[email protected]", "[email protected]")
6491                         .setSubject("testPut example")
6492                         .setBody("This is the body of the testPut email")
6493                         .build();
6494         AppSearchEmail email2 =
6495                 new AppSearchEmail.Builder("namespace", "id2")
6496                         .setFrom("[email protected]")
6497                         .setTo("[email protected]", "[email protected]")
6498                         .setSubject("testPut example 2")
6499                         .setBody("This is the body of the testPut second email")
6500                         .build();
6501         checkIsBatchResultSuccess(
6502                 mDb1.putAsync(
6503                         new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
6504         checkIsBatchResultSuccess(
6505                 mDb2.putAsync(
6506                         new PutDocumentsRequest.Builder().addGenericDocuments(email2).build()));
6507 
6508         // Check the presence of the documents
6509         assertThat(doGet(mDb1, "namespace", "id1")).hasSize(1);
6510         assertThat(doGet(mDb2, "namespace", "id2")).hasSize(1);
6511 
6512         // Delete the email type in instance 1
6513         mDb1.removeAsync(
6514                         "",
6515                         new SearchSpec.Builder()
6516                                 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
6517                                 .addFilterSchemas(AppSearchEmail.SCHEMA_TYPE)
6518                                 .build())
6519                 .get();
6520 
6521         // Make sure it's really gone in instance 1
6522         AppSearchBatchResult<String, GenericDocument> getResult =
6523                 mDb1.getByDocumentIdAsync(
6524                                 new GetByDocumentIdRequest.Builder("namespace")
6525                                         .addIds("id1")
6526                                         .build())
6527                         .get();
6528         assertThat(getResult.isSuccess()).isFalse();
6529         assertThat(getResult.getFailures().get("id1").getResultCode())
6530                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
6531 
6532         // Make sure it's still in instance 2.
6533         getResult =
6534                 mDb2.getByDocumentIdAsync(
6535                                 new GetByDocumentIdRequest.Builder("namespace")
6536                                         .addIds("id2")
6537                                         .build())
6538                         .get();
6539         assertThat(getResult.isSuccess()).isTrue();
6540         assertThat(getResult.getSuccesses().get("id2")).isEqualTo(email2);
6541     }
6542 
6543     @Test
testRemoveByNamespace()6544     public void testRemoveByNamespace() throws Exception {
6545         // Schema registration
6546         AppSearchSchema genericSchema =
6547                 new AppSearchSchema.Builder("Generic")
6548                         .addProperty(
6549                                 new StringPropertyConfig.Builder("foo")
6550                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
6551                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
6552                                         .setIndexingType(
6553                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
6554                                         .build())
6555                         .build();
6556         mDb1.setSchemaAsync(
6557                         new SetSchemaRequest.Builder()
6558                                 .addSchemas(AppSearchEmail.SCHEMA)
6559                                 .addSchemas(genericSchema)
6560                                 .build())
6561                 .get();
6562 
6563         // Index documents
6564         AppSearchEmail email1 =
6565                 new AppSearchEmail.Builder("email", "id1")
6566                         .setFrom("[email protected]")
6567                         .setTo("[email protected]", "[email protected]")
6568                         .setSubject("testPut example")
6569                         .setBody("This is the body of the testPut email")
6570                         .build();
6571         AppSearchEmail email2 =
6572                 new AppSearchEmail.Builder("email", "id2")
6573                         .setFrom("[email protected]")
6574                         .setTo("[email protected]", "[email protected]")
6575                         .setSubject("testPut example 2")
6576                         .setBody("This is the body of the testPut second email")
6577                         .build();
6578         GenericDocument document1 =
6579                 new GenericDocument.Builder<>("document", "id3", "Generic")
6580                         .setPropertyString("foo", "bar")
6581                         .build();
6582         checkIsBatchResultSuccess(
6583                 mDb1.putAsync(
6584                         new PutDocumentsRequest.Builder()
6585                                 .addGenericDocuments(email1, email2, document1)
6586                                 .build()));
6587 
6588         // Check the presence of the documents
6589         assertThat(doGet(mDb1, /* namespace= */ "email", "id1", "id2")).hasSize(2);
6590         assertThat(doGet(mDb1, /* namespace= */ "document", "id3")).hasSize(1);
6591 
6592         // Delete the email namespace
6593         mDb1.removeAsync(
6594                         "",
6595                         new SearchSpec.Builder()
6596                                 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
6597                                 .addFilterNamespaces("email")
6598                                 .build())
6599                 .get();
6600 
6601         // Make sure it's really gone
6602         AppSearchBatchResult<String, GenericDocument> getResult =
6603                 mDb1.getByDocumentIdAsync(
6604                                 new GetByDocumentIdRequest.Builder("email")
6605                                         .addIds("id1", "id2")
6606                                         .build())
6607                         .get();
6608         assertThat(getResult.isSuccess()).isFalse();
6609         assertThat(getResult.getFailures().get("id1").getResultCode())
6610                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
6611         assertThat(getResult.getFailures().get("id2").getResultCode())
6612                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
6613         getResult =
6614                 mDb1.getByDocumentIdAsync(
6615                                 new GetByDocumentIdRequest.Builder("document")
6616                                         .addIds("id3")
6617                                         .build())
6618                         .get();
6619         assertThat(getResult.isSuccess()).isTrue();
6620         assertThat(getResult.getSuccesses().get("id3")).isEqualTo(document1);
6621     }
6622 
6623     @Test
testRemoveByNamespaces_twoInstances()6624     public void testRemoveByNamespaces_twoInstances() throws Exception {
6625         // Schema registration
6626         mDb1.setSchemaAsync(
6627                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
6628                 .get();
6629         mDb2.setSchemaAsync(
6630                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
6631                 .get();
6632 
6633         // Index documents
6634         AppSearchEmail email1 =
6635                 new AppSearchEmail.Builder("email", "id1")
6636                         .setFrom("[email protected]")
6637                         .setTo("[email protected]", "[email protected]")
6638                         .setSubject("testPut example")
6639                         .setBody("This is the body of the testPut email")
6640                         .build();
6641         AppSearchEmail email2 =
6642                 new AppSearchEmail.Builder("email", "id2")
6643                         .setFrom("[email protected]")
6644                         .setTo("[email protected]", "[email protected]")
6645                         .setSubject("testPut example 2")
6646                         .setBody("This is the body of the testPut second email")
6647                         .build();
6648         checkIsBatchResultSuccess(
6649                 mDb1.putAsync(
6650                         new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
6651         checkIsBatchResultSuccess(
6652                 mDb2.putAsync(
6653                         new PutDocumentsRequest.Builder().addGenericDocuments(email2).build()));
6654 
6655         // Check the presence of the documents
6656         assertThat(doGet(mDb1, /* namespace= */ "email", "id1")).hasSize(1);
6657         assertThat(doGet(mDb2, /* namespace= */ "email", "id2")).hasSize(1);
6658 
6659         // Delete the email namespace in instance 1
6660         mDb1.removeAsync(
6661                         "",
6662                         new SearchSpec.Builder()
6663                                 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
6664                                 .addFilterNamespaces("email")
6665                                 .build())
6666                 .get();
6667 
6668         // Make sure it's really gone in instance 1
6669         AppSearchBatchResult<String, GenericDocument> getResult =
6670                 mDb1.getByDocumentIdAsync(
6671                                 new GetByDocumentIdRequest.Builder("email").addIds("id1").build())
6672                         .get();
6673         assertThat(getResult.isSuccess()).isFalse();
6674         assertThat(getResult.getFailures().get("id1").getResultCode())
6675                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
6676 
6677         // Make sure it's still in instance 2.
6678         getResult =
6679                 mDb2.getByDocumentIdAsync(
6680                                 new GetByDocumentIdRequest.Builder("email").addIds("id2").build())
6681                         .get();
6682         assertThat(getResult.isSuccess()).isTrue();
6683         assertThat(getResult.getSuccesses().get("id2")).isEqualTo(email2);
6684     }
6685 
6686     @Test
testRemoveAll_twoInstances()6687     public void testRemoveAll_twoInstances() throws Exception {
6688         // Schema registration
6689         mDb1.setSchemaAsync(
6690                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
6691                 .get();
6692         mDb2.setSchemaAsync(
6693                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
6694                 .get();
6695 
6696         // Index documents
6697         AppSearchEmail email1 =
6698                 new AppSearchEmail.Builder("namespace", "id1")
6699                         .setFrom("[email protected]")
6700                         .setTo("[email protected]", "[email protected]")
6701                         .setSubject("testPut example")
6702                         .setBody("This is the body of the testPut email")
6703                         .build();
6704         AppSearchEmail email2 =
6705                 new AppSearchEmail.Builder("namespace", "id2")
6706                         .setFrom("[email protected]")
6707                         .setTo("[email protected]", "[email protected]")
6708                         .setSubject("testPut example 2")
6709                         .setBody("This is the body of the testPut second email")
6710                         .build();
6711         checkIsBatchResultSuccess(
6712                 mDb1.putAsync(
6713                         new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
6714         checkIsBatchResultSuccess(
6715                 mDb2.putAsync(
6716                         new PutDocumentsRequest.Builder().addGenericDocuments(email2).build()));
6717 
6718         // Check the presence of the documents
6719         assertThat(doGet(mDb1, "namespace", "id1")).hasSize(1);
6720         assertThat(doGet(mDb2, "namespace", "id2")).hasSize(1);
6721 
6722         // Delete the all document in instance 1
6723         mDb1.removeAsync(
6724                         "",
6725                         new SearchSpec.Builder().setTermMatch(SearchSpec.TERM_MATCH_PREFIX).build())
6726                 .get();
6727 
6728         // Make sure it's really gone in instance 1
6729         AppSearchBatchResult<String, GenericDocument> getResult =
6730                 mDb1.getByDocumentIdAsync(
6731                                 new GetByDocumentIdRequest.Builder("namespace")
6732                                         .addIds("id1")
6733                                         .build())
6734                         .get();
6735         assertThat(getResult.isSuccess()).isFalse();
6736         assertThat(getResult.getFailures().get("id1").getResultCode())
6737                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
6738 
6739         // Make sure it's still in instance 2.
6740         getResult =
6741                 mDb2.getByDocumentIdAsync(
6742                                 new GetByDocumentIdRequest.Builder("namespace")
6743                                         .addIds("id2")
6744                                         .build())
6745                         .get();
6746         assertThat(getResult.isSuccess()).isTrue();
6747         assertThat(getResult.getSuccesses().get("id2")).isEqualTo(email2);
6748     }
6749 
6750     @Test
testRemoveAll_termMatchType()6751     public void testRemoveAll_termMatchType() throws Exception {
6752         // Schema registration
6753         mDb1.setSchemaAsync(
6754                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
6755                 .get();
6756         mDb2.setSchemaAsync(
6757                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
6758                 .get();
6759 
6760         // Index documents
6761         AppSearchEmail email1 =
6762                 new AppSearchEmail.Builder("namespace", "id1")
6763                         .setFrom("[email protected]")
6764                         .setTo("[email protected]", "[email protected]")
6765                         .setSubject("testPut example")
6766                         .setBody("This is the body of the testPut email")
6767                         .build();
6768         AppSearchEmail email2 =
6769                 new AppSearchEmail.Builder("namespace", "id2")
6770                         .setFrom("[email protected]")
6771                         .setTo("[email protected]", "[email protected]")
6772                         .setSubject("testPut example 2")
6773                         .setBody("This is the body of the testPut second email")
6774                         .build();
6775         AppSearchEmail email3 =
6776                 new AppSearchEmail.Builder("namespace", "id3")
6777                         .setFrom("[email protected]")
6778                         .setTo("[email protected]", "[email protected]")
6779                         .setSubject("testPut example 3")
6780                         .setBody("This is the body of the testPut second email")
6781                         .build();
6782         AppSearchEmail email4 =
6783                 new AppSearchEmail.Builder("namespace", "id4")
6784                         .setFrom("[email protected]")
6785                         .setTo("[email protected]", "[email protected]")
6786                         .setSubject("testPut example 4")
6787                         .setBody("This is the body of the testPut second email")
6788                         .build();
6789         checkIsBatchResultSuccess(
6790                 mDb1.putAsync(
6791                         new PutDocumentsRequest.Builder()
6792                                 .addGenericDocuments(email1, email2)
6793                                 .build()));
6794         checkIsBatchResultSuccess(
6795                 mDb2.putAsync(
6796                         new PutDocumentsRequest.Builder()
6797                                 .addGenericDocuments(email3, email4)
6798                                 .build()));
6799 
6800         // Check the presence of the documents
6801         SearchResultsShim searchResults =
6802                 mDb1.search(
6803                         "",
6804                         new SearchSpec.Builder()
6805                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
6806                                 .build());
6807         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
6808         assertThat(documents).hasSize(2);
6809         searchResults =
6810                 mDb2.search(
6811                         "",
6812                         new SearchSpec.Builder()
6813                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
6814                                 .build());
6815         documents = convertSearchResultsToDocuments(searchResults);
6816         assertThat(documents).hasSize(2);
6817 
6818         // Delete the all document in instance 1 with TERM_MATCH_PREFIX
6819         mDb1.removeAsync(
6820                         "",
6821                         new SearchSpec.Builder().setTermMatch(SearchSpec.TERM_MATCH_PREFIX).build())
6822                 .get();
6823         searchResults =
6824                 mDb1.search(
6825                         "",
6826                         new SearchSpec.Builder()
6827                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
6828                                 .build());
6829         documents = convertSearchResultsToDocuments(searchResults);
6830         assertThat(documents).isEmpty();
6831 
6832         // Delete the all document in instance 2 with TERM_MATCH_EXACT_ONLY
6833         mDb2.removeAsync(
6834                         "",
6835                         new SearchSpec.Builder()
6836                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
6837                                 .build())
6838                 .get();
6839         searchResults =
6840                 mDb2.search(
6841                         "",
6842                         new SearchSpec.Builder()
6843                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
6844                                 .build());
6845         documents = convertSearchResultsToDocuments(searchResults);
6846         assertThat(documents).isEmpty();
6847     }
6848 
6849     @Test
testRemoveAllAfterEmpty()6850     public void testRemoveAllAfterEmpty() throws Exception {
6851         // Schema registration
6852         mDb1.setSchemaAsync(
6853                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
6854                 .get();
6855 
6856         // Index documents
6857         AppSearchEmail email1 =
6858                 new AppSearchEmail.Builder("namespace", "id1")
6859                         .setFrom("[email protected]")
6860                         .setTo("[email protected]", "[email protected]")
6861                         .setSubject("testPut example")
6862                         .setBody("This is the body of the testPut email")
6863                         .build();
6864         checkIsBatchResultSuccess(
6865                 mDb1.putAsync(
6866                         new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
6867 
6868         // Check the presence of the documents
6869         assertThat(doGet(mDb1, "namespace", "id1")).hasSize(1);
6870 
6871         // Remove the document
6872         checkIsBatchResultSuccess(
6873                 mDb1.removeAsync(
6874                         new RemoveByDocumentIdRequest.Builder("namespace").addIds("id1").build()));
6875 
6876         // Make sure it's really gone
6877         AppSearchBatchResult<String, GenericDocument> getResult =
6878                 mDb1.getByDocumentIdAsync(
6879                                 new GetByDocumentIdRequest.Builder("namespace")
6880                                         .addIds("id1")
6881                                         .build())
6882                         .get();
6883         assertThat(getResult.isSuccess()).isFalse();
6884         assertThat(getResult.getFailures().get("id1").getResultCode())
6885                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
6886 
6887         // Delete the all documents
6888         mDb1.removeAsync(
6889                         "",
6890                         new SearchSpec.Builder().setTermMatch(SearchSpec.TERM_MATCH_PREFIX).build())
6891                 .get();
6892 
6893         // Make sure it's still gone
6894         getResult =
6895                 mDb1.getByDocumentIdAsync(
6896                                 new GetByDocumentIdRequest.Builder("namespace")
6897                                         .addIds("id1")
6898                                         .build())
6899                         .get();
6900         assertThat(getResult.isSuccess()).isFalse();
6901         assertThat(getResult.getFailures().get("id1").getResultCode())
6902                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
6903     }
6904 
6905     @Test
testRemoveQueryWithJoinSpecThrowsException()6906     public void testRemoveQueryWithJoinSpecThrowsException() {
6907         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
6908 
6909         IllegalArgumentException e =
6910                 assertThrows(
6911                         IllegalArgumentException.class,
6912                         () ->
6913                                 mDb2.removeAsync(
6914                                         "",
6915                                         new SearchSpec.Builder()
6916                                                 .setJoinSpec(
6917                                                         new JoinSpec.Builder("entityId").build())
6918                                                 .build()));
6919         assertThat(e.getMessage())
6920                 .isEqualTo(
6921                         "JoinSpec not allowed in removeByQuery, " + "but JoinSpec was provided.");
6922     }
6923 
6924     @Test
testCloseAndReopen()6925     public void testCloseAndReopen() throws Exception {
6926         // Schema registration
6927         mDb1.setSchemaAsync(
6928                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
6929                 .get();
6930 
6931         // Index a document
6932         AppSearchEmail inEmail =
6933                 new AppSearchEmail.Builder("namespace", "id1")
6934                         .setFrom("[email protected]")
6935                         .setTo("[email protected]", "[email protected]")
6936                         .setSubject("testPut example")
6937                         .setBody("This is the body of the testPut email")
6938                         .build();
6939         checkIsBatchResultSuccess(
6940                 mDb1.putAsync(
6941                         new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
6942 
6943         // close and re-open the appSearchSession
6944         mDb1.close();
6945         mDb1 = createSearchSessionAsync(DB_NAME_1).get();
6946 
6947         // Query for the document
6948         SearchResultsShim searchResults =
6949                 mDb1.search(
6950                         "body",
6951                         new SearchSpec.Builder()
6952                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
6953                                 .build());
6954         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
6955         assertThat(documents).containsExactly(inEmail);
6956     }
6957 
6958     @Test
testCallAfterClose()6959     public void testCallAfterClose() throws Exception {
6960 
6961         // Create a same-thread database by inject an executor which could help us maintain the
6962         // execution order of those async tasks.
6963         Context context = ApplicationProvider.getApplicationContext();
6964         AppSearchSessionShim sameThreadDb =
6965                 createSearchSessionAsync("sameThreadDb", MoreExecutors.newDirectExecutorService())
6966                         .get();
6967 
6968         try {
6969             // Schema registration -- just mutate something
6970             sameThreadDb
6971                     .setSchemaAsync(
6972                             new SetSchemaRequest.Builder()
6973                                     .addSchemas(AppSearchEmail.SCHEMA)
6974                                     .build())
6975                     .get();
6976 
6977             // Close the database. No further call will be allowed.
6978             sameThreadDb.close();
6979 
6980             // Try to query the closed database
6981             // We are using the same-thread db here to make sure it has been closed.
6982             IllegalStateException e =
6983                     assertThrows(
6984                             IllegalStateException.class,
6985                             () ->
6986                                     sameThreadDb.search(
6987                                             "query",
6988                                             new SearchSpec.Builder()
6989                                                     .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
6990                                                     .build()));
6991             assertThat(e).hasMessageThat().contains("SearchSession has already been closed");
6992         } finally {
6993             // To clean the data that has been added in the test, need to re-open the session and
6994             // set an empty schema.
6995             AppSearchSessionShim reopen =
6996                     createSearchSessionAsync(
6997                                     "sameThreadDb", MoreExecutors.newDirectExecutorService())
6998                             .get();
6999             reopen.setSchemaAsync(new SetSchemaRequest.Builder().setForceOverride(true).build())
7000                     .get();
7001         }
7002     }
7003 
7004     @Test
testReportUsage()7005     public void testReportUsage() throws Exception {
7006         mDb1.setSchemaAsync(
7007                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
7008                 .get();
7009 
7010         // Index two documents.
7011         AppSearchEmail email1 = new AppSearchEmail.Builder("namespace", "id1").build();
7012         AppSearchEmail email2 = new AppSearchEmail.Builder("namespace", "id2").build();
7013         checkIsBatchResultSuccess(
7014                 mDb1.putAsync(
7015                         new PutDocumentsRequest.Builder()
7016                                 .addGenericDocuments(email1, email2)
7017                                 .build()));
7018 
7019         // Email 1 has more usages, but email 2 has more recent usages.
7020         mDb1.reportUsageAsync(
7021                         new ReportUsageRequest.Builder("namespace", "id1")
7022                                 .setUsageTimestampMillis(1000)
7023                                 .build())
7024                 .get();
7025         mDb1.reportUsageAsync(
7026                         new ReportUsageRequest.Builder("namespace", "id1")
7027                                 .setUsageTimestampMillis(2000)
7028                                 .build())
7029                 .get();
7030         mDb1.reportUsageAsync(
7031                         new ReportUsageRequest.Builder("namespace", "id1")
7032                                 .setUsageTimestampMillis(3000)
7033                                 .build())
7034                 .get();
7035         mDb1.reportUsageAsync(
7036                         new ReportUsageRequest.Builder("namespace", "id2")
7037                                 .setUsageTimestampMillis(10000)
7038                                 .build())
7039                 .get();
7040         mDb1.reportUsageAsync(
7041                         new ReportUsageRequest.Builder("namespace", "id2")
7042                                 .setUsageTimestampMillis(20000)
7043                                 .build())
7044                 .get();
7045 
7046         // Query by number of usages
7047         List<SearchResult> results =
7048                 retrieveAllSearchResults(
7049                         mDb1.search(
7050                                 "",
7051                                 new SearchSpec.Builder()
7052                                         .setRankingStrategy(SearchSpec.RANKING_STRATEGY_USAGE_COUNT)
7053                                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
7054                                         .build()));
7055         // Email 1 has three usages and email 2 has two usages.
7056         assertThat(results).hasSize(2);
7057         assertThat(results.get(0).getGenericDocument()).isEqualTo(email1);
7058         assertThat(results.get(1).getGenericDocument()).isEqualTo(email2);
7059         assertThat(results.get(0).getRankingSignal()).isEqualTo(3);
7060         assertThat(results.get(1).getRankingSignal()).isEqualTo(2);
7061 
7062         // Query by most recent usage.
7063         results =
7064                 retrieveAllSearchResults(
7065                         mDb1.search(
7066                                 "",
7067                                 new SearchSpec.Builder()
7068                                         .setRankingStrategy(
7069                                                 SearchSpec
7070                                                         .RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP)
7071                                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
7072                                         .build()));
7073         assertThat(results).hasSize(2);
7074         assertThat(results.get(0).getGenericDocument()).isEqualTo(email2);
7075         assertThat(results.get(1).getGenericDocument()).isEqualTo(email1);
7076         assertThat(results.get(0).getRankingSignal()).isEqualTo(20000);
7077         assertThat(results.get(1).getRankingSignal()).isEqualTo(3000);
7078     }
7079 
7080     @Test
testReportUsage_invalidNamespace()7081     public void testReportUsage_invalidNamespace() throws Exception {
7082         mDb1.setSchemaAsync(
7083                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
7084                 .get();
7085         AppSearchEmail email1 = new AppSearchEmail.Builder("namespace", "id1").build();
7086         checkIsBatchResultSuccess(
7087                 mDb1.putAsync(
7088                         new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
7089 
7090         // Use the correct namespace; it works
7091         mDb1.reportUsageAsync(new ReportUsageRequest.Builder("namespace", "id1").build()).get();
7092 
7093         // Use an incorrect namespace; it fails
7094         ReportUsageRequest reportUsageRequest =
7095                 new ReportUsageRequest.Builder("namespace2", "id1").build();
7096         ExecutionException executionException =
7097                 assertThrows(
7098                         ExecutionException.class,
7099                         () -> mDb1.reportUsageAsync(reportUsageRequest).get());
7100         assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
7101         AppSearchException cause = (AppSearchException) executionException.getCause();
7102         assertThat(cause.getResultCode()).isEqualTo(RESULT_NOT_FOUND);
7103     }
7104 
7105     @Test
testGetStorageInfo()7106     public void testGetStorageInfo() throws Exception {
7107         StorageInfo storageInfo = mDb1.getStorageInfoAsync().get();
7108         assertThat(storageInfo.getSizeBytes()).isEqualTo(0);
7109 
7110         mDb1.setSchemaAsync(
7111                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
7112                 .get();
7113 
7114         // Still no storage space attributed with just a schema
7115         storageInfo = mDb1.getStorageInfoAsync().get();
7116         assertThat(storageInfo.getSizeBytes()).isEqualTo(0);
7117 
7118         // Index two documents.
7119         AppSearchEmail email1 = new AppSearchEmail.Builder("namespace1", "id1").build();
7120         AppSearchEmail email2 = new AppSearchEmail.Builder("namespace1", "id2").build();
7121         AppSearchEmail email3 = new AppSearchEmail.Builder("namespace2", "id1").build();
7122         checkIsBatchResultSuccess(
7123                 mDb1.putAsync(
7124                         new PutDocumentsRequest.Builder()
7125                                 .addGenericDocuments(email1, email2, email3)
7126                                 .build()));
7127 
7128         // Non-zero size now
7129         storageInfo = mDb1.getStorageInfoAsync().get();
7130         assertThat(storageInfo.getSizeBytes()).isGreaterThan(0);
7131         assertThat(storageInfo.getAliveDocumentsCount()).isEqualTo(3);
7132         assertThat(storageInfo.getAliveNamespacesCount()).isEqualTo(2);
7133     }
7134 
7135     @Test
testFlush()7136     public void testFlush() throws Exception {
7137         // Schema registration
7138         mDb1.setSchemaAsync(
7139                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
7140                 .get();
7141 
7142         // Index a document
7143         AppSearchEmail email =
7144                 new AppSearchEmail.Builder("namespace", "id1")
7145                         .setFrom("[email protected]")
7146                         .setTo("[email protected]", "[email protected]")
7147                         .setSubject("testPut example")
7148                         .setBody("This is the body of the testPut email")
7149                         .build();
7150 
7151         AppSearchBatchResult<String, Void> result =
7152                 checkIsBatchResultSuccess(
7153                         mDb1.putAsync(
7154                                 new PutDocumentsRequest.Builder()
7155                                         .addGenericDocuments(email)
7156                                         .build()));
7157         assertThat(result.getSuccesses()).containsExactly("id1", null);
7158         assertThat(result.getFailures()).isEmpty();
7159 
7160         // The future returned from requestFlush will be set as a void or an Exception on error.
7161         mDb1.requestFlushAsync().get();
7162     }
7163 
7164     @Test
testQuery_ResultGroupingLimits()7165     public void testQuery_ResultGroupingLimits() throws Exception {
7166         // Schema registration
7167         mDb1.setSchemaAsync(
7168                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
7169                 .get();
7170 
7171         // Index four documents.
7172         AppSearchEmail inEmail1 =
7173                 new AppSearchEmail.Builder("namespace1", "id1")
7174                         .setFrom("[email protected]")
7175                         .setTo("[email protected]", "[email protected]")
7176                         .setSubject("testPut example")
7177                         .setBody("This is the body of the testPut email")
7178                         .build();
7179         checkIsBatchResultSuccess(
7180                 mDb1.putAsync(
7181                         new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build()));
7182         AppSearchEmail inEmail2 =
7183                 new AppSearchEmail.Builder("namespace1", "id2")
7184                         .setFrom("[email protected]")
7185                         .setTo("[email protected]", "[email protected]")
7186                         .setSubject("testPut example")
7187                         .setBody("This is the body of the testPut email")
7188                         .build();
7189         checkIsBatchResultSuccess(
7190                 mDb1.putAsync(
7191                         new PutDocumentsRequest.Builder().addGenericDocuments(inEmail2).build()));
7192         AppSearchEmail inEmail3 =
7193                 new AppSearchEmail.Builder("namespace2", "id3")
7194                         .setFrom("[email protected]")
7195                         .setTo("[email protected]", "[email protected]")
7196                         .setSubject("testPut example")
7197                         .setBody("This is the body of the testPut email")
7198                         .build();
7199         checkIsBatchResultSuccess(
7200                 mDb1.putAsync(
7201                         new PutDocumentsRequest.Builder().addGenericDocuments(inEmail3).build()));
7202         AppSearchEmail inEmail4 =
7203                 new AppSearchEmail.Builder("namespace2", "id4")
7204                         .setFrom("[email protected]")
7205                         .setTo("[email protected]", "[email protected]")
7206                         .setSubject("testPut example")
7207                         .setBody("This is the body of the testPut email")
7208                         .build();
7209         checkIsBatchResultSuccess(
7210                 mDb1.putAsync(
7211                         new PutDocumentsRequest.Builder().addGenericDocuments(inEmail4).build()));
7212 
7213         // Query with per package result grouping. Only the last document 'email4' should be
7214         // returned.
7215         SearchResultsShim searchResults =
7216                 mDb1.search(
7217                         "body",
7218                         new SearchSpec.Builder()
7219                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
7220                                 .setResultGrouping(
7221                                         SearchSpec.GROUPING_TYPE_PER_PACKAGE, /* limit= */ 1)
7222                                 .build());
7223         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
7224         assertThat(documents).containsExactly(inEmail4);
7225 
7226         // Query with per namespace result grouping. Only the last document in each namespace should
7227         // be returned ('email4' and 'email2').
7228         searchResults =
7229                 mDb1.search(
7230                         "body",
7231                         new SearchSpec.Builder()
7232                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
7233                                 .setResultGrouping(
7234                                         SearchSpec.GROUPING_TYPE_PER_NAMESPACE, /* limit= */ 1)
7235                                 .build());
7236         documents = convertSearchResultsToDocuments(searchResults);
7237         assertThat(documents).containsExactly(inEmail4, inEmail2);
7238 
7239         // Query with per package and per namespace result grouping. Only the last document in each
7240         // namespace should be returned ('email4' and 'email2').
7241         searchResults =
7242                 mDb1.search(
7243                         "body",
7244                         new SearchSpec.Builder()
7245                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
7246                                 .setResultGrouping(
7247                                         SearchSpec.GROUPING_TYPE_PER_NAMESPACE
7248                                                 | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
7249                                         /* limit= */ 1)
7250                                 .build());
7251         documents = convertSearchResultsToDocuments(searchResults);
7252         assertThat(documents).containsExactly(inEmail4, inEmail2);
7253     }
7254 
7255     @Test
testQuery_ResultGroupingLimits_SchemaGroupingSupported()7256     public void testQuery_ResultGroupingLimits_SchemaGroupingSupported() throws Exception {
7257         assumeTrue(
7258                 mDb1.getFeatures()
7259                         .isFeatureSupported(Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA));
7260         // Schema registration
7261         AppSearchSchema genericSchema =
7262                 new AppSearchSchema.Builder("Generic")
7263                         .addProperty(
7264                                 new StringPropertyConfig.Builder("foo")
7265                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
7266                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
7267                                         .setIndexingType(
7268                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
7269                                         .build())
7270                         .build();
7271         mDb1.setSchemaAsync(
7272                         new SetSchemaRequest.Builder()
7273                                 .addSchemas(AppSearchEmail.SCHEMA)
7274                                 .addSchemas(genericSchema)
7275                                 .build())
7276                 .get();
7277 
7278         // Index four documents.
7279         AppSearchEmail inEmail1 =
7280                 new AppSearchEmail.Builder("namespace1", "id1")
7281                         .setFrom("[email protected]")
7282                         .setTo("[email protected]", "[email protected]")
7283                         .setSubject("testPut example")
7284                         .setBody("This is the body of the testPut email")
7285                         .build();
7286         checkIsBatchResultSuccess(
7287                 mDb1.putAsync(
7288                         new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build()));
7289         AppSearchEmail inEmail2 =
7290                 new AppSearchEmail.Builder("namespace1", "id2")
7291                         .setFrom("[email protected]")
7292                         .setTo("[email protected]", "[email protected]")
7293                         .setSubject("testPut example")
7294                         .setBody("This is the body of the testPut email")
7295                         .build();
7296         checkIsBatchResultSuccess(
7297                 mDb1.putAsync(
7298                         new PutDocumentsRequest.Builder().addGenericDocuments(inEmail2).build()));
7299         AppSearchEmail inEmail3 =
7300                 new AppSearchEmail.Builder("namespace2", "id3")
7301                         .setFrom("[email protected]")
7302                         .setTo("[email protected]", "[email protected]")
7303                         .setSubject("testPut example")
7304                         .setBody("This is the body of the testPut email")
7305                         .build();
7306         checkIsBatchResultSuccess(
7307                 mDb1.putAsync(
7308                         new PutDocumentsRequest.Builder().addGenericDocuments(inEmail3).build()));
7309         AppSearchEmail inEmail4 =
7310                 new AppSearchEmail.Builder("namespace2", "id4")
7311                         .setFrom("[email protected]")
7312                         .setTo("[email protected]", "[email protected]")
7313                         .setSubject("testPut example")
7314                         .setBody("This is the body of the testPut email")
7315                         .build();
7316         checkIsBatchResultSuccess(
7317                 mDb1.putAsync(
7318                         new PutDocumentsRequest.Builder().addGenericDocuments(inEmail4).build()));
7319         AppSearchEmail inEmail5 =
7320                 new AppSearchEmail.Builder("namespace2", "id5")
7321                         .setFrom("[email protected]")
7322                         .setTo("[email protected]", "[email protected]")
7323                         .setSubject("testPut example")
7324                         .setBody("This is the body of the testPut email")
7325                         .build();
7326         checkIsBatchResultSuccess(
7327                 mDb1.putAsync(
7328                         new PutDocumentsRequest.Builder().addGenericDocuments(inEmail5).build()));
7329         GenericDocument inDoc1 =
7330                 new GenericDocument.Builder<>("namespace3", "id6", "Generic")
7331                         .setPropertyString("foo", "body")
7332                         .build();
7333         checkIsBatchResultSuccess(
7334                 mDb1.putAsync(
7335                         new PutDocumentsRequest.Builder().addGenericDocuments(inDoc1).build()));
7336         GenericDocument inDoc2 =
7337                 new GenericDocument.Builder<>("namespace3", "id7", "Generic")
7338                         .setPropertyString("foo", "body")
7339                         .build();
7340         checkIsBatchResultSuccess(
7341                 mDb1.putAsync(
7342                         new PutDocumentsRequest.Builder().addGenericDocuments(inDoc2).build()));
7343         GenericDocument inDoc3 =
7344                 new GenericDocument.Builder<>("namespace4", "id8", "Generic")
7345                         .setPropertyString("foo", "body")
7346                         .build();
7347         checkIsBatchResultSuccess(
7348                 mDb1.putAsync(
7349                         new PutDocumentsRequest.Builder().addGenericDocuments(inDoc3).build()));
7350 
7351         // Query with per package result grouping. Only the last document 'doc3' should be
7352         // returned.
7353         SearchResultsShim searchResults =
7354                 mDb1.search(
7355                         "body",
7356                         new SearchSpec.Builder()
7357                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
7358                                 .setResultGrouping(
7359                                         SearchSpec.GROUPING_TYPE_PER_PACKAGE, /* limit= */ 1)
7360                                 .build());
7361         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
7362         assertThat(documents).containsExactly(inDoc3);
7363 
7364         // Query with per namespace result grouping. Only the last document in each namespace should
7365         // be returned ('doc3', 'doc2', 'email5' and 'email2').
7366         searchResults =
7367                 mDb1.search(
7368                         "body",
7369                         new SearchSpec.Builder()
7370                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
7371                                 .setResultGrouping(
7372                                         SearchSpec.GROUPING_TYPE_PER_NAMESPACE, /* limit= */ 1)
7373                                 .build());
7374         documents = convertSearchResultsToDocuments(searchResults);
7375         assertThat(documents).containsExactly(inDoc3, inDoc2, inEmail5, inEmail2);
7376 
7377         // Query with per namespace result grouping. Two of the last documents in each namespace
7378         // should be returned ('doc3', 'doc2', 'doc1', 'email5', 'email4', 'email2', 'email1')
7379         searchResults =
7380                 mDb1.search(
7381                         "body",
7382                         new SearchSpec.Builder()
7383                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
7384                                 .setResultGrouping(
7385                                         SearchSpec.GROUPING_TYPE_PER_NAMESPACE, /* limit= */ 2)
7386                                 .build());
7387         documents = convertSearchResultsToDocuments(searchResults);
7388         assertThat(documents)
7389                 .containsExactly(inDoc3, inDoc2, inDoc1, inEmail5, inEmail4, inEmail2, inEmail1);
7390 
7391         // Query with per schema result grouping. Only the last document of each schema type should
7392         // be returned ('doc3', 'email5')
7393         searchResults =
7394                 mDb1.search(
7395                         "body",
7396                         new SearchSpec.Builder()
7397                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
7398                                 .setResultGrouping(
7399                                         SearchSpec.GROUPING_TYPE_PER_SCHEMA, /* limit= */ 1)
7400                                 .build());
7401         documents = convertSearchResultsToDocuments(searchResults);
7402         assertThat(documents).containsExactly(inDoc3, inEmail5);
7403 
7404         // Query with per schema result grouping. Only the last two documents of each schema type
7405         // should be returned ('doc3', 'doc2', 'email5', 'email4')
7406         searchResults =
7407                 mDb1.search(
7408                         "body",
7409                         new SearchSpec.Builder()
7410                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
7411                                 .setResultGrouping(
7412                                         SearchSpec.GROUPING_TYPE_PER_SCHEMA, /* limit= */ 2)
7413                                 .build());
7414         documents = convertSearchResultsToDocuments(searchResults);
7415         assertThat(documents).containsExactly(inDoc3, inDoc2, inEmail5, inEmail4);
7416 
7417         // Query with per package and per namespace result grouping. Only the last document in each
7418         // namespace should be returned ('doc3', 'doc2', 'email5' and 'email2').
7419         searchResults =
7420                 mDb1.search(
7421                         "body",
7422                         new SearchSpec.Builder()
7423                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
7424                                 .setResultGrouping(
7425                                         SearchSpec.GROUPING_TYPE_PER_NAMESPACE
7426                                                 | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
7427                                         /* limit= */ 1)
7428                                 .build());
7429         documents = convertSearchResultsToDocuments(searchResults);
7430         assertThat(documents).containsExactly(inDoc3, inDoc2, inEmail5, inEmail2);
7431 
7432         // Query with per package and per namespace result grouping. Only the last two documents
7433         // in each namespace should be returned ('doc3', 'doc2', 'doc1', 'email5', 'email4',
7434         // 'email2', 'email1')
7435         searchResults =
7436                 mDb1.search(
7437                         "body",
7438                         new SearchSpec.Builder()
7439                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
7440                                 .setResultGrouping(
7441                                         SearchSpec.GROUPING_TYPE_PER_NAMESPACE
7442                                                 | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
7443                                         /* limit= */ 2)
7444                                 .build());
7445         documents = convertSearchResultsToDocuments(searchResults);
7446         assertThat(documents)
7447                 .containsExactly(inDoc3, inDoc2, inDoc1, inEmail5, inEmail4, inEmail2, inEmail1);
7448 
7449         // Query with per package and per schema type result grouping. Only the last document in
7450         // each schema type should be returned. ('doc3', 'email5')
7451         searchResults =
7452                 mDb1.search(
7453                         "body",
7454                         new SearchSpec.Builder()
7455                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
7456                                 .setResultGrouping(
7457                                         SearchSpec.GROUPING_TYPE_PER_SCHEMA
7458                                                 | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
7459                                         /* limit= */ 1)
7460                                 .build());
7461         documents = convertSearchResultsToDocuments(searchResults);
7462         assertThat(documents).containsExactly(inDoc3, inEmail5);
7463 
7464         // Query with per package and per schema type result grouping. Only the last two document in
7465         // each schema type should be returned. ('doc3', 'doc2', 'email5', 'email4')
7466         searchResults =
7467                 mDb1.search(
7468                         "body",
7469                         new SearchSpec.Builder()
7470                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
7471                                 .setResultGrouping(
7472                                         SearchSpec.GROUPING_TYPE_PER_SCHEMA
7473                                                 | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
7474                                         /* limit= */ 2)
7475                                 .build());
7476         documents = convertSearchResultsToDocuments(searchResults);
7477         assertThat(documents).containsExactly(inDoc3, inDoc2, inEmail5, inEmail4);
7478 
7479         // Query with per namespace and per schema type result grouping. Only the last document in
7480         // each namespace should be returned. ('doc3', 'doc2', 'email5' and 'email2').
7481         searchResults =
7482                 mDb1.search(
7483                         "body",
7484                         new SearchSpec.Builder()
7485                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
7486                                 .setResultGrouping(
7487                                         SearchSpec.GROUPING_TYPE_PER_NAMESPACE
7488                                                 | SearchSpec.GROUPING_TYPE_PER_SCHEMA,
7489                                         /* limit= */ 1)
7490                                 .build());
7491         documents = convertSearchResultsToDocuments(searchResults);
7492         assertThat(documents).containsExactly(inDoc3, inDoc2, inEmail5, inEmail2);
7493 
7494         // Query with per namespace and per schema type result grouping. Only the last two documents
7495         // in each namespace should be returned. ('doc3', 'doc2', 'doc1', 'email5', 'email4',
7496         // 'email2', 'email1')
7497         searchResults =
7498                 mDb1.search(
7499                         "body",
7500                         new SearchSpec.Builder()
7501                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
7502                                 .setResultGrouping(
7503                                         SearchSpec.GROUPING_TYPE_PER_NAMESPACE
7504                                                 | SearchSpec.GROUPING_TYPE_PER_SCHEMA,
7505                                         /* limit= */ 2)
7506                                 .build());
7507         documents = convertSearchResultsToDocuments(searchResults);
7508         assertThat(documents)
7509                 .containsExactly(inDoc3, inDoc2, inDoc1, inEmail5, inEmail4, inEmail2, inEmail1);
7510 
7511         // Query with per namespace, per package and per schema type result grouping. Only the last
7512         // document in each namespace should be returned. ('doc3', 'doc2', 'email5' and 'email2')
7513         searchResults =
7514                 mDb1.search(
7515                         "body",
7516                         new SearchSpec.Builder()
7517                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
7518                                 .setResultGrouping(
7519                                         SearchSpec.GROUPING_TYPE_PER_NAMESPACE
7520                                                 | SearchSpec.GROUPING_TYPE_PER_SCHEMA
7521                                                 | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
7522                                         /* limit= */ 1)
7523                                 .build());
7524         documents = convertSearchResultsToDocuments(searchResults);
7525         assertThat(documents).containsExactly(inDoc3, inDoc2, inEmail5, inEmail2);
7526 
7527         // Query with per namespace, per package and per schema type result grouping. Only the last
7528         // two documents in each namespace should be returned.('doc3', 'doc2', 'doc1', 'email5',
7529         // 'email4', 'email2', 'email1')
7530         searchResults =
7531                 mDb1.search(
7532                         "body",
7533                         new SearchSpec.Builder()
7534                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
7535                                 .setResultGrouping(
7536                                         SearchSpec.GROUPING_TYPE_PER_NAMESPACE
7537                                                 | SearchSpec.GROUPING_TYPE_PER_SCHEMA
7538                                                 | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
7539                                         /* limit= */ 2)
7540                                 .build());
7541         documents = convertSearchResultsToDocuments(searchResults);
7542         assertThat(documents)
7543                 .containsExactly(inDoc3, inDoc2, inDoc1, inEmail5, inEmail4, inEmail2, inEmail1);
7544     }
7545 
7546     @Test
testQuery_ResultGroupingLimits_SchemaGroupingNotSupported()7547     public void testQuery_ResultGroupingLimits_SchemaGroupingNotSupported() throws Exception {
7548         assumeFalse(
7549                 mDb1.getFeatures()
7550                         .isFeatureSupported(Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA));
7551         // Schema registration
7552         mDb1.setSchemaAsync(
7553                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
7554                 .get();
7555 
7556         // Index four documents.
7557         AppSearchEmail inEmail1 =
7558                 new AppSearchEmail.Builder("namespace1", "id1")
7559                         .setFrom("[email protected]")
7560                         .setTo("[email protected]", "[email protected]")
7561                         .setSubject("testPut example")
7562                         .setBody("This is the body of the testPut email")
7563                         .build();
7564         checkIsBatchResultSuccess(
7565                 mDb1.putAsync(
7566                         new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build()));
7567         AppSearchEmail inEmail2 =
7568                 new AppSearchEmail.Builder("namespace1", "id2")
7569                         .setFrom("[email protected]")
7570                         .setTo("[email protected]", "[email protected]")
7571                         .setSubject("testPut example")
7572                         .setBody("This is the body of the testPut email")
7573                         .build();
7574         checkIsBatchResultSuccess(
7575                 mDb1.putAsync(
7576                         new PutDocumentsRequest.Builder().addGenericDocuments(inEmail2).build()));
7577         AppSearchEmail inEmail3 =
7578                 new AppSearchEmail.Builder("namespace2", "id3")
7579                         .setFrom("[email protected]")
7580                         .setTo("[email protected]", "[email protected]")
7581                         .setSubject("testPut example")
7582                         .setBody("This is the body of the testPut email")
7583                         .build();
7584         checkIsBatchResultSuccess(
7585                 mDb1.putAsync(
7586                         new PutDocumentsRequest.Builder().addGenericDocuments(inEmail3).build()));
7587         AppSearchEmail inEmail4 =
7588                 new AppSearchEmail.Builder("namespace2", "id4")
7589                         .setFrom("[email protected]")
7590                         .setTo("[email protected]", "[email protected]")
7591                         .setSubject("testPut example")
7592                         .setBody("This is the body of the testPut email")
7593                         .build();
7594         checkIsBatchResultSuccess(
7595                 mDb1.putAsync(
7596                         new PutDocumentsRequest.Builder().addGenericDocuments(inEmail4).build()));
7597 
7598         // SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA is not supported.
7599         // UnsupportedOperationException will be thrown.
7600         SearchSpec searchSpec1 =
7601                 new SearchSpec.Builder()
7602                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
7603                         .setResultGrouping(SearchSpec.GROUPING_TYPE_PER_SCHEMA, /* limit= */ 1)
7604                         .build();
7605         UnsupportedOperationException exception =
7606                 assertThrows(
7607                         UnsupportedOperationException.class,
7608                         () -> mDb1.search("body", searchSpec1));
7609         assertThat(exception)
7610                 .hasMessageThat()
7611                 .contains(
7612                         Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA
7613                                 + " is not available on this"
7614                                 + " AppSearch implementation.");
7615 
7616         // SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA is not supported.
7617         // UnsupportedOperationException will be thrown.
7618         SearchSpec searchSpec2 =
7619                 new SearchSpec.Builder()
7620                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
7621                         .setResultGrouping(
7622                                 SearchSpec.GROUPING_TYPE_PER_PACKAGE
7623                                         | SearchSpec.GROUPING_TYPE_PER_SCHEMA,
7624                                 /* limit= */ 1)
7625                         .build();
7626         exception =
7627                 assertThrows(
7628                         UnsupportedOperationException.class,
7629                         () -> mDb1.search("body", searchSpec2));
7630         assertThat(exception)
7631                 .hasMessageThat()
7632                 .contains(
7633                         Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA
7634                                 + " is not available on this"
7635                                 + " AppSearch implementation.");
7636 
7637         // SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA is not supported.
7638         // UnsupportedOperationException will be thrown.
7639         SearchSpec searchSpec3 =
7640                 new SearchSpec.Builder()
7641                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
7642                         .setResultGrouping(
7643                                 SearchSpec.GROUPING_TYPE_PER_NAMESPACE
7644                                         | SearchSpec.GROUPING_TYPE_PER_SCHEMA,
7645                                 /* limit= */ 1)
7646                         .build();
7647         exception =
7648                 assertThrows(
7649                         UnsupportedOperationException.class,
7650                         () -> mDb1.search("body", searchSpec3));
7651         assertThat(exception)
7652                 .hasMessageThat()
7653                 .contains(
7654                         Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA
7655                                 + " is not available on this"
7656                                 + " AppSearch implementation.");
7657 
7658         // SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA is not supported.
7659         // UnsupportedOperationException will be thrown.
7660         SearchSpec searchSpec4 =
7661                 new SearchSpec.Builder()
7662                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
7663                         .setResultGrouping(
7664                                 SearchSpec.GROUPING_TYPE_PER_NAMESPACE
7665                                         | SearchSpec.GROUPING_TYPE_PER_SCHEMA
7666                                         | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
7667                                 /* limit= */ 1)
7668                         .build();
7669         exception =
7670                 assertThrows(
7671                         UnsupportedOperationException.class,
7672                         () -> mDb1.search("body", searchSpec4));
7673         assertThat(exception)
7674                 .hasMessageThat()
7675                 .contains(
7676                         Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA
7677                                 + " is not available on this"
7678                                 + " AppSearch implementation.");
7679     }
7680 
7681     @Test
testIndexNestedDocuments()7682     public void testIndexNestedDocuments() throws Exception {
7683         // Schema registration
7684         mDb1.setSchemaAsync(
7685                         new SetSchemaRequest.Builder()
7686                                 .addSchemas(AppSearchEmail.SCHEMA)
7687                                 .addSchemas(
7688                                         new AppSearchSchema.Builder("YesNestedIndex")
7689                                                 .addProperty(
7690                                                         new AppSearchSchema.DocumentPropertyConfig
7691                                                                         .Builder(
7692                                                                         "prop",
7693                                                                         AppSearchEmail.SCHEMA_TYPE)
7694                                                                 .setShouldIndexNestedProperties(
7695                                                                         true)
7696                                                                 .build())
7697                                                 .build())
7698                                 .addSchemas(
7699                                         new AppSearchSchema.Builder("NoNestedIndex")
7700                                                 .addProperty(
7701                                                         new AppSearchSchema.DocumentPropertyConfig
7702                                                                         .Builder(
7703                                                                         "prop",
7704                                                                         AppSearchEmail.SCHEMA_TYPE)
7705                                                                 .setShouldIndexNestedProperties(
7706                                                                         false)
7707                                                                 .build())
7708                                                 .build())
7709                                 .build())
7710                 .get();
7711 
7712         // Index the documents.
7713         AppSearchEmail email =
7714                 new AppSearchEmail.Builder("", "").setSubject("This is the body").build();
7715         GenericDocument yesNestedIndex =
7716                 new GenericDocument.Builder<>("namespace", "yesNestedIndex", "YesNestedIndex")
7717                         .setPropertyDocument("prop", email)
7718                         .build();
7719         GenericDocument noNestedIndex =
7720                 new GenericDocument.Builder<>("namespace", "noNestedIndex", "NoNestedIndex")
7721                         .setPropertyDocument("prop", email)
7722                         .build();
7723         checkIsBatchResultSuccess(
7724                 mDb1.putAsync(
7725                         new PutDocumentsRequest.Builder()
7726                                 .addGenericDocuments(yesNestedIndex, noNestedIndex)
7727                                 .build()));
7728 
7729         // Query.
7730         SearchResultsShim searchResults =
7731                 mDb1.search(
7732                         "body",
7733                         new SearchSpec.Builder()
7734                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
7735                                 .setSnippetCount(10)
7736                                 .setSnippetCountPerProperty(10)
7737                                 .build());
7738         List<SearchResult> page = searchResults.getNextPageAsync().get();
7739         assertThat(page).hasSize(1);
7740         assertThat(page.get(0).getGenericDocument()).isEqualTo(yesNestedIndex);
7741         List<SearchResult.MatchInfo> matches = page.get(0).getMatchInfos();
7742         assertThat(matches).hasSize(1);
7743         assertThat(matches.get(0).getPropertyPath()).isEqualTo("prop.subject");
7744         assertThat(matches.get(0).getPropertyPathObject())
7745                 .isEqualTo(new PropertyPath("prop.subject"));
7746         assertThat(matches.get(0).getFullText()).isEqualTo("This is the body");
7747         assertThat(matches.get(0).getExactMatch()).isEqualTo("body");
7748     }
7749 
7750     @Test
testCJKTQuery()7751     public void testCJKTQuery() throws Exception {
7752         // Schema registration
7753         mDb1.setSchemaAsync(
7754                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
7755                 .get();
7756 
7757         // Index a document to instance 1.
7758         AppSearchEmail inEmail1 =
7759                 new AppSearchEmail.Builder("namespace", "uri1").setBody("他是個男孩 is a boy").build();
7760         checkIsBatchResultSuccess(
7761                 mDb1.putAsync(
7762                         new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build()));
7763 
7764         // Query for "他" (He)
7765         SearchResultsShim searchResults =
7766                 mDb1.search(
7767                         "他",
7768                         new SearchSpec.Builder()
7769                                 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
7770                                 .build());
7771         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
7772         assertThat(documents).containsExactly(inEmail1);
7773 
7774         // Query for "男孩" (boy)
7775         searchResults =
7776                 mDb1.search(
7777                         "男孩",
7778                         new SearchSpec.Builder()
7779                                 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
7780                                 .build());
7781         documents = convertSearchResultsToDocuments(searchResults);
7782         assertThat(documents).containsExactly(inEmail1);
7783 
7784         // Query for "boy"
7785         searchResults =
7786                 mDb1.search(
7787                         "boy",
7788                         new SearchSpec.Builder()
7789                                 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
7790                                 .build());
7791         documents = convertSearchResultsToDocuments(searchResults);
7792         assertThat(documents).containsExactly(inEmail1);
7793     }
7794 
7795     @Test
testSetSchemaWithIncompatibleNestedSchema()7796     public void testSetSchemaWithIncompatibleNestedSchema() throws Exception {
7797         // 1. Set the original schema. This should succeed without any problems.
7798         AppSearchSchema originalNestedSchema =
7799                 new AppSearchSchema.Builder("TypeA")
7800                         .addProperty(
7801                                 new StringPropertyConfig.Builder("prop1")
7802                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
7803                                         .build())
7804                         .build();
7805         SetSchemaRequest originalRequest =
7806                 new SetSchemaRequest.Builder().addSchemas(originalNestedSchema).build();
7807         mDb1.setSchemaAsync(originalRequest).get();
7808 
7809         // 2. Set a new schema with a new type that refers to "TypeA" and an incompatible change to
7810         // "TypeA". This should fail.
7811         AppSearchSchema newNestedSchema =
7812                 new AppSearchSchema.Builder("TypeA")
7813                         .addProperty(
7814                                 new StringPropertyConfig.Builder("prop1")
7815                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
7816                                         .build())
7817                         .build();
7818         AppSearchSchema newSchema =
7819                 new AppSearchSchema.Builder("TypeB")
7820                         .addProperty(
7821                                 new AppSearchSchema.DocumentPropertyConfig.Builder("prop2", "TypeA")
7822                                         .build())
7823                         .build();
7824         final SetSchemaRequest newRequest =
7825                 new SetSchemaRequest.Builder().addSchemas(newNestedSchema, newSchema).build();
7826         ExecutionException executionException =
7827                 assertThrows(ExecutionException.class, () -> mDb1.setSchemaAsync(newRequest).get());
7828         assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
7829         AppSearchException exception = (AppSearchException) executionException.getCause();
7830         assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_SCHEMA);
7831         assertThat(exception).hasMessageThat().contains("Schema is incompatible.");
7832         assertThat(exception).hasMessageThat().contains("Incompatible types: {TypeA}");
7833 
7834         // 3. Now set that same set of schemas but with forceOverride=true. This should succeed.
7835         SetSchemaRequest newRequestForced =
7836                 new SetSchemaRequest.Builder()
7837                         .addSchemas(newNestedSchema, newSchema)
7838                         .setForceOverride(true)
7839                         .build();
7840         mDb1.setSchemaAsync(newRequestForced).get();
7841     }
7842 
7843     @Test
testEmojiSnippet()7844     public void testEmojiSnippet() throws Exception {
7845         // Schema registration
7846         mDb1.setSchemaAsync(
7847                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
7848                 .get();
7849 
7850         // String:     "Luca Brasi sleeps with the ������."
7851         //              ^    ^     ^      ^    ^   ^ ^  ^ ^
7852         // UTF8 idx:    0    5     11     18   23 27 3135 39
7853         // UTF16 idx:   0    5     11     18   23 27 2931 33
7854         // Breaks into segments: "Luca", "Brasi", "sleeps", "with", "the", "��", "��"
7855         // and "��".
7856         // Index a document to instance 1.
7857         String sicilianMessage = "Luca Brasi sleeps with the ������.";
7858         AppSearchEmail inEmail1 =
7859                 new AppSearchEmail.Builder("namespace", "uri1").setBody(sicilianMessage).build();
7860         checkIsBatchResultSuccess(
7861                 mDb1.putAsync(
7862                         new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build()));
7863 
7864         AppSearchEmail inEmail2 =
7865                 new AppSearchEmail.Builder("namespace", "uri2")
7866                         .setBody("Some other content.")
7867                         .build();
7868         checkIsBatchResultSuccess(
7869                 mDb1.putAsync(
7870                         new PutDocumentsRequest.Builder().addGenericDocuments(inEmail2).build()));
7871 
7872         // Query for "��"
7873         SearchResultsShim searchResults =
7874                 mDb1.search(
7875                         "��",
7876                         new SearchSpec.Builder()
7877                                 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
7878                                 .setSnippetCount(1)
7879                                 .setSnippetCountPerProperty(1)
7880                                 .build());
7881         List<SearchResult> page = searchResults.getNextPageAsync().get();
7882         assertThat(page).hasSize(1);
7883         assertThat(page.get(0).getGenericDocument()).isEqualTo(inEmail1);
7884         List<SearchResult.MatchInfo> matches = page.get(0).getMatchInfos();
7885         assertThat(matches).hasSize(1);
7886         assertThat(matches.get(0).getPropertyPath()).isEqualTo("body");
7887         assertThat(matches.get(0).getFullText()).isEqualTo(sicilianMessage);
7888         assertThat(matches.get(0).getExactMatch()).isEqualTo("��");
7889         if (mDb1.getFeatures().isFeatureSupported(Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH)) {
7890             assertThat(matches.get(0).getSubmatch()).isEqualTo("��");
7891         }
7892     }
7893 
7894     @Test
testRfc822()7895     public void testRfc822() throws Exception {
7896         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.TOKENIZER_TYPE_RFC822));
7897         AppSearchSchema emailSchema =
7898                 new AppSearchSchema.Builder("Email")
7899                         .addProperty(
7900                                 new StringPropertyConfig.Builder("address")
7901                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
7902                                         .setIndexingType(
7903                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
7904                                         .setTokenizerType(
7905                                                 StringPropertyConfig.TOKENIZER_TYPE_RFC822)
7906                                         .build())
7907                         .build();
7908         mDb1.setSchemaAsync(
7909                         new SetSchemaRequest.Builder()
7910                                 .setForceOverride(true)
7911                                 .addSchemas(emailSchema)
7912                                 .build())
7913                 .get();
7914 
7915         GenericDocument email =
7916                 new GenericDocument.Builder<>("NS", "alex1", "Email")
7917                         .setPropertyString("address", "Alex Saveliev <[email protected]>")
7918                         .build();
7919         mDb1.putAsync(new PutDocumentsRequest.Builder().addGenericDocuments(email).build()).get();
7920 
7921         SearchResultsShim sr = mDb1.search("com", new SearchSpec.Builder().build());
7922         List<SearchResult> page = sr.getNextPageAsync().get();
7923 
7924         // RFC tokenization will produce the following tokens for
7925         // "Alex Saveliev <[email protected]>" : ["Alex Saveliev <[email protected]>", "Alex",
7926         // "Saveliev", "alex.sav", "[email protected]", "alex.sav", "google", "com"]. Therefore,
7927         // a query for "com" should match the document.
7928         assertThat(page).hasSize(1);
7929         assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("alex1");
7930 
7931         // Plain tokenizer will not match this
7932         AppSearchSchema plainEmailSchema =
7933                 new AppSearchSchema.Builder("Email")
7934                         .addProperty(
7935                                 new StringPropertyConfig.Builder("address")
7936                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
7937                                         .setIndexingType(
7938                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
7939                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
7940                                         .build())
7941                         .build();
7942 
7943         // Flipping the tokenizer type is a backwards compatible change. The index will be
7944         // rebuilt with the email doc being tokenized in the new way.
7945         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(plainEmailSchema).build())
7946                 .get();
7947 
7948         sr = mDb1.search("com", new SearchSpec.Builder().build());
7949 
7950         // Plain tokenization will produce the following tokens for
7951         // "Alex Saveliev <[email protected]>" : ["Alex", "Saveliev", "<", "alex.sav",
7952         // "google.com", ">"]. So "com" will not match any of the tokens produced.
7953         assertThat(sr.getNextPageAsync().get()).hasSize(0);
7954     }
7955 
7956     @Test
testRfc822_unsupportedFeature_throwsException()7957     public void testRfc822_unsupportedFeature_throwsException() {
7958         assumeFalse(mDb1.getFeatures().isFeatureSupported(Features.TOKENIZER_TYPE_RFC822));
7959 
7960         AppSearchSchema emailSchema =
7961                 new AppSearchSchema.Builder("Email")
7962                         .addProperty(
7963                                 new StringPropertyConfig.Builder("address")
7964                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
7965                                         .setIndexingType(
7966                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
7967                                         .setTokenizerType(
7968                                                 StringPropertyConfig.TOKENIZER_TYPE_RFC822)
7969                                         .build())
7970                         .build();
7971 
7972         Exception e =
7973                 assertThrows(
7974                         IllegalArgumentException.class,
7975                         () ->
7976                                 mDb1.setSchemaAsync(
7977                                                 new SetSchemaRequest.Builder()
7978                                                         .setForceOverride(true)
7979                                                         .addSchemas(emailSchema)
7980                                                         .build())
7981                                         .get());
7982         assertThat(e.getMessage()).isEqualTo("tokenizerType is out of range of [0, 1] (too high)");
7983     }
7984 
7985     @Test
testQuery_verbatimSearch()7986     public void testQuery_verbatimSearch() throws Exception {
7987         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.VERBATIM_SEARCH));
7988         AppSearchSchema verbatimSchema =
7989                 new AppSearchSchema.Builder("VerbatimSchema")
7990                         .addProperty(
7991                                 new StringPropertyConfig.Builder("verbatimProp")
7992                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
7993                                         .setIndexingType(
7994                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
7995                                         .setTokenizerType(
7996                                                 StringPropertyConfig.TOKENIZER_TYPE_VERBATIM)
7997                                         .build())
7998                         .build();
7999         mDb1.setSchemaAsync(
8000                         new SetSchemaRequest.Builder()
8001                                 .setForceOverride(true)
8002                                 .addSchemas(verbatimSchema)
8003                                 .build())
8004                 .get();
8005 
8006         GenericDocument email =
8007                 new GenericDocument.Builder<>("namespace1", "id1", "VerbatimSchema")
8008                         .setPropertyString("verbatimProp", "Hello, world!")
8009                         .build();
8010         mDb1.putAsync(new PutDocumentsRequest.Builder().addGenericDocuments(email).build()).get();
8011 
8012         SearchResultsShim sr =
8013                 mDb1.search(
8014                         "\"Hello, world!\"",
8015                         new SearchSpec.Builder().setVerbatimSearchEnabled(true).build());
8016         List<SearchResult> page = sr.getNextPageAsync().get();
8017 
8018         // Verbatim tokenization would produce one token 'Hello, world!'.
8019         assertThat(page).hasSize(1);
8020         assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id1");
8021     }
8022 
8023     @Test
testQuery_verbatimSearchWithoutEnablingFeatureFails()8024     public void testQuery_verbatimSearchWithoutEnablingFeatureFails() throws Exception {
8025         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.VERBATIM_SEARCH));
8026         AppSearchSchema verbatimSchema =
8027                 new AppSearchSchema.Builder("VerbatimSchema")
8028                         .addProperty(
8029                                 new StringPropertyConfig.Builder("verbatimProp")
8030                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
8031                                         .setIndexingType(
8032                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
8033                                         .setTokenizerType(
8034                                                 StringPropertyConfig.TOKENIZER_TYPE_VERBATIM)
8035                                         .build())
8036                         .build();
8037         mDb1.setSchemaAsync(
8038                         new SetSchemaRequest.Builder()
8039                                 .setForceOverride(true)
8040                                 .addSchemas(verbatimSchema)
8041                                 .build())
8042                 .get();
8043 
8044         GenericDocument email =
8045                 new GenericDocument.Builder<>("namespace1", "id1", "VerbatimSchema")
8046                         .setPropertyString("verbatimProp", "Hello, world!")
8047                         .build();
8048         mDb1.putAsync(new PutDocumentsRequest.Builder().addGenericDocuments(email).build()).get();
8049 
8050         // Disable VERBATIM_SEARCH in the SearchSpec.
8051         SearchResultsShim searchResults =
8052                 mDb1.search(
8053                         "\"Hello, world!\"",
8054                         new SearchSpec.Builder().setVerbatimSearchEnabled(false).build());
8055         ExecutionException executionException =
8056                 assertThrows(
8057                         ExecutionException.class, () -> searchResults.getNextPageAsync().get());
8058         assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
8059         AppSearchException exception = (AppSearchException) executionException.getCause();
8060         assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT);
8061         assertThat(exception).hasMessageThat().contains("Attempted use of unenabled feature");
8062         assertThat(exception).hasMessageThat().contains(Features.VERBATIM_SEARCH);
8063     }
8064 
8065     @Test
testQuery_listFilterQueryWithEnablingFeatureSucceeds()8066     public void testQuery_listFilterQueryWithEnablingFeatureSucceeds() throws Exception {
8067         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_QUERY_LANGUAGE));
8068         AppSearchSchema schema =
8069                 new AppSearchSchema.Builder("Schema")
8070                         .addProperty(
8071                                 new StringPropertyConfig.Builder("prop")
8072                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
8073                                         .setIndexingType(
8074                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
8075                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
8076                                         .build())
8077                         .build();
8078         mDb1.setSchemaAsync(
8079                         new SetSchemaRequest.Builder()
8080                                 .setForceOverride(true)
8081                                 .addSchemas(schema)
8082                                 .build())
8083                 .get();
8084 
8085         GenericDocument email =
8086                 new GenericDocument.Builder<>("namespace1", "id1", "Schema")
8087                         .setPropertyString("prop", "Hello, world!")
8088                         .build();
8089         mDb1.putAsync(new PutDocumentsRequest.Builder().addGenericDocuments(email).build()).get();
8090 
8091         SearchSpec searchSpec =
8092                 new SearchSpec.Builder()
8093                         .setListFilterQueryLanguageEnabled(true)
8094                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
8095                         .build();
8096         // Support for function calls `search`, `createList` was added in list filters
8097         SearchResultsShim searchResults =
8098                 mDb1.search("search(\"hello\", createList(\"prop\"))", searchSpec);
8099         List<SearchResult> page = searchResults.getNextPageAsync().get();
8100         assertThat(page).hasSize(1);
8101         assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id1");
8102 
8103         // Support for prefix operator * was added in list filters.
8104         searchResults = mDb1.search("wor*", searchSpec);
8105         page = searchResults.getNextPageAsync().get();
8106         assertThat(page).hasSize(1);
8107         assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id1");
8108 
8109         // Combining negations with compound statements and property restricts was added in list
8110         // filters.
8111         searchResults = mDb1.search("NOT (foo OR otherProp:hello)", searchSpec);
8112         page = searchResults.getNextPageAsync().get();
8113         assertThat(page).hasSize(1);
8114         assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id1");
8115     }
8116 
8117     @Test
testQuery_PropertyDefinedWithEnablingFeatureSucceeds()8118     public void testQuery_PropertyDefinedWithEnablingFeatureSucceeds() throws Exception {
8119         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_QUERY_LANGUAGE));
8120         AppSearchSchema schema1 =
8121                 new AppSearchSchema.Builder("Schema1")
8122                         .addProperty(
8123                                 new StringPropertyConfig.Builder("prop1")
8124                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
8125                                         .setIndexingType(
8126                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
8127                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
8128                                         .build())
8129                         .build();
8130         AppSearchSchema schema2 =
8131                 new AppSearchSchema.Builder("Schema2")
8132                         .addProperty(
8133                                 new StringPropertyConfig.Builder("prop2")
8134                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
8135                                         .setIndexingType(
8136                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
8137                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
8138                                         .build())
8139                         .build();
8140         mDb1.setSchemaAsync(
8141                         new SetSchemaRequest.Builder()
8142                                 .setForceOverride(true)
8143                                 .addSchemas(schema1, schema2)
8144                                 .build())
8145                 .get();
8146 
8147         GenericDocument doc1 =
8148                 new GenericDocument.Builder<>("namespace1", "id1", "Schema1")
8149                         .setPropertyString("prop1", "Hello, world!")
8150                         .build();
8151         GenericDocument doc2 =
8152                 new GenericDocument.Builder<>("namespace1", "id2", "Schema2")
8153                         .setPropertyString("prop2", "Hello, world!")
8154                         .build();
8155         mDb1.putAsync(new PutDocumentsRequest.Builder().addGenericDocuments(doc1, doc2).build())
8156                 .get();
8157 
8158         SearchSpec searchSpec =
8159                 new SearchSpec.Builder()
8160                         .setListFilterQueryLanguageEnabled(true)
8161                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
8162                         .build();
8163         // Support for function calls `search`, `createList` was added in list filters
8164         SearchResultsShim searchResults = mDb1.search("propertyDefined(\"prop1\")", searchSpec);
8165         List<SearchResult> page = searchResults.getNextPageAsync().get();
8166         assertThat(page).hasSize(1);
8167         assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id1");
8168 
8169         // Support for prefix operator * was added in list filters.
8170         searchResults = mDb1.search("propertyDefined(\"prop2\")", searchSpec);
8171         page = searchResults.getNextPageAsync().get();
8172         assertThat(page).hasSize(1);
8173         assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id2");
8174 
8175         // Combining negations with compound statements and property restricts was added in list
8176         // filters.
8177         searchResults = mDb1.search("NOT propertyDefined(\"prop1\")", searchSpec);
8178         page = searchResults.getNextPageAsync().get();
8179         assertThat(page).hasSize(1);
8180         assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id2");
8181     }
8182 
8183     @Test
testQuery_listFilterQueryWithoutEnablingFeatureFails()8184     public void testQuery_listFilterQueryWithoutEnablingFeatureFails() throws Exception {
8185         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_QUERY_LANGUAGE));
8186         AppSearchSchema schema =
8187                 new AppSearchSchema.Builder("Schema")
8188                         .addProperty(
8189                                 new StringPropertyConfig.Builder("prop")
8190                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
8191                                         .setIndexingType(
8192                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
8193                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
8194                                         .build())
8195                         .build();
8196         mDb1.setSchemaAsync(
8197                         new SetSchemaRequest.Builder()
8198                                 .setForceOverride(true)
8199                                 .addSchemas(schema)
8200                                 .build())
8201                 .get();
8202 
8203         GenericDocument email =
8204                 new GenericDocument.Builder<>("namespace1", "id1", "Schema")
8205                         .setPropertyString("prop", "Hello, world!")
8206                         .build();
8207         mDb1.putAsync(new PutDocumentsRequest.Builder().addGenericDocuments(email).build()).get();
8208 
8209         // Disable LIST_FILTER_QUERY_LANGUAGE in the SearchSpec.
8210         SearchSpec searchSpec =
8211                 new SearchSpec.Builder().setListFilterQueryLanguageEnabled(false).build();
8212         SearchResultsShim searchResults =
8213                 mDb1.search("search(\"hello\", createList(\"prop\"))", searchSpec);
8214         ExecutionException executionException =
8215                 assertThrows(
8216                         ExecutionException.class, () -> searchResults.getNextPageAsync().get());
8217         assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
8218         AppSearchException exception = (AppSearchException) executionException.getCause();
8219         assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT);
8220         assertThat(exception).hasMessageThat().contains("Attempted use of unenabled feature");
8221         assertThat(exception).hasMessageThat().contains(Features.LIST_FILTER_QUERY_LANGUAGE);
8222 
8223         SearchResultsShim searchResults2 = mDb1.search("wor*", searchSpec);
8224         executionException =
8225                 assertThrows(
8226                         ExecutionException.class, () -> searchResults2.getNextPageAsync().get());
8227         assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
8228         exception = (AppSearchException) executionException.getCause();
8229         assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT);
8230         assertThat(exception).hasMessageThat().contains("Attempted use of unenabled feature");
8231         assertThat(exception).hasMessageThat().contains(Features.LIST_FILTER_QUERY_LANGUAGE);
8232 
8233         SearchResultsShim searchResults3 = mDb1.search("NOT (foo OR otherProp:hello)", searchSpec);
8234         executionException =
8235                 assertThrows(
8236                         ExecutionException.class, () -> searchResults3.getNextPageAsync().get());
8237         assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
8238         exception = (AppSearchException) executionException.getCause();
8239         assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT);
8240         assertThat(exception).hasMessageThat().contains("Attempted use of unenabled feature");
8241         assertThat(exception).hasMessageThat().contains(Features.LIST_FILTER_QUERY_LANGUAGE);
8242 
8243         SearchResultsShim searchResults4 = mDb1.search("propertyDefined(\"prop\")", searchSpec);
8244         executionException =
8245                 assertThrows(
8246                         ExecutionException.class, () -> searchResults4.getNextPageAsync().get());
8247         assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
8248         exception = (AppSearchException) executionException.getCause();
8249         assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT);
8250         assertThat(exception).hasMessageThat().contains("Attempted use of unenabled feature");
8251         assertThat(exception).hasMessageThat().contains(Features.LIST_FILTER_QUERY_LANGUAGE);
8252     }
8253 
8254     @Test
testQuery_listFilterQueryFeatures_notSupported()8255     public void testQuery_listFilterQueryFeatures_notSupported() throws Exception {
8256         assumeFalse(mDb1.getFeatures().isFeatureSupported(Features.NUMERIC_SEARCH));
8257         assumeFalse(mDb1.getFeatures().isFeatureSupported(Features.VERBATIM_SEARCH));
8258         assumeFalse(mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_QUERY_LANGUAGE));
8259 
8260         // UnsupportedOperationException will be thrown with these queries so no need to
8261         // define a schema and index document.
8262         SearchSpec.Builder builder = new SearchSpec.Builder();
8263         SearchSpec searchSpec1 = builder.setNumericSearchEnabled(true).build();
8264         SearchSpec searchSpec2 = builder.setVerbatimSearchEnabled(true).build();
8265         SearchSpec searchSpec3 = builder.setListFilterQueryLanguageEnabled(true).build();
8266 
8267         assertThrows(
8268                 UnsupportedOperationException.class,
8269                 () -> mDb1.search("\"Hello, world!\"", searchSpec1));
8270         assertThrows(
8271                 UnsupportedOperationException.class,
8272                 () -> mDb1.search("\"Hello, world!\"", searchSpec2));
8273         assertThrows(
8274                 UnsupportedOperationException.class,
8275                 () -> mDb1.search("\"Hello, world!\"", searchSpec3));
8276     }
8277 
8278     @Test
testQuery_listFilterQueryHasPropertyFunction_notSupported()8279     public void testQuery_listFilterQueryHasPropertyFunction_notSupported() throws Exception {
8280         assumeFalse(
8281                 mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_HAS_PROPERTY_FUNCTION));
8282 
8283         // UnsupportedOperationException will be thrown with these queries so no need to
8284         // define a schema and index document.
8285         SearchSpec.Builder builder = new SearchSpec.Builder();
8286         SearchSpec searchSpec = builder.setListFilterHasPropertyFunctionEnabled(true).build();
8287 
8288         UnsupportedOperationException exception =
8289                 assertThrows(
8290                         UnsupportedOperationException.class,
8291                         () -> mDb1.search("\"Hello, world!\"", searchSpec));
8292         assertThat(exception)
8293                 .hasMessageThat()
8294                 .contains(
8295                         Features.LIST_FILTER_HAS_PROPERTY_FUNCTION
8296                                 + " is not available on this AppSearch implementation.");
8297     }
8298 
8299     @Test
testQuery_hasPropertyFunction()8300     public void testQuery_hasPropertyFunction() throws Exception {
8301         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_QUERY_LANGUAGE));
8302         assumeTrue(
8303                 mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_HAS_PROPERTY_FUNCTION));
8304         AppSearchSchema schema =
8305                 new AppSearchSchema.Builder("Schema")
8306                         .addProperty(
8307                                 new StringPropertyConfig.Builder("prop1")
8308                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
8309                                         .setIndexingType(
8310                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
8311                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
8312                                         .build())
8313                         .addProperty(
8314                                 new StringPropertyConfig.Builder("prop2")
8315                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
8316                                         .setIndexingType(
8317                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
8318                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
8319                                         .build())
8320                         .build();
8321         mDb1.setSchemaAsync(
8322                         new SetSchemaRequest.Builder()
8323                                 .setForceOverride(true)
8324                                 .addSchemas(schema)
8325                                 .build())
8326                 .get();
8327 
8328         GenericDocument doc1 =
8329                 new GenericDocument.Builder<>("namespace", "id1", "Schema")
8330                         .setPropertyString("prop1", "Hello, world!")
8331                         .build();
8332         GenericDocument doc2 =
8333                 new GenericDocument.Builder<>("namespace", "id2", "Schema")
8334                         .setPropertyString("prop2", "Hello, world!")
8335                         .build();
8336         GenericDocument doc3 =
8337                 new GenericDocument.Builder<>("namespace", "id3", "Schema")
8338                         .setPropertyString("prop1", "Hello, world!")
8339                         .setPropertyString("prop2", "Hello, world!")
8340                         .build();
8341         mDb1.putAsync(
8342                         new PutDocumentsRequest.Builder()
8343                                 .addGenericDocuments(doc1, doc2, doc3)
8344                                 .build())
8345                 .get();
8346 
8347         SearchSpec searchSpec =
8348                 new SearchSpec.Builder()
8349                         .setListFilterQueryLanguageEnabled(true)
8350                         .setListFilterHasPropertyFunctionEnabled(true)
8351                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
8352                         .build();
8353         SearchResultsShim searchResults = mDb1.search("hasProperty(\"prop1\")", searchSpec);
8354         List<SearchResult> page = searchResults.getNextPageAsync().get();
8355         assertThat(page).hasSize(2);
8356         assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id3");
8357         assertThat(page.get(1).getGenericDocument().getId()).isEqualTo("id1");
8358 
8359         searchResults = mDb1.search("hasProperty(\"prop2\")", searchSpec);
8360         page = searchResults.getNextPageAsync().get();
8361         assertThat(page).hasSize(2);
8362         assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id3");
8363         assertThat(page.get(1).getGenericDocument().getId()).isEqualTo("id2");
8364 
8365         searchResults =
8366                 mDb1.search("hasProperty(\"prop1\") AND hasProperty(\"prop2\")", searchSpec);
8367         page = searchResults.getNextPageAsync().get();
8368         assertThat(page).hasSize(1);
8369         assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id3");
8370     }
8371 
8372     @Test
testQuery_hasPropertyFunctionWithoutEnablingFeatureFails()8373     public void testQuery_hasPropertyFunctionWithoutEnablingFeatureFails() throws Exception {
8374         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_QUERY_LANGUAGE));
8375         assumeTrue(
8376                 mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_HAS_PROPERTY_FUNCTION));
8377         AppSearchSchema schema =
8378                 new AppSearchSchema.Builder("Schema")
8379                         .addProperty(
8380                                 new StringPropertyConfig.Builder("prop")
8381                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
8382                                         .setIndexingType(
8383                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
8384                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
8385                                         .build())
8386                         .build();
8387         mDb1.setSchemaAsync(
8388                         new SetSchemaRequest.Builder()
8389                                 .setForceOverride(true)
8390                                 .addSchemas(schema)
8391                                 .build())
8392                 .get();
8393 
8394         GenericDocument doc =
8395                 new GenericDocument.Builder<>("namespace1", "id1", "Schema")
8396                         .setPropertyString("prop", "Hello, world!")
8397                         .build();
8398         mDb1.putAsync(new PutDocumentsRequest.Builder().addGenericDocuments(doc).build()).get();
8399 
8400         // Enable LIST_FILTER_HAS_PROPERTY_FUNCTION but disable LIST_FILTER_QUERY_LANGUAGE in the
8401         // SearchSpec.
8402         SearchSpec searchSpec =
8403                 new SearchSpec.Builder()
8404                         .setListFilterQueryLanguageEnabled(false)
8405                         .setListFilterHasPropertyFunctionEnabled(true)
8406                         .build();
8407         SearchResultsShim searchResults = mDb1.search("hasProperty(\"prop\")", searchSpec);
8408         ExecutionException executionException =
8409                 assertThrows(
8410                         ExecutionException.class, () -> searchResults.getNextPageAsync().get());
8411         assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
8412         AppSearchException exception = (AppSearchException) executionException.getCause();
8413         assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT);
8414         assertThat(exception).hasMessageThat().contains("Attempted use of unenabled feature");
8415         assertThat(exception).hasMessageThat().contains(Features.LIST_FILTER_QUERY_LANGUAGE);
8416 
8417         // Disable LIST_FILTER_HAS_PROPERTY_FUNCTION in the SearchSpec.
8418         searchSpec =
8419                 new SearchSpec.Builder()
8420                         .setListFilterQueryLanguageEnabled(true)
8421                         .setListFilterHasPropertyFunctionEnabled(false)
8422                         .build();
8423         SearchResultsShim searchResults2 = mDb1.search("hasProperty(\"prop\")", searchSpec);
8424         executionException =
8425                 assertThrows(
8426                         ExecutionException.class, () -> searchResults2.getNextPageAsync().get());
8427         assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
8428         exception = (AppSearchException) executionException.getCause();
8429         assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT);
8430         assertThat(exception).hasMessageThat().contains("Attempted use of unenabled feature");
8431         assertThat(exception).hasMessageThat().contains("HAS_PROPERTY_FUNCTION");
8432     }
8433 
8434     @Test
8435     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_LIST_FILTER_MATCH_SCORE_EXPRESSION_FUNCTION)
testQuery_matchScoreExpression()8436     public void testQuery_matchScoreExpression() throws Exception {
8437         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_QUERY_LANGUAGE));
8438         assumeTrue(
8439                 mDb1.getFeatures()
8440                         .isFeatureSupported(Features.LIST_FILTER_MATCH_SCORE_EXPRESSION_FUNCTION));
8441         AppSearchSchema schema =
8442                 new AppSearchSchema.Builder("Schema")
8443                         .addProperty(
8444                                 new StringPropertyConfig.Builder("prop")
8445                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
8446                                         .setIndexingType(
8447                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
8448                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
8449                                         .build())
8450                         .build();
8451         mDb1.setSchemaAsync(
8452                         new SetSchemaRequest.Builder()
8453                                 .setForceOverride(true)
8454                                 .addSchemas(schema)
8455                                 .build())
8456                 .get();
8457 
8458         // Put documents with document scores 3, 4, and 5.
8459         GenericDocument doc1 =
8460                 new GenericDocument.Builder<>("namespace", "id1", "Schema")
8461                         .setPropertyString("prop", "Hello, world!")
8462                         .setScore(3)
8463                         .build();
8464         GenericDocument doc2 =
8465                 new GenericDocument.Builder<>("namespace", "id2", "Schema")
8466                         .setPropertyString("prop", "Hello, world!")
8467                         .setScore(4)
8468                         .build();
8469         GenericDocument doc3 =
8470                 new GenericDocument.Builder<>("namespace", "id3", "Schema")
8471                         .setPropertyString("prop", "Hello, world!")
8472                         .setScore(5)
8473                         .build();
8474         mDb1.putAsync(
8475                         new PutDocumentsRequest.Builder()
8476                                 .addGenericDocuments(doc1, doc2, doc3)
8477                                 .build())
8478                 .get();
8479 
8480         // Get documents of scores in [3, 4], which should return doc1 and doc2.
8481         SearchSpec searchSpec =
8482                 new SearchSpec.Builder()
8483                         .setListFilterQueryLanguageEnabled(true)
8484                         .setListFilterMatchScoreExpressionFunctionEnabled(true)
8485                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
8486                         .build();
8487         SearchResultsShim searchResults =
8488                 mDb1.search("matchScoreExpression(\"this.documentScore()\", 3, 4)", searchSpec);
8489         List<SearchResult> page = searchResults.getNextPageAsync().get();
8490         assertThat(page).hasSize(2);
8491         assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id2");
8492         assertThat(page.get(1).getGenericDocument().getId()).isEqualTo("id1");
8493 
8494         // Get documents of scores in [3, 5], which should return all documents.
8495         searchResults =
8496                 mDb1.search("matchScoreExpression(\"this.documentScore()\", 3, 5)", searchSpec);
8497         page = searchResults.getNextPageAsync().get();
8498         assertThat(page).hasSize(3);
8499         assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id3");
8500         assertThat(page.get(1).getGenericDocument().getId()).isEqualTo("id2");
8501         assertThat(page.get(2).getGenericDocument().getId()).isEqualTo("id1");
8502     }
8503 
8504     @Test
8505     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_LIST_FILTER_MATCH_SCORE_EXPRESSION_FUNCTION)
testQuery_listFilterQueryMatchScoreExpressionFunction_notSupported()8506     public void testQuery_listFilterQueryMatchScoreExpressionFunction_notSupported()
8507             throws Exception {
8508         assumeFalse(
8509                 mDb1.getFeatures()
8510                         .isFeatureSupported(Features.LIST_FILTER_MATCH_SCORE_EXPRESSION_FUNCTION));
8511 
8512         // UnsupportedOperationException will be thrown with these queries so no need to
8513         // define a schema and index document.
8514         SearchSpec.Builder builder = new SearchSpec.Builder();
8515         SearchSpec searchSpec =
8516                 builder.setListFilterMatchScoreExpressionFunctionEnabled(true).build();
8517 
8518         UnsupportedOperationException exception =
8519                 assertThrows(
8520                         UnsupportedOperationException.class,
8521                         () -> mDb1.search("\"Hello, world!\"", searchSpec));
8522         assertThat(exception)
8523                 .hasMessageThat()
8524                 .contains(
8525                         Features.LIST_FILTER_MATCH_SCORE_EXPRESSION_FUNCTION
8526                                 + " is not available on this AppSearch implementation.");
8527     }
8528 
8529     @Test
8530     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_LIST_FILTER_MATCH_SCORE_EXPRESSION_FUNCTION)
testQuery_matchScoreExpressionFunctionWithoutEnablingFeatureFails()8531     public void testQuery_matchScoreExpressionFunctionWithoutEnablingFeatureFails()
8532             throws Exception {
8533         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_QUERY_LANGUAGE));
8534         assumeTrue(
8535                 mDb1.getFeatures()
8536                         .isFeatureSupported(Features.LIST_FILTER_MATCH_SCORE_EXPRESSION_FUNCTION));
8537         AppSearchSchema schema =
8538                 new AppSearchSchema.Builder("Schema")
8539                         .addProperty(
8540                                 new StringPropertyConfig.Builder("prop")
8541                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
8542                                         .setIndexingType(
8543                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
8544                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
8545                                         .build())
8546                         .build();
8547         mDb1.setSchemaAsync(
8548                         new SetSchemaRequest.Builder()
8549                                 .setForceOverride(true)
8550                                 .addSchemas(schema)
8551                                 .build())
8552                 .get();
8553 
8554         GenericDocument doc =
8555                 new GenericDocument.Builder<>("namespace", "id", "Schema")
8556                         .setPropertyString("prop", "Hello, world!")
8557                         .build();
8558         mDb1.putAsync(new PutDocumentsRequest.Builder().addGenericDocuments(doc).build()).get();
8559 
8560         // Enable LIST_FILTER_MATCH_SCORE_EXPRESSION_FUNCTION but disable
8561         // LIST_FILTER_QUERY_LANGUAGE in the SearchSpec.
8562         SearchSpec searchSpec =
8563                 new SearchSpec.Builder()
8564                         .setListFilterQueryLanguageEnabled(false)
8565                         .setListFilterMatchScoreExpressionFunctionEnabled(true)
8566                         .build();
8567         SearchResultsShim searchResults =
8568                 mDb1.search("matchScoreExpression(\"this.documentScore()\", 3, 4)", searchSpec);
8569         ExecutionException executionException =
8570                 assertThrows(
8571                         ExecutionException.class, () -> searchResults.getNextPageAsync().get());
8572         assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
8573         AppSearchException exception = (AppSearchException) executionException.getCause();
8574         assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT);
8575         assertThat(exception).hasMessageThat().contains("Attempted use of unenabled feature");
8576         assertThat(exception).hasMessageThat().contains(Features.LIST_FILTER_QUERY_LANGUAGE);
8577 
8578         // Disable LIST_FILTER_MATCH_SCORE_EXPRESSION_FUNCTION in the SearchSpec.
8579         searchSpec =
8580                 new SearchSpec.Builder()
8581                         .setListFilterQueryLanguageEnabled(true)
8582                         .setListFilterMatchScoreExpressionFunctionEnabled(false)
8583                         .build();
8584         SearchResultsShim searchResults2 =
8585                 mDb1.search("matchScoreExpression(\"this.documentScore()\", 3, 4)", searchSpec);
8586         executionException =
8587                 assertThrows(
8588                         ExecutionException.class, () -> searchResults2.getNextPageAsync().get());
8589         assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
8590         exception = (AppSearchException) executionException.getCause();
8591         assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT);
8592         assertThat(exception).hasMessageThat().contains("Attempted use of unenabled feature");
8593         assertThat(exception).hasMessageThat().contains("MATCH_SCORE_EXPRESSION");
8594     }
8595 
8596     @Test
testQuery_propertyWeightsNotSupported()8597     public void testQuery_propertyWeightsNotSupported() throws Exception {
8598         assumeFalse(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SPEC_PROPERTY_WEIGHTS));
8599 
8600         // Schema registration
8601         mDb1.setSchemaAsync(
8602                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
8603                 .get();
8604 
8605         // Index two documents
8606         AppSearchEmail email1 =
8607                 new AppSearchEmail.Builder("namespace", "id1")
8608                         .setCreationTimestampMillis(1000)
8609                         .setSubject("foo")
8610                         .build();
8611         AppSearchEmail email2 =
8612                 new AppSearchEmail.Builder("namespace", "id2")
8613                         .setCreationTimestampMillis(1000)
8614                         .setBody("foo")
8615                         .build();
8616         checkIsBatchResultSuccess(
8617                 mDb1.putAsync(
8618                         new PutDocumentsRequest.Builder()
8619                                 .addGenericDocuments(email1, email2)
8620                                 .build()));
8621 
8622         SearchSpec searchSpec =
8623                 new SearchSpec.Builder()
8624                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
8625                         .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE)
8626                         .setOrder(SearchSpec.ORDER_DESCENDING)
8627                         .setPropertyWeights(
8628                                 AppSearchEmail.SCHEMA_TYPE,
8629                                 ImmutableMap.of("subject", 2.0, "body", 0.5))
8630                         .build();
8631         UnsupportedOperationException exception =
8632                 assertThrows(
8633                         UnsupportedOperationException.class,
8634                         () -> mDb1.search("Hello", searchSpec));
8635         assertThat(exception).hasMessageThat().contains("Property weights are not supported");
8636     }
8637 
8638     @Test
testQuery_propertyWeights()8639     public void testQuery_propertyWeights() throws Exception {
8640         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SPEC_PROPERTY_WEIGHTS));
8641 
8642         // Schema registration
8643         mDb1.setSchemaAsync(
8644                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
8645                 .get();
8646 
8647         // Index two documents
8648         AppSearchEmail email1 =
8649                 new AppSearchEmail.Builder("namespace", "id1")
8650                         .setCreationTimestampMillis(1000)
8651                         .setSubject("foo")
8652                         .build();
8653         AppSearchEmail email2 =
8654                 new AppSearchEmail.Builder("namespace", "id2")
8655                         .setCreationTimestampMillis(1000)
8656                         .setBody("foo")
8657                         .build();
8658         checkIsBatchResultSuccess(
8659                 mDb1.putAsync(
8660                         new PutDocumentsRequest.Builder()
8661                                 .addGenericDocuments(email1, email2)
8662                                 .build()));
8663 
8664         // Query for "foo". It should match both emails.
8665         SearchResultsShim searchResults =
8666                 mDb1.search(
8667                         "foo",
8668                         new SearchSpec.Builder()
8669                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
8670                                 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE)
8671                                 .setOrder(SearchSpec.ORDER_DESCENDING)
8672                                 .setPropertyWeights(
8673                                         AppSearchEmail.SCHEMA_TYPE,
8674                                         ImmutableMap.of("subject", 2.0, "body", 0.5))
8675                                 .build());
8676         List<SearchResult> results = retrieveAllSearchResults(searchResults);
8677 
8678         // email1 should be ranked higher because "foo" appears in the "subject" property which
8679         // has higher weight than the "body" property.
8680         assertThat(results).hasSize(2);
8681         assertThat(results.get(0).getRankingSignal()).isGreaterThan(0);
8682         assertThat(results.get(0).getRankingSignal())
8683                 .isGreaterThan(results.get(1).getRankingSignal());
8684         assertThat(results.get(0).getGenericDocument()).isEqualTo(email1);
8685         assertThat(results.get(1).getGenericDocument()).isEqualTo(email2);
8686 
8687         // Query for "foo" without property weights.
8688         SearchSpec searchSpecWithoutWeights =
8689                 new SearchSpec.Builder()
8690                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
8691                         .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE)
8692                         .setOrder(SearchSpec.ORDER_DESCENDING)
8693                         .build();
8694         SearchResultsShim searchResultsWithoutWeights =
8695                 mDb1.search("foo", searchSpecWithoutWeights);
8696         List<SearchResult> resultsWithoutWeights =
8697                 retrieveAllSearchResults(searchResultsWithoutWeights);
8698 
8699         // email1 should have the same ranking signal as email2 as each contains the term "foo"
8700         // once.
8701         assertThat(resultsWithoutWeights).hasSize(2);
8702         assertThat(resultsWithoutWeights.get(0).getRankingSignal()).isGreaterThan(0);
8703         assertThat(resultsWithoutWeights.get(0).getRankingSignal())
8704                 .isEqualTo(resultsWithoutWeights.get(1).getRankingSignal());
8705     }
8706 
8707     @Test
testQuery_propertyWeightsNestedProperties()8708     public void testQuery_propertyWeightsNestedProperties() throws Exception {
8709         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SPEC_PROPERTY_WEIGHTS));
8710 
8711         // Register a schema with a nested type
8712         AppSearchSchema schema =
8713                 new AppSearchSchema.Builder("TypeA")
8714                         .addProperty(
8715                                 new AppSearchSchema.DocumentPropertyConfig.Builder(
8716                                                 "nestedEmail", AppSearchEmail.SCHEMA_TYPE)
8717                                         .setShouldIndexNestedProperties(true)
8718                                         .build())
8719                         .build();
8720         mDb1.setSchemaAsync(
8721                         new SetSchemaRequest.Builder()
8722                                 .addSchemas(AppSearchEmail.SCHEMA, schema)
8723                                 .build())
8724                 .get();
8725 
8726         // Index two documents
8727         AppSearchEmail nestedEmail1 =
8728                 new AppSearchEmail.Builder("namespace", "id1")
8729                         .setCreationTimestampMillis(1000)
8730                         .setSubject("foo")
8731                         .build();
8732         GenericDocument doc1 =
8733                 new GenericDocument.Builder<>("namespace", "id1", "TypeA")
8734                         .setPropertyDocument("nestedEmail", nestedEmail1)
8735                         .build();
8736         AppSearchEmail nestedEmail2 =
8737                 new AppSearchEmail.Builder("namespace", "id2")
8738                         .setCreationTimestampMillis(1000)
8739                         .setBody("foo")
8740                         .build();
8741         GenericDocument doc2 =
8742                 new GenericDocument.Builder<>("namespace", "id2", "TypeA")
8743                         .setPropertyDocument("nestedEmail", nestedEmail2)
8744                         .build();
8745         checkIsBatchResultSuccess(
8746                 mDb1.putAsync(
8747                         new PutDocumentsRequest.Builder().addGenericDocuments(doc1, doc2).build()));
8748 
8749         // Query for "foo". It should match both emails.
8750         SearchResultsShim searchResults =
8751                 mDb1.search(
8752                         "foo",
8753                         new SearchSpec.Builder()
8754                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
8755                                 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE)
8756                                 .setOrder(SearchSpec.ORDER_DESCENDING)
8757                                 .setPropertyWeights(
8758                                         "TypeA",
8759                                         ImmutableMap.of(
8760                                                 "nestedEmail.subject",
8761                                                 2.0,
8762                                                 "nestedEmail.body",
8763                                                 0.5))
8764                                 .build());
8765         List<SearchResult> results = retrieveAllSearchResults(searchResults);
8766 
8767         // email1 should be ranked higher because "foo" appears in the "nestedEmail.subject"
8768         // property which has higher weight than the "nestedEmail.body" property.
8769         assertThat(results).hasSize(2);
8770         assertThat(results.get(0).getRankingSignal()).isGreaterThan(0);
8771         assertThat(results.get(0).getRankingSignal())
8772                 .isGreaterThan(results.get(1).getRankingSignal());
8773         assertThat(results.get(0).getGenericDocument()).isEqualTo(doc1);
8774         assertThat(results.get(1).getGenericDocument()).isEqualTo(doc2);
8775 
8776         // Query for "foo" without property weights.
8777         SearchSpec searchSpecWithoutWeights =
8778                 new SearchSpec.Builder()
8779                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
8780                         .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE)
8781                         .setOrder(SearchSpec.ORDER_DESCENDING)
8782                         .build();
8783         SearchResultsShim searchResultsWithoutWeights =
8784                 mDb1.search("foo", searchSpecWithoutWeights);
8785         List<SearchResult> resultsWithoutWeights =
8786                 retrieveAllSearchResults(searchResultsWithoutWeights);
8787 
8788         // email1 should have the same ranking signal as email2 as each contains the term "foo"
8789         // once.
8790         assertThat(resultsWithoutWeights).hasSize(2);
8791         assertThat(resultsWithoutWeights.get(0).getRankingSignal()).isGreaterThan(0);
8792         assertThat(resultsWithoutWeights.get(0).getRankingSignal())
8793                 .isEqualTo(resultsWithoutWeights.get(1).getRankingSignal());
8794     }
8795 
8796     @Test
testQuery_propertyWeightsDefaults()8797     public void testQuery_propertyWeightsDefaults() throws Exception {
8798         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SPEC_PROPERTY_WEIGHTS));
8799 
8800         // Schema registration
8801         mDb1.setSchemaAsync(
8802                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
8803                 .get();
8804 
8805         // Index two documents
8806         AppSearchEmail email1 =
8807                 new AppSearchEmail.Builder("namespace", "id1")
8808                         .setCreationTimestampMillis(1000)
8809                         .setSubject("foo")
8810                         .build();
8811         AppSearchEmail email2 =
8812                 new AppSearchEmail.Builder("namespace", "id2")
8813                         .setCreationTimestampMillis(1000)
8814                         .setBody("foo bar")
8815                         .build();
8816         checkIsBatchResultSuccess(
8817                 mDb1.putAsync(
8818                         new PutDocumentsRequest.Builder()
8819                                 .addGenericDocuments(email1, email2)
8820                                 .build()));
8821 
8822         // Query for "foo" without assigning property weights for any path.
8823         SearchResultsShim searchResults =
8824                 mDb1.search(
8825                         "foo",
8826                         new SearchSpec.Builder()
8827                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
8828                                 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE)
8829                                 .setOrder(SearchSpec.ORDER_DESCENDING)
8830                                 .setPropertyWeights(AppSearchEmail.SCHEMA_TYPE, ImmutableMap.of())
8831                                 .build());
8832         List<SearchResult> resultsWithoutPropertyWeights = retrieveAllSearchResults(searchResults);
8833 
8834         // Query for "foo" with assigning default property weights.
8835         searchResults =
8836                 mDb1.search(
8837                         "foo",
8838                         new SearchSpec.Builder()
8839                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
8840                                 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE)
8841                                 .setOrder(SearchSpec.ORDER_DESCENDING)
8842                                 .setPropertyWeights(
8843                                         AppSearchEmail.SCHEMA_TYPE,
8844                                         ImmutableMap.of("subject", 1.0, "body", 1.0))
8845                                 .build());
8846         List<SearchResult> expectedResults = retrieveAllSearchResults(searchResults);
8847 
8848         assertThat(resultsWithoutPropertyWeights).hasSize(2);
8849         assertThat(expectedResults).hasSize(2);
8850 
8851         assertThat(resultsWithoutPropertyWeights.get(0).getGenericDocument()).isEqualTo(email1);
8852         assertThat(resultsWithoutPropertyWeights.get(1).getGenericDocument()).isEqualTo(email2);
8853         assertThat(expectedResults.get(0).getGenericDocument()).isEqualTo(email1);
8854         assertThat(expectedResults.get(1).getGenericDocument()).isEqualTo(email2);
8855 
8856         // The ranking signal for results with no property path and weights set should be equal
8857         // to the ranking signal for results with explicitly set default weights.
8858         assertThat(resultsWithoutPropertyWeights.get(0).getRankingSignal())
8859                 .isEqualTo(expectedResults.get(0).getRankingSignal());
8860         assertThat(resultsWithoutPropertyWeights.get(1).getRankingSignal())
8861                 .isEqualTo(expectedResults.get(1).getRankingSignal());
8862     }
8863 
8864     @Test
testQuery_propertyWeightsIgnoresInvalidPropertyPaths()8865     public void testQuery_propertyWeightsIgnoresInvalidPropertyPaths() throws Exception {
8866         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SPEC_PROPERTY_WEIGHTS));
8867 
8868         // Schema registration
8869         mDb1.setSchemaAsync(
8870                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
8871                 .get();
8872 
8873         // Index an email
8874         AppSearchEmail email1 =
8875                 new AppSearchEmail.Builder("namespace", "id1")
8876                         .setCreationTimestampMillis(1000)
8877                         .setSubject("baz")
8878                         .build();
8879         checkIsBatchResultSuccess(
8880                 mDb1.putAsync(
8881                         new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
8882 
8883         // Query for "baz" with property weight for "subject", a valid property in the schema type.
8884         SearchResultsShim searchResults =
8885                 mDb1.search(
8886                         "baz",
8887                         new SearchSpec.Builder()
8888                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
8889                                 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE)
8890                                 .setOrder(SearchSpec.ORDER_DESCENDING)
8891                                 .setPropertyWeights(
8892                                         AppSearchEmail.SCHEMA_TYPE, ImmutableMap.of("subject", 2.0))
8893                                 .build());
8894         List<SearchResult> results = retrieveAllSearchResults(searchResults);
8895 
8896         // Query for "baz" with property weights, one for valid property "subject" and one for a
8897         // non-existing property "invalid".
8898         searchResults =
8899                 mDb1.search(
8900                         "baz",
8901                         new SearchSpec.Builder()
8902                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
8903                                 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE)
8904                                 .setOrder(SearchSpec.ORDER_DESCENDING)
8905                                 .setPropertyWeights(
8906                                         AppSearchEmail.SCHEMA_TYPE,
8907                                         ImmutableMap.of("subject", 2.0, "invalid", 3.0))
8908                                 .build());
8909         List<SearchResult> resultsWithInvalidPath = retrieveAllSearchResults(searchResults);
8910 
8911         assertThat(results).hasSize(1);
8912         assertThat(resultsWithInvalidPath).hasSize(1);
8913 
8914         // We expect the ranking signal to be unchanged in the presence of an invalid property
8915         // weight.
8916         assertThat(results.get(0).getRankingSignal()).isGreaterThan(0);
8917         assertThat(resultsWithInvalidPath.get(0).getRankingSignal())
8918                 .isEqualTo(results.get(0).getRankingSignal());
8919 
8920         assertThat(results.get(0).getGenericDocument()).isEqualTo(email1);
8921         assertThat(resultsWithInvalidPath.get(0).getGenericDocument()).isEqualTo(email1);
8922     }
8923 
8924     @Test
testQueryWithJoin_typePropertyFiltersOnNestedSpec()8925     public void testQueryWithJoin_typePropertyFiltersOnNestedSpec() throws Exception {
8926         assumeTrue(
8927                 mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
8928         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
8929 
8930         // A full example of how join might be used with property filters in join spec
8931         AppSearchSchema actionSchema =
8932                 new AppSearchSchema.Builder("ViewAction")
8933                         .addProperty(
8934                                 new StringPropertyConfig.Builder("entityId")
8935                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
8936                                         .setIndexingType(
8937                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
8938                                         .setJoinableValueType(
8939                                                 StringPropertyConfig
8940                                                         .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
8941                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
8942                                         .build())
8943                         .addProperty(
8944                                 new StringPropertyConfig.Builder("note")
8945                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
8946                                         .setIndexingType(
8947                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
8948                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
8949                                         .build())
8950                         .addProperty(
8951                                 new StringPropertyConfig.Builder("viewType")
8952                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
8953                                         .setIndexingType(
8954                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
8955                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
8956                                         .build())
8957                         .build();
8958 
8959         // Schema registration
8960         mDb1.setSchemaAsync(
8961                         new SetSchemaRequest.Builder()
8962                                 .addSchemas(AppSearchEmail.SCHEMA, actionSchema)
8963                                 .build())
8964                 .get();
8965 
8966         // Index 2 email documents
8967         AppSearchEmail inEmail =
8968                 new AppSearchEmail.Builder("namespace", "id1")
8969                         .setFrom("[email protected]")
8970                         .setTo("[email protected]", "[email protected]")
8971                         .setSubject("testPut example")
8972                         .setBody("This is the body of the testPut email")
8973                         .build();
8974 
8975         AppSearchEmail inEmail2 =
8976                 new AppSearchEmail.Builder("namespace", "id2")
8977                         .setFrom("[email protected]")
8978                         .setTo("[email protected]", "[email protected]")
8979                         .setSubject("testPut example")
8980                         .setBody("This is the body of the testPut email")
8981                         .build();
8982 
8983         // Index 2 viewAction documents, one for email1 and the other for email2
8984         String qualifiedId1 =
8985                 DocumentIdUtil.createQualifiedId(
8986                         ApplicationProvider.getApplicationContext().getPackageName(),
8987                         DB_NAME_1,
8988                         "namespace",
8989                         "id1");
8990         String qualifiedId2 =
8991                 DocumentIdUtil.createQualifiedId(
8992                         ApplicationProvider.getApplicationContext().getPackageName(),
8993                         DB_NAME_1,
8994                         "namespace",
8995                         "id2");
8996         GenericDocument viewAction1 =
8997                 new GenericDocument.Builder<>("NS", "id3", "ViewAction")
8998                         .setPropertyString("entityId", qualifiedId1)
8999                         .setPropertyString("note", "Viewed email on Monday")
9000                         .setPropertyString("viewType", "Stared")
9001                         .build();
9002         GenericDocument viewAction2 =
9003                 new GenericDocument.Builder<>("NS", "id4", "ViewAction")
9004                         .setPropertyString("entityId", qualifiedId2)
9005                         .setPropertyString("note", "Viewed email on Tuesday")
9006                         .setPropertyString("viewType", "Viewed")
9007                         .build();
9008         checkIsBatchResultSuccess(
9009                 mDb1.putAsync(
9010                         new PutDocumentsRequest.Builder()
9011                                 .addGenericDocuments(inEmail, inEmail2, viewAction1, viewAction2)
9012                                 .build()));
9013 
9014         // The nested search spec only allows searching the viewType property for viewAction
9015         // schema type. It also specifies a property filter for Email schema.
9016         SearchSpec nestedSearchSpec =
9017                 new SearchSpec.Builder()
9018                         .addFilterProperties("ViewAction", ImmutableList.of("viewType"))
9019                         .addFilterProperties(
9020                                 AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("subject"))
9021                         .build();
9022 
9023         // Search for the term "Viewed" in join spec
9024         JoinSpec js =
9025                 new JoinSpec.Builder("entityId")
9026                         .setNestedSearch("Viewed", nestedSearchSpec)
9027                         .setAggregationScoringStrategy(JoinSpec.AGGREGATION_SCORING_RESULT_COUNT)
9028                         .build();
9029 
9030         SearchResultsShim searchResults =
9031                 mDb1.search(
9032                         "body email",
9033                         new SearchSpec.Builder()
9034                                 .setRankingStrategy(
9035                                         SearchSpec.RANKING_STRATEGY_JOIN_AGGREGATE_SCORE)
9036                                 .setJoinSpec(js)
9037                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
9038                                 .build());
9039 
9040         List<SearchResult> sr = searchResults.getNextPageAsync().get();
9041 
9042         // Both email docs are returned, email2 comes first because it has higher number of
9043         // joined documents. The property filters for Email schema specified in the nested search
9044         // specs don't apply to the outer query (otherwise none of the email documents would have
9045         // been returned).
9046         assertThat(sr).hasSize(2);
9047 
9048         // Email2 has a viewAction document viewAction2 that satisfies the property filters in
9049         // the join spec, so it should be present in the joined results.
9050         assertThat(sr.get(0).getGenericDocument().getId()).isEqualTo("id2");
9051         assertThat(sr.get(0).getRankingSignal()).isEqualTo(1.0);
9052         assertThat(sr.get(0).getJoinedResults()).hasSize(1);
9053         assertThat(sr.get(0).getJoinedResults().get(0).getGenericDocument()).isEqualTo(viewAction2);
9054 
9055         // Email1 has a viewAction document viewAction1 but it doesn't satisfy the property filters
9056         // in the join spec, so it should not be present in the joined results.
9057         assertThat(sr.get(1).getGenericDocument().getId()).isEqualTo("id1");
9058         assertThat(sr.get(1).getRankingSignal()).isEqualTo(0.0);
9059         assertThat(sr.get(1).getJoinedResults()).isEmpty();
9060     }
9061 
9062     @Test
testQueryWithJoin_typePropertyFiltersOnOuterSpec()9063     public void testQueryWithJoin_typePropertyFiltersOnOuterSpec() throws Exception {
9064         assumeTrue(
9065                 mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
9066         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
9067 
9068         // A full example of how join might be used with property filters in join spec
9069         AppSearchSchema actionSchema =
9070                 new AppSearchSchema.Builder("ViewAction")
9071                         .addProperty(
9072                                 new StringPropertyConfig.Builder("entityId")
9073                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
9074                                         .setIndexingType(
9075                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
9076                                         .setJoinableValueType(
9077                                                 StringPropertyConfig
9078                                                         .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
9079                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
9080                                         .build())
9081                         .addProperty(
9082                                 new StringPropertyConfig.Builder("note")
9083                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
9084                                         .setIndexingType(
9085                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
9086                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
9087                                         .build())
9088                         .addProperty(
9089                                 new StringPropertyConfig.Builder("viewType")
9090                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
9091                                         .setIndexingType(
9092                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
9093                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
9094                                         .build())
9095                         .build();
9096 
9097         // Schema registration
9098         mDb1.setSchemaAsync(
9099                         new SetSchemaRequest.Builder()
9100                                 .addSchemas(AppSearchEmail.SCHEMA, actionSchema)
9101                                 .build())
9102                 .get();
9103 
9104         // Index 2 email documents
9105         AppSearchEmail inEmail =
9106                 new AppSearchEmail.Builder("namespace", "id1")
9107                         .setFrom("[email protected]")
9108                         .setTo("[email protected]", "[email protected]")
9109                         .setSubject("testPut example")
9110                         .setBody("This is the body of the testPut email")
9111                         .build();
9112 
9113         AppSearchEmail inEmail2 =
9114                 new AppSearchEmail.Builder("namespace", "id2")
9115                         .setFrom("[email protected]")
9116                         .setTo("[email protected]", "[email protected]")
9117                         .setSubject("testPut example")
9118                         .setBody("This is the body of the testPut email")
9119                         .build();
9120 
9121         // Index 2 viewAction documents, one for email1 and the other for email2
9122         String qualifiedId1 =
9123                 DocumentIdUtil.createQualifiedId(
9124                         ApplicationProvider.getApplicationContext().getPackageName(),
9125                         DB_NAME_1,
9126                         "namespace",
9127                         "id1");
9128         String qualifiedId2 =
9129                 DocumentIdUtil.createQualifiedId(
9130                         ApplicationProvider.getApplicationContext().getPackageName(),
9131                         DB_NAME_1,
9132                         "namespace",
9133                         "id2");
9134         GenericDocument viewAction1 =
9135                 new GenericDocument.Builder<>("NS", "id3", "ViewAction")
9136                         .setPropertyString("entityId", qualifiedId1)
9137                         .setPropertyString("note", "Viewed email on Monday")
9138                         .setPropertyString("viewType", "Stared")
9139                         .build();
9140         GenericDocument viewAction2 =
9141                 new GenericDocument.Builder<>("NS", "id4", "ViewAction")
9142                         .setPropertyString("entityId", qualifiedId2)
9143                         .setPropertyString("note", "Viewed email on Tuesday")
9144                         .setPropertyString("viewType", "Viewed")
9145                         .build();
9146         checkIsBatchResultSuccess(
9147                 mDb1.putAsync(
9148                         new PutDocumentsRequest.Builder()
9149                                 .addGenericDocuments(inEmail, inEmail2, viewAction1, viewAction2)
9150                                 .build()));
9151 
9152         // The nested search spec doesn't specify any property filters.
9153         SearchSpec nestedSearchSpec = new SearchSpec.Builder().build();
9154 
9155         // Search for the term "Viewed" in join spec
9156         JoinSpec js =
9157                 new JoinSpec.Builder("entityId")
9158                         .setNestedSearch("Viewed", nestedSearchSpec)
9159                         .setAggregationScoringStrategy(JoinSpec.AGGREGATION_SCORING_RESULT_COUNT)
9160                         .build();
9161 
9162         // Outer search spec adds property filters for both Email and ViewAction schema
9163         SearchResultsShim searchResults =
9164                 mDb1.search(
9165                         "body email",
9166                         new SearchSpec.Builder()
9167                                 .setRankingStrategy(
9168                                         SearchSpec.RANKING_STRATEGY_JOIN_AGGREGATE_SCORE)
9169                                 .setJoinSpec(js)
9170                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
9171                                 .addFilterProperties(
9172                                         AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("body"))
9173                                 .addFilterProperties("ViewAction", ImmutableList.of("viewType"))
9174                                 .build());
9175 
9176         List<SearchResult> sr = searchResults.getNextPageAsync().get();
9177 
9178         // Both email docs are returned as they both satisfy the property filters for Email, email2
9179         // comes first because it has higher id lexicographically.
9180         assertThat(sr).hasSize(2);
9181 
9182         // Email2 has a viewAction document viewAction2 that satisfies the property filters in
9183         // the outer spec (although those property filters are irrelevant for joined documents),
9184         // it should be present in the joined results.
9185         assertThat(sr.get(0).getGenericDocument().getId()).isEqualTo("id2");
9186         assertThat(sr.get(0).getRankingSignal()).isEqualTo(1.0);
9187         assertThat(sr.get(0).getJoinedResults()).hasSize(1);
9188         assertThat(sr.get(0).getJoinedResults().get(0).getGenericDocument()).isEqualTo(viewAction2);
9189 
9190         // Email1 has a viewAction document viewAction1 that doesn't satisfy the property filters
9191         // in the outer spec, but property filters in the outer spec should not apply on joined
9192         // documents, so viewAction1 should be present in the joined results.
9193         assertThat(sr.get(1).getGenericDocument().getId()).isEqualTo("id1");
9194         assertThat(sr.get(1).getRankingSignal()).isEqualTo(1.0);
9195         assertThat(sr.get(0).getJoinedResults()).hasSize(1);
9196         assertThat(sr.get(1).getJoinedResults().get(0).getGenericDocument()).isEqualTo(viewAction1);
9197     }
9198 
9199     @Test
testQuery_typePropertyFiltersNotSupported()9200     public void testQuery_typePropertyFiltersNotSupported() throws Exception {
9201         assumeFalse(
9202                 mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
9203         // Schema registration
9204         mDb1.setSchemaAsync(
9205                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
9206                 .get();
9207 
9208         // Query with type property filters {"Email", ["subject", "to"]} and verify that unsupported
9209         // exception is thrown
9210         SearchSpec searchSpec =
9211                 new SearchSpec.Builder()
9212                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
9213                         .addFilterProperties(
9214                                 AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("subject", "to"))
9215                         .build();
9216         UnsupportedOperationException exception =
9217                 assertThrows(
9218                         UnsupportedOperationException.class, () -> mDb1.search("body", searchSpec));
9219         assertThat(exception)
9220                 .hasMessageThat()
9221                 .contains(
9222                         Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES
9223                                 + " is not available on this AppSearch implementation.");
9224     }
9225 
9226     @Test
9227     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SEARCH_RESULT_PARENT_TYPES)
testQuery_searchResultWrapsParentTypeMapForPolymorphism()9228     public void testQuery_searchResultWrapsParentTypeMapForPolymorphism() throws Exception {
9229         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
9230         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_RESULT_PARENT_TYPES));
9231 
9232         // Schema registration
9233         AppSearchSchema personSchema =
9234                 new AppSearchSchema.Builder("Person")
9235                         .addProperty(
9236                                 new StringPropertyConfig.Builder("name")
9237                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
9238                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
9239                                         .setIndexingType(
9240                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
9241                                         .build())
9242                         .build();
9243         AppSearchSchema artistSchema =
9244                 new AppSearchSchema.Builder("Artist")
9245                         .addParentType("Person")
9246                         .addProperty(
9247                                 new StringPropertyConfig.Builder("name")
9248                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
9249                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
9250                                         .setIndexingType(
9251                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
9252                                         .build())
9253                         .addProperty(
9254                                 new StringPropertyConfig.Builder("company")
9255                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
9256                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
9257                                         .setIndexingType(
9258                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
9259                                         .build())
9260                         .build();
9261         AppSearchSchema musicianSchema =
9262                 new AppSearchSchema.Builder("Musician")
9263                         .addParentType("Artist")
9264                         .addProperty(
9265                                 new StringPropertyConfig.Builder("name")
9266                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
9267                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
9268                                         .setIndexingType(
9269                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
9270                                         .build())
9271                         .addProperty(
9272                                 new StringPropertyConfig.Builder("company")
9273                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
9274                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
9275                                         .setIndexingType(
9276                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
9277                                         .build())
9278                         .build();
9279         AppSearchSchema messageSchema =
9280                 new AppSearchSchema.Builder("Message")
9281                         .addProperty(
9282                                 new AppSearchSchema.DocumentPropertyConfig.Builder(
9283                                                 "receivers", "Person")
9284                                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
9285                                         .setShouldIndexNestedProperties(true)
9286                                         .build())
9287                         .build();
9288         mDb1.setSchemaAsync(
9289                         new SetSchemaRequest.Builder()
9290                                 .addSchemas(personSchema)
9291                                 .addSchemas(artistSchema)
9292                                 .addSchemas(musicianSchema)
9293                                 .addSchemas(messageSchema)
9294                                 .build())
9295                 .get();
9296 
9297         // Index documents
9298         GenericDocument personDoc =
9299                 new GenericDocument.Builder<>("namespace", "id1", "Person")
9300                         .setPropertyString("name", "person")
9301                         .build();
9302         GenericDocument artistDoc =
9303                 new GenericDocument.Builder<>("namespace", "id2", "Artist")
9304                         .setPropertyString("name", "artist")
9305                         .setPropertyString("company", "foo")
9306                         .build();
9307         GenericDocument musicianDoc =
9308                 new GenericDocument.Builder<>("namespace", "id3", "Musician")
9309                         .setPropertyString("name", "musician")
9310                         .setPropertyString("company", "foo")
9311                         .build();
9312         GenericDocument messageDoc =
9313                 new GenericDocument.Builder<>("namespace", "id4", "Message")
9314                         .setPropertyDocument("receivers", artistDoc, musicianDoc)
9315                         .build();
9316 
9317         Map<String, List<String>> expectedPersonParentTypeMap = Collections.emptyMap();
9318         Map<String, List<String>> expectedArtistParentTypeMap =
9319                 ImmutableMap.of("Artist", ImmutableList.of("Person"));
9320         Map<String, List<String>> expectedMusicianParentTypeMap =
9321                 ImmutableMap.of("Musician", ImmutableList.of("Artist", "Person"));
9322         // artistDoc and musicianDoc are nested in messageDoc, so messageDoc's parent type map
9323         // should have the entries for both the Artist and Musician type.
9324         Map<String, List<String>> expectedMessageParentTypeMap =
9325                 ImmutableMap.of(
9326                         "Artist", ImmutableList.of("Person"),
9327                         "Musician", ImmutableList.of("Artist", "Person"));
9328 
9329         checkIsBatchResultSuccess(
9330                 mDb1.putAsync(
9331                         new PutDocumentsRequest.Builder()
9332                                 .addGenericDocuments(personDoc, artistDoc, musicianDoc, messageDoc)
9333                                 .build()));
9334 
9335         // Query to get all the documents
9336         List<SearchResult> searchResults =
9337                 retrieveAllSearchResults(
9338                         mDb1.search(
9339                                 "",
9340                                 new SearchSpec.Builder()
9341                                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
9342                                         .build()));
9343         assertThat(searchResults).hasSize(4);
9344         assertThat(searchResults.get(0).getGenericDocument().getSchemaType()).isEqualTo("Message");
9345         assertThat(searchResults.get(0).getParentTypeMap()).isEqualTo(expectedMessageParentTypeMap);
9346 
9347         assertThat(searchResults.get(1).getGenericDocument().getSchemaType()).isEqualTo("Musician");
9348         assertThat(searchResults.get(1).getParentTypeMap())
9349                 .isEqualTo(expectedMusicianParentTypeMap);
9350 
9351         assertThat(searchResults.get(2).getGenericDocument().getSchemaType()).isEqualTo("Artist");
9352         assertThat(searchResults.get(2).getParentTypeMap()).isEqualTo(expectedArtistParentTypeMap);
9353 
9354         assertThat(searchResults.get(3).getGenericDocument().getSchemaType()).isEqualTo("Person");
9355         assertThat(searchResults.get(3).getParentTypeMap()).isEqualTo(expectedPersonParentTypeMap);
9356     }
9357 
9358     @Test
testSimpleJoin()9359     public void testSimpleJoin() throws Exception {
9360         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
9361 
9362         // A full example of how join might be used
9363         AppSearchSchema actionSchema =
9364                 new AppSearchSchema.Builder("ViewAction")
9365                         .addProperty(
9366                                 new StringPropertyConfig.Builder("entityId")
9367                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
9368                                         .setIndexingType(
9369                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
9370                                         .setJoinableValueType(
9371                                                 StringPropertyConfig
9372                                                         .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
9373                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
9374                                         .build())
9375                         .addProperty(
9376                                 new StringPropertyConfig.Builder("note")
9377                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
9378                                         .setIndexingType(
9379                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
9380                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
9381                                         .build())
9382                         .build();
9383 
9384         // Schema registration
9385         mDb1.setSchemaAsync(
9386                         new SetSchemaRequest.Builder()
9387                                 .addSchemas(AppSearchEmail.SCHEMA, actionSchema)
9388                                 .build())
9389                 .get();
9390 
9391         // Index a document
9392         // While inEmail2 has a higher document score, we will rank based on the number of joined
9393         // documents. inEmail1 will have 1 joined document while inEmail2 will have 0 joined
9394         // documents.
9395         AppSearchEmail inEmail =
9396                 new AppSearchEmail.Builder("namespace", "id1")
9397                         .setFrom("[email protected]")
9398                         .setTo("[email protected]", "[email protected]")
9399                         .setSubject("testPut example")
9400                         .setBody("This is the body of the testPut email")
9401                         .setScore(1)
9402                         .build();
9403 
9404         AppSearchEmail inEmail2 =
9405                 new AppSearchEmail.Builder("namespace", "id2")
9406                         .setFrom("[email protected]")
9407                         .setTo("[email protected]", "[email protected]")
9408                         .setSubject("testPut example")
9409                         .setBody("This is the body of the testPut email")
9410                         .setScore(10)
9411                         .build();
9412 
9413         String qualifiedId =
9414                 DocumentIdUtil.createQualifiedId(
9415                         mContext.getPackageName(), DB_NAME_1, "namespace", "id1");
9416         GenericDocument viewAction1 =
9417                 new GenericDocument.Builder<>("NS", "id3", "ViewAction")
9418                         .setScore(1)
9419                         .setPropertyString("entityId", qualifiedId)
9420                         .setPropertyString("note", "Viewed email on Monday")
9421                         .build();
9422         GenericDocument viewAction2 =
9423                 new GenericDocument.Builder<>("NS", "id4", "ViewAction")
9424                         .setScore(2)
9425                         .setPropertyString("entityId", qualifiedId)
9426                         .setPropertyString("note", "Viewed email on Tuesday")
9427                         .build();
9428         checkIsBatchResultSuccess(
9429                 mDb1.putAsync(
9430                         new PutDocumentsRequest.Builder()
9431                                 .addGenericDocuments(inEmail, inEmail2, viewAction1, viewAction2)
9432                                 .build()));
9433 
9434         SearchSpec nestedSearchSpec =
9435                 new SearchSpec.Builder()
9436                         .setRankingStrategy(SearchSpec.RANKING_STRATEGY_DOCUMENT_SCORE)
9437                         .setOrder(SearchSpec.ORDER_ASCENDING)
9438                         .build();
9439 
9440         JoinSpec js =
9441                 new JoinSpec.Builder("entityId")
9442                         .setNestedSearch("", nestedSearchSpec)
9443                         .setAggregationScoringStrategy(JoinSpec.AGGREGATION_SCORING_RESULT_COUNT)
9444                         .setMaxJoinedResultCount(1)
9445                         .build();
9446 
9447         SearchResultsShim searchResults =
9448                 mDb1.search(
9449                         "body email",
9450                         new SearchSpec.Builder()
9451                                 .setRankingStrategy(
9452                                         SearchSpec.RANKING_STRATEGY_JOIN_AGGREGATE_SCORE)
9453                                 .setJoinSpec(js)
9454                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
9455                                 .build());
9456 
9457         List<SearchResult> sr = searchResults.getNextPageAsync().get();
9458 
9459         // Both email docs are returned, but id1 comes first due to the join
9460         assertThat(sr).hasSize(2);
9461 
9462         assertThat(sr.get(0).getGenericDocument().getId()).isEqualTo("id1");
9463         assertThat(sr.get(0).getJoinedResults()).hasSize(1);
9464         assertThat(sr.get(0).getJoinedResults().get(0).getGenericDocument()).isEqualTo(viewAction1);
9465         // SearchSpec.Builder#setMaxJoinedResultCount only limits the number of child documents
9466         // returned. It does not affect the number of child documents that are scored. So the score
9467         // (the COUNT of the number of children) is 2, even though only one child is returned.
9468         assertThat(sr.get(0).getRankingSignal()).isEqualTo(2.0);
9469 
9470         assertThat(sr.get(1).getGenericDocument().getId()).isEqualTo("id2");
9471         assertThat(sr.get(1).getRankingSignal()).isEqualTo(0.0);
9472         assertThat(sr.get(1).getJoinedResults()).isEmpty();
9473     }
9474 
9475     @Test
testJoin_unsupportedFeature_throwsException()9476     public void testJoin_unsupportedFeature_throwsException() throws Exception {
9477         assumeFalse(mDb1.getFeatures().isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
9478 
9479         SearchSpec nestedSearchSpec = new SearchSpec.Builder().build();
9480         JoinSpec js =
9481                 new JoinSpec.Builder("entityId").setNestedSearch("", nestedSearchSpec).build();
9482         Exception e =
9483                 assertThrows(
9484                         UnsupportedOperationException.class,
9485                         () ->
9486                                 mDb1.search(
9487                                         /*queryExpression */ "",
9488                                         new SearchSpec.Builder()
9489                                                 .setJoinSpec(js)
9490                                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
9491                                                 .build()));
9492         assertThat(e.getMessage())
9493                 .isEqualTo("JoinSpec is not available on this AppSearch " + "implementation.");
9494     }
9495 
9496     @Test
testSearchSuggestion_notSupported()9497     public void testSearchSuggestion_notSupported() throws Exception {
9498         assumeFalse(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION));
9499 
9500         assertThrows(
9501                 UnsupportedOperationException.class,
9502                 () ->
9503                         mDb1.searchSuggestionAsync(
9504                                         /* suggestionQueryExpression= */ "t",
9505                                         new SearchSuggestionSpec.Builder(
9506                                                         /* maximumResultCount= */ 2)
9507                                                 .build())
9508                                 .get());
9509     }
9510 
9511     @Test
testSearchSuggestion()9512     public void testSearchSuggestion() throws Exception {
9513         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION));
9514         // Schema registration
9515         AppSearchSchema schema =
9516                 new AppSearchSchema.Builder("Type")
9517                         .addProperty(
9518                                 new StringPropertyConfig.Builder("body")
9519                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
9520                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
9521                                         .setIndexingType(
9522                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
9523                                         .build())
9524                         .build();
9525         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
9526 
9527         // Index documents
9528         GenericDocument doc1 =
9529                 new GenericDocument.Builder<>("namespace", "id1", "Type")
9530                         .setPropertyString("body", "termOne termTwo termThree termFour")
9531                         .build();
9532         GenericDocument doc2 =
9533                 new GenericDocument.Builder<>("namespace", "id2", "Type")
9534                         .setPropertyString("body", "termOne termTwo termThree")
9535                         .build();
9536         GenericDocument doc3 =
9537                 new GenericDocument.Builder<>("namespace", "id3", "Type")
9538                         .setPropertyString("body", "termOne termTwo")
9539                         .build();
9540         GenericDocument doc4 =
9541                 new GenericDocument.Builder<>("namespace", "id4", "Type")
9542                         .setPropertyString("body", "termOne")
9543                         .build();
9544 
9545         checkIsBatchResultSuccess(
9546                 mDb1.putAsync(
9547                         new PutDocumentsRequest.Builder()
9548                                 .addGenericDocuments(doc1, doc2, doc3, doc4)
9549                                 .build()));
9550 
9551         SearchSuggestionResult resultOne =
9552                 new SearchSuggestionResult.Builder().setSuggestedResult("termone").build();
9553         SearchSuggestionResult resultTwo =
9554                 new SearchSuggestionResult.Builder().setSuggestedResult("termtwo").build();
9555         SearchSuggestionResult resultThree =
9556                 new SearchSuggestionResult.Builder().setSuggestedResult("termthree").build();
9557         SearchSuggestionResult resultFour =
9558                 new SearchSuggestionResult.Builder().setSuggestedResult("termfour").build();
9559 
9560         List<SearchSuggestionResult> suggestions =
9561                 mDb1.searchSuggestionAsync(
9562                                 /* suggestionQueryExpression= */ "t",
9563                                 new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10)
9564                                         .build())
9565                         .get();
9566         assertThat(suggestions)
9567                 .containsExactly(resultOne, resultTwo, resultThree, resultFour)
9568                 .inOrder();
9569 
9570         // Query first 2 suggestions, and they will be ranked.
9571         suggestions =
9572                 mDb1.searchSuggestionAsync(
9573                                 /* suggestionQueryExpression= */ "t",
9574                                 new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 2)
9575                                         .build())
9576                         .get();
9577         assertThat(suggestions).containsExactly(resultOne, resultTwo).inOrder();
9578     }
9579 
9580     @Test
testSearchSuggestion_namespaceFilter()9581     public void testSearchSuggestion_namespaceFilter() throws Exception {
9582         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION));
9583         // Schema registration
9584         AppSearchSchema schema =
9585                 new AppSearchSchema.Builder("Type")
9586                         .addProperty(
9587                                 new StringPropertyConfig.Builder("body")
9588                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
9589                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
9590                                         .setIndexingType(
9591                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
9592                                         .build())
9593                         .build();
9594         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
9595 
9596         // Index documents
9597         GenericDocument doc1 =
9598                 new GenericDocument.Builder<>("namespace1", "id1", "Type")
9599                         .setPropertyString("body", "fo foo")
9600                         .build();
9601         GenericDocument doc2 =
9602                 new GenericDocument.Builder<>("namespace2", "id2", "Type")
9603                         .setPropertyString("body", "foo")
9604                         .build();
9605         GenericDocument doc3 =
9606                 new GenericDocument.Builder<>("namespace3", "id3", "Type")
9607                         .setPropertyString("body", "fool")
9608                         .build();
9609 
9610         checkIsBatchResultSuccess(
9611                 mDb1.putAsync(
9612                         new PutDocumentsRequest.Builder()
9613                                 .addGenericDocuments(doc1, doc2, doc3)
9614                                 .build()));
9615 
9616         SearchSuggestionResult resultFo =
9617                 new SearchSuggestionResult.Builder().setSuggestedResult("fo").build();
9618         SearchSuggestionResult resultFoo =
9619                 new SearchSuggestionResult.Builder().setSuggestedResult("foo").build();
9620         SearchSuggestionResult resultFool =
9621                 new SearchSuggestionResult.Builder().setSuggestedResult("fool").build();
9622 
9623         // namespace1 has 2 results.
9624         List<SearchSuggestionResult> suggestions =
9625                 mDb1.searchSuggestionAsync(
9626                                 /* suggestionQueryExpression= */ "f",
9627                                 new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10)
9628                                         .addFilterNamespaces("namespace1")
9629                                         .build())
9630                         .get();
9631         assertThat(suggestions).containsExactly(resultFoo, resultFo).inOrder();
9632 
9633         // namespace2 has 1 result.
9634         suggestions =
9635                 mDb1.searchSuggestionAsync(
9636                                 /* suggestionQueryExpression= */ "f",
9637                                 new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10)
9638                                         .addFilterNamespaces("namespace2")
9639                                         .build())
9640                         .get();
9641         assertThat(suggestions).containsExactly(resultFoo).inOrder();
9642 
9643         // namespace2 and 3 has 2 results.
9644         suggestions =
9645                 mDb1.searchSuggestionAsync(
9646                                 /* suggestionQueryExpression= */ "f",
9647                                 new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10)
9648                                         .addFilterNamespaces("namespace2", "namespace3")
9649                                         .build())
9650                         .get();
9651         assertThat(suggestions).containsExactly(resultFoo, resultFool);
9652 
9653         // non exist namespace has empty result
9654         suggestions =
9655                 mDb1.searchSuggestionAsync(
9656                                 /* suggestionQueryExpression= */ "f",
9657                                 new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10)
9658                                         .addFilterNamespaces("nonExistNamespace")
9659                                         .build())
9660                         .get();
9661         assertThat(suggestions).isEmpty();
9662     }
9663 
9664     @Test
testSearchSuggestion_documentIdFilter()9665     public void testSearchSuggestion_documentIdFilter() throws Exception {
9666         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION));
9667         // Schema registration
9668         AppSearchSchema schema =
9669                 new AppSearchSchema.Builder("Type")
9670                         .addProperty(
9671                                 new StringPropertyConfig.Builder("body")
9672                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
9673                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
9674                                         .setIndexingType(
9675                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
9676                                         .build())
9677                         .build();
9678         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
9679 
9680         // Index documents
9681         GenericDocument doc1 =
9682                 new GenericDocument.Builder<>("namespace1", "id1", "Type")
9683                         .setPropertyString("body", "termone")
9684                         .build();
9685         GenericDocument doc2 =
9686                 new GenericDocument.Builder<>("namespace1", "id2", "Type")
9687                         .setPropertyString("body", "termtwo")
9688                         .build();
9689         GenericDocument doc3 =
9690                 new GenericDocument.Builder<>("namespace2", "id3", "Type")
9691                         .setPropertyString("body", "termthree")
9692                         .build();
9693         GenericDocument doc4 =
9694                 new GenericDocument.Builder<>("namespace2", "id4", "Type")
9695                         .setPropertyString("body", "termfour")
9696                         .build();
9697 
9698         checkIsBatchResultSuccess(
9699                 mDb1.putAsync(
9700                         new PutDocumentsRequest.Builder()
9701                                 .addGenericDocuments(doc1, doc2, doc3, doc4)
9702                                 .build()));
9703 
9704         SearchSuggestionResult resultOne =
9705                 new SearchSuggestionResult.Builder().setSuggestedResult("termone").build();
9706         SearchSuggestionResult resultTwo =
9707                 new SearchSuggestionResult.Builder().setSuggestedResult("termtwo").build();
9708         SearchSuggestionResult resultThree =
9709                 new SearchSuggestionResult.Builder().setSuggestedResult("termthree").build();
9710         SearchSuggestionResult resultFour =
9711                 new SearchSuggestionResult.Builder().setSuggestedResult("termfour").build();
9712 
9713         // Only search for namespace1/doc1
9714         List<SearchSuggestionResult> suggestions =
9715                 mDb1.searchSuggestionAsync(
9716                                 /* suggestionQueryExpression= */ "t",
9717                                 new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10)
9718                                         .addFilterNamespaces("namespace1")
9719                                         .addFilterDocumentIds("namespace1", "id1")
9720                                         .build())
9721                         .get();
9722         assertThat(suggestions).containsExactly(resultOne);
9723 
9724         // Only search for namespace1/doc1 and namespace1/doc2
9725         suggestions =
9726                 mDb1.searchSuggestionAsync(
9727                                 /* suggestionQueryExpression= */ "t",
9728                                 new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10)
9729                                         .addFilterNamespaces("namespace1")
9730                                         .addFilterDocumentIds(
9731                                                 "namespace1", ImmutableList.of("id1", "id2"))
9732                                         .build())
9733                         .get();
9734         assertThat(suggestions).containsExactly(resultOne, resultTwo);
9735 
9736         // Only search for namespace1/doc1 and namespace2/doc3
9737         suggestions =
9738                 mDb1.searchSuggestionAsync(
9739                                 /* suggestionQueryExpression= */ "t",
9740                                 new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10)
9741                                         .addFilterNamespaces("namespace1", "namespace2")
9742                                         .addFilterDocumentIds("namespace1", "id1")
9743                                         .addFilterDocumentIds("namespace2", ImmutableList.of("id3"))
9744                                         .build())
9745                         .get();
9746         assertThat(suggestions).containsExactly(resultOne, resultThree);
9747 
9748         // Only search for namespace1/doc1 and everything in namespace2
9749         suggestions =
9750                 mDb1.searchSuggestionAsync(
9751                                 /* suggestionQueryExpression= */ "t",
9752                                 new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10)
9753                                         .addFilterDocumentIds("namespace1", "id1")
9754                                         .build())
9755                         .get();
9756         assertThat(suggestions).containsExactly(resultOne, resultThree, resultFour);
9757     }
9758 
9759     @Test
testSearchSuggestion_schemaFilter()9760     public void testSearchSuggestion_schemaFilter() throws Exception {
9761         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION));
9762         // Schema registration
9763         AppSearchSchema schemaType1 =
9764                 new AppSearchSchema.Builder("Type1")
9765                         .addProperty(
9766                                 new StringPropertyConfig.Builder("body")
9767                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
9768                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
9769                                         .setIndexingType(
9770                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
9771                                         .build())
9772                         .build();
9773         AppSearchSchema schemaType2 =
9774                 new AppSearchSchema.Builder("Type2")
9775                         .addProperty(
9776                                 new StringPropertyConfig.Builder("body")
9777                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
9778                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
9779                                         .setIndexingType(
9780                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
9781                                         .build())
9782                         .build();
9783         AppSearchSchema schemaType3 =
9784                 new AppSearchSchema.Builder("Type3")
9785                         .addProperty(
9786                                 new StringPropertyConfig.Builder("body")
9787                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
9788                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
9789                                         .setIndexingType(
9790                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
9791                                         .build())
9792                         .build();
9793         mDb1.setSchemaAsync(
9794                         new SetSchemaRequest.Builder()
9795                                 .addSchemas(schemaType1, schemaType2, schemaType3)
9796                                 .build())
9797                 .get();
9798 
9799         // Index documents
9800         GenericDocument doc1 =
9801                 new GenericDocument.Builder<>("namespace", "id1", "Type1")
9802                         .setPropertyString("body", "fo foo")
9803                         .build();
9804         GenericDocument doc2 =
9805                 new GenericDocument.Builder<>("namespace", "id2", "Type2")
9806                         .setPropertyString("body", "foo")
9807                         .build();
9808         GenericDocument doc3 =
9809                 new GenericDocument.Builder<>("namespace", "id3", "Type3")
9810                         .setPropertyString("body", "fool")
9811                         .build();
9812 
9813         checkIsBatchResultSuccess(
9814                 mDb1.putAsync(
9815                         new PutDocumentsRequest.Builder()
9816                                 .addGenericDocuments(doc1, doc2, doc3)
9817                                 .build()));
9818 
9819         SearchSuggestionResult resultFo =
9820                 new SearchSuggestionResult.Builder().setSuggestedResult("fo").build();
9821         SearchSuggestionResult resultFoo =
9822                 new SearchSuggestionResult.Builder().setSuggestedResult("foo").build();
9823         SearchSuggestionResult resultFool =
9824                 new SearchSuggestionResult.Builder().setSuggestedResult("fool").build();
9825 
9826         // Type1 has 2 results.
9827         List<SearchSuggestionResult> suggestions =
9828                 mDb1.searchSuggestionAsync(
9829                                 /* suggestionQueryExpression= */ "f",
9830                                 new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10)
9831                                         .addFilterSchemas("Type1")
9832                                         .build())
9833                         .get();
9834         assertThat(suggestions).containsExactly(resultFoo, resultFo).inOrder();
9835 
9836         // Type2 has 1 result.
9837         suggestions =
9838                 mDb1.searchSuggestionAsync(
9839                                 /* suggestionQueryExpression= */ "f",
9840                                 new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10)
9841                                         .addFilterSchemas("Type2")
9842                                         .build())
9843                         .get();
9844         assertThat(suggestions).containsExactly(resultFoo).inOrder();
9845 
9846         // Type2 and 3 has 2 results.
9847         suggestions =
9848                 mDb1.searchSuggestionAsync(
9849                                 /* suggestionQueryExpression= */ "f",
9850                                 new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10)
9851                                         .addFilterSchemas("Type2", "Type3")
9852                                         .build())
9853                         .get();
9854         assertThat(suggestions).containsExactly(resultFoo, resultFool);
9855 
9856         // non exist type has empty result.
9857         suggestions =
9858                 mDb1.searchSuggestionAsync(
9859                                 /* suggestionQueryExpression= */ "f",
9860                                 new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10)
9861                                         .addFilterSchemas("nonExistType")
9862                                         .build())
9863                         .get();
9864         assertThat(suggestions).isEmpty();
9865     }
9866 
9867     @Test
testSearchSuggestion_differentPrefix()9868     public void testSearchSuggestion_differentPrefix() throws Exception {
9869         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION));
9870         // Schema registration
9871         AppSearchSchema schema =
9872                 new AppSearchSchema.Builder("Type")
9873                         .addProperty(
9874                                 new StringPropertyConfig.Builder("body")
9875                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
9876                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
9877                                         .setIndexingType(
9878                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
9879                                         .build())
9880                         .build();
9881         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
9882 
9883         // Index documents
9884         GenericDocument doc1 =
9885                 new GenericDocument.Builder<>("namespace", "id1", "Type")
9886                         .setPropertyString("body", "foo")
9887                         .build();
9888         GenericDocument doc2 =
9889                 new GenericDocument.Builder<>("namespace", "id2", "Type")
9890                         .setPropertyString("body", "fool")
9891                         .build();
9892         GenericDocument doc3 =
9893                 new GenericDocument.Builder<>("namespace", "id3", "Type")
9894                         .setPropertyString("body", "bar")
9895                         .build();
9896         GenericDocument doc4 =
9897                 new GenericDocument.Builder<>("namespace", "id4", "Type")
9898                         .setPropertyString("body", "baz")
9899                         .build();
9900 
9901         checkIsBatchResultSuccess(
9902                 mDb1.putAsync(
9903                         new PutDocumentsRequest.Builder()
9904                                 .addGenericDocuments(doc1, doc2, doc3, doc4)
9905                                 .build()));
9906 
9907         SearchSuggestionResult resultFoo =
9908                 new SearchSuggestionResult.Builder().setSuggestedResult("foo").build();
9909         SearchSuggestionResult resultFool =
9910                 new SearchSuggestionResult.Builder().setSuggestedResult("fool").build();
9911         SearchSuggestionResult resultBar =
9912                 new SearchSuggestionResult.Builder().setSuggestedResult("bar").build();
9913         SearchSuggestionResult resultBaz =
9914                 new SearchSuggestionResult.Builder().setSuggestedResult("baz").build();
9915 
9916         // prefix f has 2 results.
9917         List<SearchSuggestionResult> suggestions =
9918                 mDb1.searchSuggestionAsync(
9919                                 /* suggestionQueryExpression= */ "f",
9920                                 new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10)
9921                                         .build())
9922                         .get();
9923         assertThat(suggestions).containsExactly(resultFoo, resultFool);
9924 
9925         // prefix b has 2 results.
9926         suggestions =
9927                 mDb1.searchSuggestionAsync(
9928                                 /* suggestionQueryExpression= */ "b",
9929                                 new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10)
9930                                         .build())
9931                         .get();
9932         assertThat(suggestions).containsExactly(resultBar, resultBaz);
9933     }
9934 
9935     @Test
testSearchSuggestion_differentRankingStrategy()9936     public void testSearchSuggestion_differentRankingStrategy() throws Exception {
9937         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION));
9938         // Schema registration
9939         AppSearchSchema schema =
9940                 new AppSearchSchema.Builder("Type")
9941                         .addProperty(
9942                                 new StringPropertyConfig.Builder("body")
9943                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
9944                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
9945                                         .setIndexingType(
9946                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
9947                                         .build())
9948                         .build();
9949         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
9950 
9951         // Index documents
9952         // term1 appears 3 times in all 3 docs.
9953         // term2 appears 4 times in 2 docs.
9954         // term3 appears 5 times in 1 doc.
9955         GenericDocument doc1 =
9956                 new GenericDocument.Builder<>("namespace", "id1", "Type")
9957                         .setPropertyString("body", "term1 term3 term3 term3 term3 term3")
9958                         .build();
9959         GenericDocument doc2 =
9960                 new GenericDocument.Builder<>("namespace", "id2", "Type")
9961                         .setPropertyString("body", "term1 term2 term2 term2")
9962                         .build();
9963         GenericDocument doc3 =
9964                 new GenericDocument.Builder<>("namespace", "id3", "Type")
9965                         .setPropertyString("body", "term1 term2")
9966                         .build();
9967 
9968         checkIsBatchResultSuccess(
9969                 mDb1.putAsync(
9970                         new PutDocumentsRequest.Builder()
9971                                 .addGenericDocuments(doc1, doc2, doc3)
9972                                 .build()));
9973 
9974         SearchSuggestionResult result1 =
9975                 new SearchSuggestionResult.Builder().setSuggestedResult("term1").build();
9976         SearchSuggestionResult result2 =
9977                 new SearchSuggestionResult.Builder().setSuggestedResult("term2").build();
9978         SearchSuggestionResult result3 =
9979                 new SearchSuggestionResult.Builder().setSuggestedResult("term3").build();
9980 
9981         // rank by NONE, the order should be arbitrary but all terms appear.
9982         List<SearchSuggestionResult> suggestions =
9983                 mDb1.searchSuggestionAsync(
9984                                 /* suggestionQueryExpression= */ "t",
9985                                 new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10)
9986                                         .setRankingStrategy(
9987                                                 SearchSuggestionSpec
9988                                                         .SUGGESTION_RANKING_STRATEGY_NONE)
9989                                         .build())
9990                         .get();
9991         assertThat(suggestions).containsExactly(result2, result1, result3);
9992 
9993         // rank by document count, the order should be term1:3 > term2:2 > term3:1
9994         suggestions =
9995                 mDb1.searchSuggestionAsync(
9996                                 /* suggestionQueryExpression= */ "t",
9997                                 new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10)
9998                                         .setRankingStrategy(
9999                                                 SearchSuggestionSpec
10000                                                         .SUGGESTION_RANKING_STRATEGY_DOCUMENT_COUNT)
10001                                         .build())
10002                         .get();
10003         assertThat(suggestions).containsExactly(result1, result2, result3).inOrder();
10004 
10005         // rank by term frequency, the order should be term3:5 > term2:4 > term1:3
10006         suggestions =
10007                 mDb1.searchSuggestionAsync(
10008                                 /* suggestionQueryExpression= */ "t",
10009                                 new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10)
10010                                         .setRankingStrategy(
10011                                                 SearchSuggestionSpec
10012                                                         .SUGGESTION_RANKING_STRATEGY_TERM_FREQUENCY)
10013                                         .build())
10014                         .get();
10015         assertThat(suggestions).containsExactly(result3, result2, result1).inOrder();
10016     }
10017 
10018     @Test
testSearchSuggestion_removeDocument()10019     public void testSearchSuggestion_removeDocument() throws Exception {
10020         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION));
10021         // Schema registration
10022         AppSearchSchema schema =
10023                 new AppSearchSchema.Builder("Type")
10024                         .addProperty(
10025                                 new StringPropertyConfig.Builder("body")
10026                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
10027                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
10028                                         .setIndexingType(
10029                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
10030                                         .build())
10031                         .build();
10032         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
10033 
10034         // Index documents
10035         GenericDocument docTwo =
10036                 new GenericDocument.Builder<>("namespace", "idTwo", "Type")
10037                         .setPropertyString("body", "two")
10038                         .build();
10039         GenericDocument docThree =
10040                 new GenericDocument.Builder<>("namespace", "idThree", "Type")
10041                         .setPropertyString("body", "three")
10042                         .build();
10043         GenericDocument docTart =
10044                 new GenericDocument.Builder<>("namespace", "idTart", "Type")
10045                         .setPropertyString("body", "tart")
10046                         .build();
10047 
10048         checkIsBatchResultSuccess(
10049                 mDb1.putAsync(
10050                         new PutDocumentsRequest.Builder()
10051                                 .addGenericDocuments(docTwo, docThree, docTart)
10052                                 .build()));
10053 
10054         SearchSuggestionResult resultTwo =
10055                 new SearchSuggestionResult.Builder().setSuggestedResult("two").build();
10056         SearchSuggestionResult resultThree =
10057                 new SearchSuggestionResult.Builder().setSuggestedResult("three").build();
10058         SearchSuggestionResult resultTart =
10059                 new SearchSuggestionResult.Builder().setSuggestedResult("tart").build();
10060 
10061         // prefix t has 3 results.
10062         List<SearchSuggestionResult> suggestions =
10063                 mDb1.searchSuggestionAsync(
10064                                 /* suggestionQueryExpression= */ "t",
10065                                 new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10)
10066                                         .build())
10067                         .get();
10068         assertThat(suggestions).containsExactly(resultTwo, resultThree, resultTart);
10069 
10070         // Delete the document
10071         checkIsBatchResultSuccess(
10072                 mDb1.removeAsync(
10073                         new RemoveByDocumentIdRequest.Builder("namespace")
10074                                 .addIds("idTwo")
10075                                 .build()));
10076 
10077         // now prefix t has 2 results.
10078         suggestions =
10079                 mDb1.searchSuggestionAsync(
10080                                 /* suggestionQueryExpression= */ "t",
10081                                 new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10)
10082                                         .build())
10083                         .get();
10084         assertThat(suggestions).containsExactly(resultThree, resultTart);
10085     }
10086 
10087     @Test
testSearchSuggestion_replacementDocument()10088     public void testSearchSuggestion_replacementDocument() throws Exception {
10089         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION));
10090         // Schema registration
10091         AppSearchSchema schema =
10092                 new AppSearchSchema.Builder("Type")
10093                         .addProperty(
10094                                 new StringPropertyConfig.Builder("body")
10095                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
10096                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
10097                                         .setIndexingType(
10098                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
10099                                         .build())
10100                         .build();
10101         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
10102 
10103         // Index documents
10104         GenericDocument doc =
10105                 new GenericDocument.Builder<>("namespace", "id", "Type")
10106                         .setPropertyString("body", "two three tart")
10107                         .build();
10108 
10109         checkIsBatchResultSuccess(
10110                 mDb1.putAsync(new PutDocumentsRequest.Builder().addGenericDocuments(doc).build()));
10111 
10112         SearchSuggestionResult resultTwo =
10113                 new SearchSuggestionResult.Builder().setSuggestedResult("two").build();
10114         SearchSuggestionResult resultThree =
10115                 new SearchSuggestionResult.Builder().setSuggestedResult("three").build();
10116         SearchSuggestionResult resultTart =
10117                 new SearchSuggestionResult.Builder().setSuggestedResult("tart").build();
10118         SearchSuggestionResult resultTwist =
10119                 new SearchSuggestionResult.Builder().setSuggestedResult("twist").build();
10120 
10121         // prefix t has 3 results.
10122         List<SearchSuggestionResult> suggestions =
10123                 mDb1.searchSuggestionAsync(
10124                                 /* suggestionQueryExpression= */ "t",
10125                                 new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10)
10126                                         .build())
10127                         .get();
10128         assertThat(suggestions).containsExactly(resultTwo, resultThree, resultTart);
10129 
10130         // replace the document
10131         GenericDocument replaceDoc =
10132                 new GenericDocument.Builder<>("namespace", "id", "Type")
10133                         .setPropertyString("body", "twist three")
10134                         .build();
10135         checkIsBatchResultSuccess(
10136                 mDb1.putAsync(
10137                         new PutDocumentsRequest.Builder().addGenericDocuments(replaceDoc).build()));
10138 
10139         // prefix t has 2 results for now.
10140         suggestions =
10141                 mDb1.searchSuggestionAsync(
10142                                 /* suggestionQueryExpression= */ "t",
10143                                 new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10)
10144                                         .build())
10145                         .get();
10146         assertThat(suggestions).containsExactly(resultThree, resultTwist);
10147     }
10148 
10149     @Test
testSearchSuggestion_twoInstances()10150     public void testSearchSuggestion_twoInstances() throws Exception {
10151         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION));
10152         // Schema registration
10153         AppSearchSchema schema =
10154                 new AppSearchSchema.Builder("Type")
10155                         .addProperty(
10156                                 new StringPropertyConfig.Builder("body")
10157                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
10158                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
10159                                         .setIndexingType(
10160                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
10161                                         .build())
10162                         .build();
10163         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
10164         mDb2.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
10165 
10166         // Index documents to database 1.
10167         GenericDocument doc1 =
10168                 new GenericDocument.Builder<>("namespace", "id1", "Type")
10169                         .setPropertyString("body", "termOne termTwo")
10170                         .build();
10171         GenericDocument doc2 =
10172                 new GenericDocument.Builder<>("namespace", "id2", "Type")
10173                         .setPropertyString("body", "termOne")
10174                         .build();
10175         checkIsBatchResultSuccess(
10176                 mDb1.putAsync(
10177                         new PutDocumentsRequest.Builder().addGenericDocuments(doc1, doc2).build()));
10178 
10179         SearchSuggestionResult resultOne =
10180                 new SearchSuggestionResult.Builder().setSuggestedResult("termone").build();
10181         SearchSuggestionResult resultTwo =
10182                 new SearchSuggestionResult.Builder().setSuggestedResult("termtwo").build();
10183 
10184         // database 1 could get suggestion results
10185         List<SearchSuggestionResult> suggestions =
10186                 mDb1.searchSuggestionAsync(
10187                                 /* suggestionQueryExpression= */ "t",
10188                                 new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10)
10189                                         .build())
10190                         .get();
10191         assertThat(suggestions).containsExactly(resultOne, resultTwo).inOrder();
10192 
10193         // database 2 couldn't get suggestion results
10194         suggestions =
10195                 mDb2.searchSuggestionAsync(
10196                                 /* suggestionQueryExpression= */ "t",
10197                                 new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10)
10198                                         .build())
10199                         .get();
10200         assertThat(suggestions).isEmpty();
10201     }
10202 
10203     @Test
testSearchSuggestion_multipleTerms()10204     public void testSearchSuggestion_multipleTerms() throws Exception {
10205         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION));
10206         // Schema registration
10207         AppSearchSchema schema =
10208                 new AppSearchSchema.Builder("Type")
10209                         .addProperty(
10210                                 new StringPropertyConfig.Builder("body")
10211                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
10212                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
10213                                         .setIndexingType(
10214                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
10215                                         .build())
10216                         .build();
10217         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
10218 
10219         // Index documents
10220         GenericDocument doc1 =
10221                 new GenericDocument.Builder<>("namespace", "id1", "Type")
10222                         .setPropertyString("body", "bar fo")
10223                         .build();
10224         GenericDocument doc2 =
10225                 new GenericDocument.Builder<>("namespace", "id2", "Type")
10226                         .setPropertyString("body", "cat foo")
10227                         .build();
10228         GenericDocument doc3 =
10229                 new GenericDocument.Builder<>("namespace", "id3", "Type")
10230                         .setPropertyString("body", "fool")
10231                         .build();
10232         checkIsBatchResultSuccess(
10233                 mDb1.putAsync(
10234                         new PutDocumentsRequest.Builder()
10235                                 .addGenericDocuments(doc1, doc2, doc3)
10236                                 .build()));
10237 
10238         // Search "bar AND f" only document 1 should match the search.
10239         List<SearchSuggestionResult> suggestions =
10240                 mDb1.searchSuggestionAsync(
10241                                 /* suggestionQueryExpression= */ "bar f",
10242                                 new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10)
10243                                         .build())
10244                         .get();
10245         SearchSuggestionResult barFo =
10246                 new SearchSuggestionResult.Builder().setSuggestedResult("bar fo").build();
10247         assertThat(suggestions).containsExactly(barFo);
10248 
10249         // Search for "(bar OR cat) AND f" both document1 "bar fo" and document2 "cat foo" could
10250         // match.
10251         suggestions =
10252                 mDb1.searchSuggestionAsync(
10253                                 /* suggestionQueryExpression= */ "bar OR cat f",
10254                                 new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10)
10255                                         .build())
10256                         .get();
10257         SearchSuggestionResult barCatFo =
10258                 new SearchSuggestionResult.Builder().setSuggestedResult("bar OR cat fo").build();
10259         SearchSuggestionResult barCatFoo =
10260                 new SearchSuggestionResult.Builder().setSuggestedResult("bar OR cat foo").build();
10261         assertThat(suggestions).containsExactly(barCatFo, barCatFoo);
10262 
10263         // Search for "(bar AND cat) OR f", all documents could match.
10264         suggestions =
10265                 mDb1.searchSuggestionAsync(
10266                                 /* suggestionQueryExpression= */ "(bar cat) OR f",
10267                                 new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10)
10268                                         .build())
10269                         .get();
10270         SearchSuggestionResult barCatOrFo =
10271                 new SearchSuggestionResult.Builder().setSuggestedResult("(bar cat) OR fo").build();
10272         SearchSuggestionResult barCatOrFoo =
10273                 new SearchSuggestionResult.Builder().setSuggestedResult("(bar cat) OR foo").build();
10274         SearchSuggestionResult barCatOrFool =
10275                 new SearchSuggestionResult.Builder()
10276                         .setSuggestedResult("(bar cat) OR fool")
10277                         .build();
10278         assertThat(suggestions).containsExactly(barCatOrFo, barCatOrFoo, barCatOrFool);
10279 
10280         // Search for "-bar f", document2 "cat foo" could and document3 "fool" could match.
10281         suggestions =
10282                 mDb1.searchSuggestionAsync(
10283                                 /* suggestionQueryExpression= */ "-bar f",
10284                                 new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10)
10285                                         .build())
10286                         .get();
10287         SearchSuggestionResult noBarFoo =
10288                 new SearchSuggestionResult.Builder().setSuggestedResult("-bar foo").build();
10289         SearchSuggestionResult noBarFool =
10290                 new SearchSuggestionResult.Builder().setSuggestedResult("-bar fool").build();
10291         assertThat(suggestions).containsExactly(noBarFoo, noBarFool);
10292     }
10293 
10294     @Test
testSearchSuggestion_propertyFilter()10295     public void testSearchSuggestion_propertyFilter() throws Exception {
10296         assumeTrue(
10297                 mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
10298         // Schema registration
10299         AppSearchSchema schemaType1 =
10300                 new AppSearchSchema.Builder("Type1")
10301                         .addProperty(
10302                                 new StringPropertyConfig.Builder("propertyone")
10303                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
10304                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
10305                                         .setIndexingType(
10306                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
10307                                         .build())
10308                         .addProperty(
10309                                 new StringPropertyConfig.Builder("propertytwo")
10310                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
10311                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
10312                                         .setIndexingType(
10313                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
10314                                         .build())
10315                         .build();
10316         AppSearchSchema schemaType2 =
10317                 new AppSearchSchema.Builder("Type2")
10318                         .addProperty(
10319                                 new StringPropertyConfig.Builder("propertythree")
10320                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
10321                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
10322                                         .setIndexingType(
10323                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
10324                                         .build())
10325                         .addProperty(
10326                                 new StringPropertyConfig.Builder("propertyfour")
10327                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
10328                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
10329                                         .setIndexingType(
10330                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
10331                                         .build())
10332                         .build();
10333         mDb1.setSchemaAsync(
10334                         new SetSchemaRequest.Builder().addSchemas(schemaType1, schemaType2).build())
10335                 .get();
10336 
10337         // Index documents
10338         GenericDocument doc1 =
10339                 new GenericDocument.Builder<>("namespace", "id1", "Type1")
10340                         .setPropertyString("propertyone", "termone")
10341                         .setPropertyString("propertytwo", "termtwo")
10342                         .build();
10343         GenericDocument doc2 =
10344                 new GenericDocument.Builder<>("namespace", "id2", "Type2")
10345                         .setPropertyString("propertythree", "termthree")
10346                         .setPropertyString("propertyfour", "termfour")
10347                         .build();
10348 
10349         checkIsBatchResultSuccess(
10350                 mDb1.putAsync(
10351                         new PutDocumentsRequest.Builder().addGenericDocuments(doc1, doc2).build()));
10352 
10353         SearchSuggestionResult resultOne =
10354                 new SearchSuggestionResult.Builder().setSuggestedResult("termone").build();
10355         SearchSuggestionResult resultTwo =
10356                 new SearchSuggestionResult.Builder().setSuggestedResult("termtwo").build();
10357         SearchSuggestionResult resultThree =
10358                 new SearchSuggestionResult.Builder().setSuggestedResult("termthree").build();
10359         SearchSuggestionResult resultFour =
10360                 new SearchSuggestionResult.Builder().setSuggestedResult("termfour").build();
10361 
10362         // Only search for type1/propertyone
10363         List<SearchSuggestionResult> suggestions =
10364                 mDb1.searchSuggestionAsync(
10365                                 /* suggestionQueryExpression= */ "t",
10366                                 new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10)
10367                                         .addFilterSchemas("Type1")
10368                                         .addFilterProperties(
10369                                                 "Type1", ImmutableList.of("propertyone"))
10370                                         .build())
10371                         .get();
10372         assertThat(suggestions).containsExactly(resultOne);
10373 
10374         // Only search for type1/propertyone and type1/propertytwo
10375         suggestions =
10376                 mDb1.searchSuggestionAsync(
10377                                 /* suggestionQueryExpression= */ "t",
10378                                 new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10)
10379                                         .addFilterSchemas("Type1")
10380                                         .addFilterProperties(
10381                                                 "Type1",
10382                                                 ImmutableList.of("propertyone", "propertytwo"))
10383                                         .build())
10384                         .get();
10385         assertThat(suggestions).containsExactly(resultOne, resultTwo);
10386 
10387         // Only search for type1/propertyone and type2/propertythree
10388         suggestions =
10389                 mDb1.searchSuggestionAsync(
10390                                 /* suggestionQueryExpression= */ "t",
10391                                 new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10)
10392                                         .addFilterSchemas("Type1", "Type2")
10393                                         .addFilterProperties(
10394                                                 "Type1", ImmutableList.of("propertyone"))
10395                                         .addFilterProperties(
10396                                                 "Type2", ImmutableList.of("propertythree"))
10397                                         .build())
10398                         .get();
10399         assertThat(suggestions).containsExactly(resultOne, resultThree);
10400 
10401         // Only search for type1/propertyone and type2/propertyfour, in addFilterPropertyPaths
10402         suggestions =
10403                 mDb1.searchSuggestionAsync(
10404                                 /* suggestionQueryExpression= */ "t",
10405                                 new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10)
10406                                         .addFilterSchemas("Type1", "Type2")
10407                                         .addFilterProperties(
10408                                                 "Type1", ImmutableList.of("propertyone"))
10409                                         .addFilterPropertyPaths(
10410                                                 "Type2",
10411                                                 ImmutableList.of(new PropertyPath("propertyfour")))
10412                                         .build())
10413                         .get();
10414         assertThat(suggestions).containsExactly(resultOne, resultFour);
10415 
10416         // Only search for type1/propertyone and everything in type2
10417         suggestions =
10418                 mDb1.searchSuggestionAsync(
10419                                 /* suggestionQueryExpression= */ "t",
10420                                 new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10)
10421                                         .addFilterProperties(
10422                                                 "Type1", ImmutableList.of("propertyone"))
10423                                         .build())
10424                         .get();
10425         assertThat(suggestions).containsExactly(resultOne, resultThree, resultFour);
10426     }
10427 
10428     @Test
testSearchSuggestion_propertyFilter_notSupported()10429     public void testSearchSuggestion_propertyFilter_notSupported() throws Exception {
10430         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION));
10431         assumeFalse(
10432                 mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
10433 
10434         SearchSuggestionSpec searchSuggestionSpec =
10435                 new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10)
10436                         .addFilterSchemas("Type1")
10437                         .addFilterProperties("Type1", ImmutableList.of("property"))
10438                         .build();
10439 
10440         // Search suggest with type property filters {"Email", ["property"]} and verify that
10441         // unsupported exception is thrown
10442         UnsupportedOperationException exception =
10443                 assertThrows(
10444                         UnsupportedOperationException.class,
10445                         () ->
10446                                 mDb1.searchSuggestionAsync(
10447                                                 /* suggestionQueryExpression= */ "t",
10448                                                 searchSuggestionSpec)
10449                                         .get());
10450         assertThat(exception)
10451                 .hasMessageThat()
10452                 .contains(
10453                         Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES
10454                                 + " is not available on this AppSearch implementation.");
10455     }
10456 
10457     @Test
testSearchSuggestion_PropertyRestriction()10458     public void testSearchSuggestion_PropertyRestriction() throws Exception {
10459         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION));
10460         // Schema registration
10461         AppSearchSchema schema =
10462                 new AppSearchSchema.Builder("Type")
10463                         .addProperty(
10464                                 new StringPropertyConfig.Builder("subject")
10465                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
10466                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
10467                                         .setIndexingType(
10468                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
10469                                         .build())
10470                         .addProperty(
10471                                 new StringPropertyConfig.Builder("body")
10472                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
10473                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
10474                                         .setIndexingType(
10475                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
10476                                         .build())
10477                         .build();
10478         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
10479 
10480         // Index documents
10481         GenericDocument doc1 =
10482                 new GenericDocument.Builder<>("namespace", "id1", "Type")
10483                         .setPropertyString("subject", "bar fo")
10484                         .setPropertyString("body", "fool")
10485                         .build();
10486         GenericDocument doc2 =
10487                 new GenericDocument.Builder<>("namespace", "id2", "Type")
10488                         .setPropertyString("subject", "bar cat foo")
10489                         .setPropertyString("body", "fool")
10490                         .build();
10491         GenericDocument doc3 =
10492                 new GenericDocument.Builder<>("namespace", "ide", "Type")
10493                         .setPropertyString("subject", "fool")
10494                         .setPropertyString("body", "fool")
10495                         .build();
10496         checkIsBatchResultSuccess(
10497                 mDb1.putAsync(
10498                         new PutDocumentsRequest.Builder()
10499                                 .addGenericDocuments(doc1, doc2, doc3)
10500                                 .build()));
10501 
10502         // Search for "bar AND subject:f"
10503         List<SearchSuggestionResult> suggestions =
10504                 mDb1.searchSuggestionAsync(
10505                                 /* suggestionQueryExpression= */ "bar subject:f",
10506                                 new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10)
10507                                         .build())
10508                         .get();
10509         SearchSuggestionResult barSubjectFo =
10510                 new SearchSuggestionResult.Builder().setSuggestedResult("bar subject:fo").build();
10511         SearchSuggestionResult barSubjectFoo =
10512                 new SearchSuggestionResult.Builder().setSuggestedResult("bar subject:foo").build();
10513         assertThat(suggestions).containsExactly(barSubjectFo, barSubjectFoo);
10514     }
10515 
10516     @Test
testGetSchema_parentTypes()10517     public void testGetSchema_parentTypes() throws Exception {
10518         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
10519         AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email").build();
10520         AppSearchSchema messageSchema = new AppSearchSchema.Builder("Message").build();
10521         AppSearchSchema emailMessageSchema =
10522                 new AppSearchSchema.Builder("EmailMessage")
10523                         .addProperty(
10524                                 new StringPropertyConfig.Builder("sender")
10525                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
10526                                         .build())
10527                         .addProperty(
10528                                 new StringPropertyConfig.Builder("email")
10529                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
10530                                         .build())
10531                         .addProperty(
10532                                 new StringPropertyConfig.Builder("content")
10533                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
10534                                         .build())
10535                         .addParentType("Email")
10536                         .addParentType("Message")
10537                         .build();
10538 
10539         SetSchemaRequest request =
10540                 new SetSchemaRequest.Builder()
10541                         .addSchemas(emailMessageSchema)
10542                         .addSchemas(emailSchema)
10543                         .addSchemas(messageSchema)
10544                         .build();
10545 
10546         mDb1.setSchemaAsync(request).get();
10547 
10548         Set<AppSearchSchema> actual = mDb1.getSchemaAsync().get().getSchemas();
10549         assertThat(actual).hasSize(3);
10550         assertThat(actual).isEqualTo(request.getSchemas());
10551 
10552         // Check that calling getParentType() for the EmailMessage schema returns Email and Message
10553         for (AppSearchSchema schema : actual) {
10554             if (schema.getSchemaType().equals("EmailMessage")) {
10555                 assertThat(schema.getParentTypes()).containsExactly("Email", "Message");
10556             }
10557         }
10558     }
10559 
10560     @Test
testGetSchema_parentTypes_notSupported()10561     public void testGetSchema_parentTypes_notSupported() throws Exception {
10562         assumeFalse(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
10563         AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email").build();
10564         AppSearchSchema messageSchema = new AppSearchSchema.Builder("Message").build();
10565         AppSearchSchema emailMessageSchema =
10566                 new AppSearchSchema.Builder("EmailMessage")
10567                         .addParentType("Email")
10568                         .addParentType("Message")
10569                         .build();
10570 
10571         SetSchemaRequest request =
10572                 new SetSchemaRequest.Builder()
10573                         .addSchemas(emailMessageSchema)
10574                         .addSchemas(emailSchema)
10575                         .addSchemas(messageSchema)
10576                         .build();
10577 
10578         UnsupportedOperationException e =
10579                 assertThrows(
10580                         UnsupportedOperationException.class,
10581                         () -> mDb1.setSchemaAsync(request).get());
10582         assertThat(e)
10583                 .hasMessageThat()
10584                 .contains(
10585                         Features.SCHEMA_ADD_PARENT_TYPE
10586                                 + " is not available on this AppSearch implementation.");
10587     }
10588 
10589     @Test
testGetSchema_indexableNestedPropsList()10590     public void testGetSchema_indexableNestedPropsList() throws Exception {
10591         assumeTrue(
10592                 mDb1.getFeatures()
10593                         .isFeatureSupported(Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES));
10594 
10595         AppSearchSchema personSchema =
10596                 new AppSearchSchema.Builder("Person")
10597                         .addProperty(
10598                                 new StringPropertyConfig.Builder("name")
10599                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
10600                                         .setIndexingType(
10601                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
10602                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
10603                                         .build())
10604                         .addProperty(
10605                                 new AppSearchSchema.DocumentPropertyConfig.Builder(
10606                                                 "worksFor", "Organization")
10607                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
10608                                         .setShouldIndexNestedProperties(false)
10609                                         .addIndexableNestedProperties(Collections.singleton("name"))
10610                                         .build())
10611                         .build();
10612         AppSearchSchema organizationSchema =
10613                 new AppSearchSchema.Builder("Organization")
10614                         .addProperty(
10615                                 new StringPropertyConfig.Builder("name")
10616                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
10617                                         .setIndexingType(
10618                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
10619                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
10620                                         .build())
10621                         .addProperty(
10622                                 new StringPropertyConfig.Builder("notes")
10623                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
10624                                         .setIndexingType(
10625                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
10626                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
10627                                         .build())
10628                         .build();
10629 
10630         SetSchemaRequest setSchemaRequest =
10631                 new SetSchemaRequest.Builder().addSchemas(personSchema, organizationSchema).build();
10632         mDb1.setSchemaAsync(setSchemaRequest).get();
10633 
10634         Set<AppSearchSchema> actual = mDb1.getSchemaAsync().get().getSchemas();
10635         assertThat(actual).hasSize(2);
10636         assertThat(actual).isEqualTo(setSchemaRequest.getSchemas());
10637 
10638         for (AppSearchSchema schema : actual) {
10639             if (schema.getSchemaType().equals("Person")) {
10640                 for (PropertyConfig property : schema.getProperties()) {
10641                     if (property.getName().equals("worksFor")) {
10642                         assertThat(
10643                                         ((DocumentPropertyConfig) property)
10644                                                 .getIndexableNestedProperties())
10645                                 .containsExactly("name");
10646                     }
10647                 }
10648             }
10649         }
10650     }
10651 
10652     @Test
testSetSchema_dataTypeIncompatibleWithParentTypes()10653     public void testSetSchema_dataTypeIncompatibleWithParentTypes() throws Exception {
10654         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
10655         AppSearchSchema messageSchema =
10656                 new AppSearchSchema.Builder("Message")
10657                         .addProperty(
10658                                 new AppSearchSchema.LongPropertyConfig.Builder("sender")
10659                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
10660                                         .build())
10661                         .build();
10662         AppSearchSchema emailSchema =
10663                 new AppSearchSchema.Builder("Email")
10664                         .addParentType("Message")
10665                         .addProperty(
10666                                 new StringPropertyConfig.Builder("sender")
10667                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
10668                                         .build())
10669                         .build();
10670 
10671         SetSchemaRequest request =
10672                 new SetSchemaRequest.Builder()
10673                         .addSchemas(messageSchema)
10674                         .addSchemas(emailSchema)
10675                         .build();
10676 
10677         ExecutionException executionException =
10678                 assertThrows(ExecutionException.class, () -> mDb1.setSchemaAsync(request).get());
10679         assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
10680         AppSearchException exception = (AppSearchException) executionException.getCause();
10681         assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT);
10682         assertThat(exception)
10683                 .hasMessageThat()
10684                 .containsMatch(
10685                         "Property sender from child type .*\\$/Email is not compatible"
10686                                 + " to the parent type .*\\$/Message.");
10687     }
10688 
10689     @Test
testSetSchema_documentTypeIncompatibleWithParentTypes()10690     public void testSetSchema_documentTypeIncompatibleWithParentTypes() throws Exception {
10691         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
10692         AppSearchSchema personSchema = new AppSearchSchema.Builder("Person").build();
10693         AppSearchSchema artistSchema =
10694                 new AppSearchSchema.Builder("Artist").addParentType("Person").build();
10695         AppSearchSchema messageSchema =
10696                 new AppSearchSchema.Builder("Message")
10697                         .addProperty(
10698                                 new AppSearchSchema.DocumentPropertyConfig.Builder(
10699                                                 "sender", "Artist")
10700                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
10701                                         .build())
10702                         .build();
10703         AppSearchSchema emailSchema =
10704                 new AppSearchSchema.Builder("Email")
10705                         .addParentType("Message")
10706                         // "sender" is defined as an Artist in the parent type Message, which
10707                         // requires "sender"'s type here to be a subtype of Artist. Thus, this is
10708                         // incompatible because Person is not a subtype of Artist.
10709                         .addProperty(
10710                                 new AppSearchSchema.DocumentPropertyConfig.Builder(
10711                                                 "sender", "Person")
10712                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
10713                                         .build())
10714                         .build();
10715 
10716         SetSchemaRequest request =
10717                 new SetSchemaRequest.Builder()
10718                         .addSchemas(personSchema)
10719                         .addSchemas(artistSchema)
10720                         .addSchemas(messageSchema)
10721                         .addSchemas(emailSchema)
10722                         .build();
10723 
10724         ExecutionException executionException =
10725                 assertThrows(ExecutionException.class, () -> mDb1.setSchemaAsync(request).get());
10726         assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
10727         AppSearchException exception = (AppSearchException) executionException.getCause();
10728         assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT);
10729         assertThat(exception)
10730                 .hasMessageThat()
10731                 .containsMatch(
10732                         "Property sender from child type .*\\$/Email is not compatible"
10733                                 + " to the parent type .*\\$/Message.");
10734     }
10735 
10736     @Test
testSetSchema_compatibleWithParentTypes()10737     public void testSetSchema_compatibleWithParentTypes() throws Exception {
10738         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
10739         AppSearchSchema personSchema = new AppSearchSchema.Builder("Person").build();
10740         AppSearchSchema artistSchema =
10741                 new AppSearchSchema.Builder("Artist").addParentType("Person").build();
10742         AppSearchSchema messageSchema =
10743                 new AppSearchSchema.Builder("Message")
10744                         .addProperty(
10745                                 new AppSearchSchema.DocumentPropertyConfig.Builder(
10746                                                 "sender", "Person")
10747                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
10748                                         .build())
10749                         .addProperty(
10750                                 new StringPropertyConfig.Builder("note")
10751                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
10752                                         .setIndexingType(
10753                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
10754                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
10755                                         .build())
10756                         .build();
10757         AppSearchSchema emailSchema =
10758                 new AppSearchSchema.Builder("Email")
10759                         .addParentType("Message")
10760                         .addProperty(
10761                                 // Artist is a subtype of Person, so compatible
10762                                 new AppSearchSchema.DocumentPropertyConfig.Builder(
10763                                                 "sender", "Artist")
10764                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
10765                                         .build())
10766                         .addProperty(
10767                                 new StringPropertyConfig.Builder("note")
10768                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
10769                                         // A different indexing or tokenizer type is ok.
10770                                         .setIndexingType(
10771                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
10772                                         .setTokenizerType(
10773                                                 StringPropertyConfig.TOKENIZER_TYPE_VERBATIM)
10774                                         .build())
10775                         .build();
10776 
10777         SetSchemaRequest request =
10778                 new SetSchemaRequest.Builder()
10779                         .addSchemas(personSchema)
10780                         .addSchemas(artistSchema)
10781                         .addSchemas(messageSchema)
10782                         .addSchemas(emailSchema)
10783                         .build();
10784 
10785         mDb1.setSchemaAsync(request).get();
10786 
10787         Set<AppSearchSchema> actual = mDb1.getSchemaAsync().get().getSchemas();
10788         assertThat(actual).hasSize(4);
10789         assertThat(actual).isEqualTo(request.getSchemas());
10790     }
10791 
10792     @Test
testSetSchema_indexableNestedPropsList()10793     public void testSetSchema_indexableNestedPropsList() throws Exception {
10794         assumeTrue(
10795                 mDb1.getFeatures()
10796                         .isFeatureSupported(Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES));
10797 
10798         AppSearchSchema personSchema =
10799                 new AppSearchSchema.Builder("Person")
10800                         .addProperty(
10801                                 new StringPropertyConfig.Builder("name")
10802                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
10803                                         .setIndexingType(
10804                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
10805                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
10806                                         .build())
10807                         .addProperty(
10808                                 new AppSearchSchema.DocumentPropertyConfig.Builder(
10809                                                 "worksFor", "Organization")
10810                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
10811                                         .setShouldIndexNestedProperties(false)
10812                                         .addIndexableNestedProperties(Collections.singleton("name"))
10813                                         .build())
10814                         .build();
10815         AppSearchSchema organizationSchema =
10816                 new AppSearchSchema.Builder("Organization")
10817                         .addProperty(
10818                                 new StringPropertyConfig.Builder("name")
10819                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
10820                                         .setIndexingType(
10821                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
10822                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
10823                                         .build())
10824                         .addProperty(
10825                                 new StringPropertyConfig.Builder("notes")
10826                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
10827                                         .setIndexingType(
10828                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
10829                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
10830                                         .build())
10831                         .build();
10832 
10833         mDb1.setSchemaAsync(
10834                         new SetSchemaRequest.Builder()
10835                                 .addSchemas(personSchema, organizationSchema)
10836                                 .build())
10837                 .get();
10838 
10839         // Test that properties in Person's indexable_nested_properties_list are indexed and
10840         // searchable
10841         GenericDocument org1 =
10842                 new GenericDocument.Builder<>("namespace", "org1", "Organization")
10843                         .setPropertyString("name", "Org1")
10844                         .setPropertyString("notes", "Some notes")
10845                         .build();
10846         GenericDocument person1 =
10847                 new GenericDocument.Builder<>("namespace", "person1", "Person")
10848                         .setPropertyString("name", "Jane")
10849                         .setPropertyDocument("worksFor", org1)
10850                         .build();
10851 
10852         AppSearchBatchResult<String, Void> putResult =
10853                 checkIsBatchResultSuccess(
10854                         mDb1.putAsync(
10855                                 new PutDocumentsRequest.Builder()
10856                                         .addGenericDocuments(person1, org1)
10857                                         .build()));
10858         assertThat(putResult.getSuccesses()).containsExactly("person1", null, "org1", null);
10859         assertThat(putResult.getFailures()).isEmpty();
10860 
10861         GetByDocumentIdRequest getByDocumentIdRequest =
10862                 new GetByDocumentIdRequest.Builder("namespace").addIds("person1", "org1").build();
10863         List<GenericDocument> outDocuments = doGet(mDb1, getByDocumentIdRequest);
10864         assertThat(outDocuments).hasSize(2);
10865         assertThat(outDocuments).containsExactly(person1, org1);
10866 
10867         // Both org1 and person should be returned for query "Org1"
10868         // For org1 this matches the 'name' property and for person1 this matches the
10869         // 'worksFor.name' property.
10870         SearchResultsShim searchResults =
10871                 mDb1.search(
10872                         "Org1",
10873                         new SearchSpec.Builder()
10874                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
10875                                 .build());
10876         outDocuments = convertSearchResultsToDocuments(searchResults);
10877         assertThat(outDocuments).hasSize(2);
10878         assertThat(outDocuments).containsExactly(person1, org1);
10879 
10880         // Only org1 should be returned for query "notes", since 'worksFor.notes' is not indexed
10881         // for the Person-type.
10882         searchResults =
10883                 mDb1.search(
10884                         "notes",
10885                         new SearchSpec.Builder()
10886                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
10887                                 .build());
10888         outDocuments = convertSearchResultsToDocuments(searchResults);
10889         assertThat(outDocuments).hasSize(1);
10890         assertThat(outDocuments).containsExactly(org1);
10891     }
10892 
10893     @Test
testSetSchema_indexableNestedPropsList_notSupported()10894     public void testSetSchema_indexableNestedPropsList_notSupported() throws Exception {
10895         assumeFalse(
10896                 mDb1.getFeatures()
10897                         .isFeatureSupported(Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES));
10898 
10899         AppSearchSchema personSchema =
10900                 new AppSearchSchema.Builder("Person")
10901                         .addProperty(
10902                                 new StringPropertyConfig.Builder("name")
10903                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
10904                                         .setIndexingType(
10905                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
10906                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
10907                                         .build())
10908                         .addProperty(
10909                                 new AppSearchSchema.DocumentPropertyConfig.Builder(
10910                                                 "worksFor", "Organization")
10911                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
10912                                         .setShouldIndexNestedProperties(false)
10913                                         .addIndexableNestedProperties(Collections.singleton("name"))
10914                                         .build())
10915                         .build();
10916         AppSearchSchema organizationSchema =
10917                 new AppSearchSchema.Builder("Organization")
10918                         .addProperty(
10919                                 new StringPropertyConfig.Builder("name")
10920                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
10921                                         .setIndexingType(
10922                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
10923                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
10924                                         .build())
10925                         .addProperty(
10926                                 new StringPropertyConfig.Builder("notes")
10927                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
10928                                         .setIndexingType(
10929                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
10930                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
10931                                         .build())
10932                         .build();
10933 
10934         SetSchemaRequest setSchemaRequest =
10935                 new SetSchemaRequest.Builder().addSchemas(personSchema, organizationSchema).build();
10936         UnsupportedOperationException e =
10937                 assertThrows(
10938                         UnsupportedOperationException.class,
10939                         () -> mDb1.setSchemaAsync(setSchemaRequest).get());
10940         assertThat(e)
10941                 .hasMessageThat()
10942                 .contains(
10943                         "DocumentPropertyConfig.addIndexableNestedProperties is not supported on"
10944                                 + " this AppSearch implementation.");
10945     }
10946 
10947     @Test
testSetSchema_indexableNestedPropsList_nonIndexableProp()10948     public void testSetSchema_indexableNestedPropsList_nonIndexableProp() throws Exception {
10949         assumeTrue(
10950                 mDb1.getFeatures()
10951                         .isFeatureSupported(Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES));
10952 
10953         AppSearchSchema personSchema =
10954                 new AppSearchSchema.Builder("Person")
10955                         .addProperty(
10956                                 new StringPropertyConfig.Builder("name")
10957                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
10958                                         .setIndexingType(
10959                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
10960                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
10961                                         .build())
10962                         .addProperty(
10963                                 new AppSearchSchema.DocumentPropertyConfig.Builder(
10964                                                 "worksFor", "Organization")
10965                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
10966                                         .setShouldIndexNestedProperties(false)
10967                                         .addIndexableNestedProperties(Collections.singleton("name"))
10968                                         .build())
10969                         .build();
10970         AppSearchSchema organizationSchema =
10971                 new AppSearchSchema.Builder("Organization")
10972                         .addProperty(
10973                                 new StringPropertyConfig.Builder("name")
10974                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
10975                                         .setIndexingType(
10976                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
10977                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
10978                                         .build())
10979                         .addProperty(
10980                                 new StringPropertyConfig.Builder("notes")
10981                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
10982                                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_NONE)
10983                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_NONE)
10984                                         .build())
10985                         .build();
10986 
10987         mDb1.setSchemaAsync(
10988                         new SetSchemaRequest.Builder()
10989                                 .addSchemas(personSchema, organizationSchema)
10990                                 .build())
10991                 .get();
10992 
10993         // Test that Person's nested properties are indexed correctly.
10994         GenericDocument org1 =
10995                 new GenericDocument.Builder<>("namespace", "org1", "Organization")
10996                         .setPropertyString("name", "Org1")
10997                         .setPropertyString("notes", "Some notes")
10998                         .build();
10999         GenericDocument person1 =
11000                 new GenericDocument.Builder<>("namespace", "person1", "Person")
11001                         .setPropertyString("name", "Jane")
11002                         .setPropertyDocument("worksFor", org1)
11003                         .build();
11004 
11005         AppSearchBatchResult<String, Void> putResult =
11006                 checkIsBatchResultSuccess(
11007                         mDb1.putAsync(
11008                                 new PutDocumentsRequest.Builder()
11009                                         .addGenericDocuments(person1, org1)
11010                                         .build()));
11011         assertThat(putResult.getSuccesses()).containsExactly("person1", null, "org1", null);
11012         assertThat(putResult.getFailures()).isEmpty();
11013 
11014         GetByDocumentIdRequest getByDocumentIdRequest =
11015                 new GetByDocumentIdRequest.Builder("namespace").addIds("person1", "org1").build();
11016         List<GenericDocument> outDocuments = doGet(mDb1, getByDocumentIdRequest);
11017         assertThat(outDocuments).hasSize(2);
11018         assertThat(outDocuments).containsExactly(person1, org1);
11019 
11020         // Both org1 and person should be returned for query "Org1"
11021         // For org1 this matches the 'name' property and for person1 this matches the
11022         // 'worksFor.name' property.
11023         SearchResultsShim searchResults =
11024                 mDb1.search(
11025                         "Org1",
11026                         new SearchSpec.Builder()
11027                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
11028                                 .build());
11029         outDocuments = convertSearchResultsToDocuments(searchResults);
11030         assertThat(outDocuments).hasSize(2);
11031         assertThat(outDocuments).containsExactly(person1, org1);
11032 
11033         // No documents should match for "notes", since both 'Organization:notes'
11034         // and 'Person:worksFor.notes' are non-indexable.
11035         searchResults =
11036                 mDb1.search(
11037                         "notes",
11038                         new SearchSpec.Builder()
11039                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
11040                                 .build());
11041         outDocuments = convertSearchResultsToDocuments(searchResults);
11042         assertThat(outDocuments).hasSize(0);
11043     }
11044 
11045     @Test
testSetSchema_indexableNestedPropsList_multipleNestedLevels()11046     public void testSetSchema_indexableNestedPropsList_multipleNestedLevels() throws Exception {
11047         assumeTrue(
11048                 mDb1.getFeatures()
11049                         .isFeatureSupported(Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES));
11050 
11051         AppSearchSchema emailSchema =
11052                 new AppSearchSchema.Builder("Email")
11053                         .addProperty(
11054                                 new StringPropertyConfig.Builder("subject")
11055                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
11056                                         .setIndexingType(
11057                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
11058                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
11059                                         .build())
11060                         .addProperty(
11061                                 new AppSearchSchema.DocumentPropertyConfig.Builder(
11062                                                 "sender", "Person")
11063                                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
11064                                         .setShouldIndexNestedProperties(false)
11065                                         .addIndexableNestedProperties(
11066                                                 Arrays.asList(
11067                                                         "name", "worksFor.name", "worksFor.notes"))
11068                                         .build())
11069                         .addProperty(
11070                                 new AppSearchSchema.DocumentPropertyConfig.Builder(
11071                                                 "recipient", "Person")
11072                                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
11073                                         .setShouldIndexNestedProperties(true)
11074                                         .build())
11075                         .build();
11076         AppSearchSchema personSchema =
11077                 new AppSearchSchema.Builder("Person")
11078                         .addProperty(
11079                                 new StringPropertyConfig.Builder("name")
11080                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
11081                                         .setIndexingType(
11082                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
11083                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
11084                                         .build())
11085                         .addProperty(
11086                                 new StringPropertyConfig.Builder("age")
11087                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
11088                                         .setIndexingType(
11089                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
11090                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
11091                                         .build())
11092                         .addProperty(
11093                                 new AppSearchSchema.DocumentPropertyConfig.Builder(
11094                                                 "worksFor", "Organization")
11095                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
11096                                         .setShouldIndexNestedProperties(false)
11097                                         .addIndexableNestedProperties(Arrays.asList("name", "id"))
11098                                         .build())
11099                         .build();
11100         AppSearchSchema organizationSchema =
11101                 new AppSearchSchema.Builder("Organization")
11102                         .addProperty(
11103                                 new StringPropertyConfig.Builder("name")
11104                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
11105                                         .setIndexingType(
11106                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
11107                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
11108                                         .build())
11109                         .addProperty(
11110                                 new StringPropertyConfig.Builder("notes")
11111                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
11112                                         .setIndexingType(
11113                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
11114                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
11115                                         .build())
11116                         .addProperty(
11117                                 new StringPropertyConfig.Builder("id")
11118                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
11119                                         .setIndexingType(
11120                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
11121                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
11122                                         .build())
11123                         .build();
11124 
11125         mDb1.setSchemaAsync(
11126                         new SetSchemaRequest.Builder()
11127                                 .addSchemas(emailSchema, personSchema, organizationSchema)
11128                                 .build())
11129                 .get();
11130 
11131         // Test that Email and Person's nested properties are indexed correctly.
11132         GenericDocument org1 =
11133                 new GenericDocument.Builder<>("namespace", "org1", "Organization")
11134                         .setPropertyString("name", "Org1")
11135                         .setPropertyString("notes", "Some notes")
11136                         .setPropertyString("id", "1234")
11137                         .build();
11138         GenericDocument person1 =
11139                 new GenericDocument.Builder<>("namespace", "person1", "Person")
11140                         .setPropertyString("name", "Jane")
11141                         .setPropertyString("age", "20")
11142                         .setPropertyDocument("worksFor", org1)
11143                         .build();
11144         GenericDocument person2 =
11145                 new GenericDocument.Builder<>("namespace", "person2", "Person")
11146                         .setPropertyString("name", "John")
11147                         .setPropertyString("age", "30")
11148                         .setPropertyDocument("worksFor", org1)
11149                         .build();
11150         GenericDocument email1 =
11151                 new GenericDocument.Builder<>("namespace", "email1", "Email")
11152                         .setPropertyString("subject", "Greetings!")
11153                         .setPropertyDocument("sender", person1)
11154                         .setPropertyDocument("recipient", person2)
11155                         .build();
11156         AppSearchBatchResult<String, Void> putResult =
11157                 checkIsBatchResultSuccess(
11158                         mDb1.putAsync(
11159                                 new PutDocumentsRequest.Builder()
11160                                         .addGenericDocuments(person1, org1, person2, email1)
11161                                         .build()));
11162         assertThat(putResult.getSuccesses())
11163                 .containsExactly("person1", null, "org1", null, "person2", null, "email1", null);
11164         assertThat(putResult.getFailures()).isEmpty();
11165 
11166         GetByDocumentIdRequest getByDocumentIdRequest =
11167                 new GetByDocumentIdRequest.Builder("namespace")
11168                         .addIds("person1", "org1", "person2", "email1")
11169                         .build();
11170         List<GenericDocument> outDocuments = doGet(mDb1, getByDocumentIdRequest);
11171         assertThat(outDocuments).hasSize(4);
11172         assertThat(outDocuments).containsExactly(person1, org1, person2, email1);
11173 
11174         // Indexed properties:
11175         // Email: 'subject', 'sender.name', 'sender.worksFor.name', 'sender.worksFor.notes',
11176         //        'recipient.name', 'recipient.age', 'recipient.worksFor.name',
11177         //        'recipient.worksFor.id'
11178         //        (Email:recipient sets index_nested_props=true, so it follows the same indexing
11179         //         configs as the next schema-type level (person))
11180         // Person: 'name', 'age', 'worksFor.name', 'worksFor.id'
11181         // Organization: 'name', 'notes', 'id'
11182         //
11183         // All documents should be returned for query 'Org1' because all schemaTypes index the
11184         // 'Organization:name' property.
11185         SearchResultsShim searchResults =
11186                 mDb1.search(
11187                         "Org1",
11188                         new SearchSpec.Builder()
11189                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
11190                                 .build());
11191         outDocuments = convertSearchResultsToDocuments(searchResults);
11192         assertThat(outDocuments).hasSize(4);
11193         assertThat(outDocuments).containsExactly(person1, org1, person2, email1);
11194 
11195         // org1 and email1 should be returned for query 'notes'
11196         searchResults =
11197                 mDb1.search(
11198                         "notes",
11199                         new SearchSpec.Builder()
11200                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
11201                                 .build());
11202         outDocuments = convertSearchResultsToDocuments(searchResults);
11203         assertThat(outDocuments).hasSize(2);
11204         assertThat(outDocuments).containsExactly(org1, email1);
11205 
11206         // all docs should be returned for query "1234"
11207         searchResults =
11208                 mDb1.search(
11209                         "1234",
11210                         new SearchSpec.Builder()
11211                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
11212                                 .build());
11213         outDocuments = convertSearchResultsToDocuments(searchResults);
11214         assertThat(outDocuments).hasSize(4);
11215         assertThat(outDocuments).containsExactly(person1, org1, person2, email1);
11216 
11217         // email1 should be returned for query "30", but not for "20" since sender.age is not
11218         // indexed, but recipient.age is.
11219         // For query "30", person2 should also be returned
11220         // For query "20, person1 should be returned.
11221         searchResults =
11222                 mDb1.search(
11223                         "30",
11224                         new SearchSpec.Builder()
11225                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
11226                                 .build());
11227         outDocuments = convertSearchResultsToDocuments(searchResults);
11228         assertThat(outDocuments).hasSize(2);
11229         assertThat(outDocuments).containsExactly(person2, email1);
11230 
11231         searchResults =
11232                 mDb1.search(
11233                         "20",
11234                         new SearchSpec.Builder()
11235                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
11236                                 .build());
11237         outDocuments = convertSearchResultsToDocuments(searchResults);
11238         assertThat(outDocuments).hasSize(1);
11239         assertThat(outDocuments).containsExactly(person1);
11240     }
11241 
11242     @Test
testSetSchema_indexableNestedPropsList_circularRefs()11243     public void testSetSchema_indexableNestedPropsList_circularRefs() throws Exception {
11244         assumeTrue(
11245                 mDb1.getFeatures()
11246                         .isFeatureSupported(Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES));
11247         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SET_SCHEMA_CIRCULAR_REFERENCES));
11248 
11249         // Create schema with valid cycle: Person -> Organization -> Person...
11250         AppSearchSchema personSchema =
11251                 new AppSearchSchema.Builder("Person")
11252                         .addProperty(
11253                                 new StringPropertyConfig.Builder("name")
11254                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
11255                                         .setIndexingType(
11256                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
11257                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
11258                                         .build())
11259                         .addProperty(
11260                                 new StringPropertyConfig.Builder("address")
11261                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
11262                                         .setIndexingType(
11263                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
11264                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
11265                                         .build())
11266                         .addProperty(
11267                                 new AppSearchSchema.DocumentPropertyConfig.Builder(
11268                                                 "worksFor", "Organization")
11269                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
11270                                         .setShouldIndexNestedProperties(false)
11271                                         .addIndexableNestedProperties(
11272                                                 Arrays.asList("name", "notes", "funder.name"))
11273                                         .build())
11274                         .build();
11275         AppSearchSchema organizationSchema =
11276                 new AppSearchSchema.Builder("Organization")
11277                         .addProperty(
11278                                 new StringPropertyConfig.Builder("name")
11279                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
11280                                         .setIndexingType(
11281                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
11282                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
11283                                         .build())
11284                         .addProperty(
11285                                 new StringPropertyConfig.Builder("notes")
11286                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
11287                                         .setIndexingType(
11288                                                 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
11289                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
11290                                         .build())
11291                         .addProperty(
11292                                 new AppSearchSchema.DocumentPropertyConfig.Builder(
11293                                                 "funder", "Person")
11294                                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
11295                                         .setShouldIndexNestedProperties(false)
11296                                         .addIndexableNestedProperties(
11297                                                 Arrays.asList(
11298                                                         "name",
11299                                                         "worksFor.name",
11300                                                         "worksFor.funder.address",
11301                                                         "worksFor.funder.worksFor.notes"))
11302                                         .build())
11303                         .build();
11304         mDb1.setSchemaAsync(
11305                         new SetSchemaRequest.Builder()
11306                                 .addSchemas(personSchema, organizationSchema)
11307                                 .build())
11308                 .get();
11309 
11310         // Test that documents following the circular schema are indexed correctly, and that its
11311         // sections are searchable
11312         GenericDocument person1 =
11313                 new GenericDocument.Builder<>("namespace", "person1", "Person")
11314                         .setPropertyString("name", "Person1")
11315                         .setPropertyString("address", "someAddress")
11316                         .build();
11317         GenericDocument org1 =
11318                 new GenericDocument.Builder<>("namespace", "org1", "Organization")
11319                         .setPropertyString("name", "Org1")
11320                         .setPropertyString("notes", "someNote")
11321                         .setPropertyDocument("funder", person1)
11322                         .build();
11323         GenericDocument person2 =
11324                 new GenericDocument.Builder<>("namespace", "person2", "Person")
11325                         .setPropertyString("name", "Person2")
11326                         .setPropertyString("address", "anotherAddress")
11327                         .setPropertyDocument("worksFor", org1)
11328                         .build();
11329         GenericDocument org2 =
11330                 new GenericDocument.Builder<>("namespace", "org2", "Organization")
11331                         .setPropertyString("name", "Org2")
11332                         .setPropertyString("notes", "anotherNote")
11333                         .setPropertyDocument("funder", person2)
11334                         .build();
11335 
11336         AppSearchBatchResult<String, Void> putResult =
11337                 checkIsBatchResultSuccess(
11338                         mDb1.putAsync(
11339                                 new PutDocumentsRequest.Builder()
11340                                         .addGenericDocuments(person1, org1, person2, org2)
11341                                         .build()));
11342         assertThat(putResult.getSuccesses())
11343                 .containsExactly("person1", null, "org1", null, "person2", null, "org2", null);
11344         assertThat(putResult.getFailures()).isEmpty();
11345 
11346         GetByDocumentIdRequest getByDocumentIdRequest =
11347                 new GetByDocumentIdRequest.Builder("namespace")
11348                         .addIds("person1", "person2", "org1", "org2")
11349                         .build();
11350         List<GenericDocument> outDocuments = doGet(mDb1, getByDocumentIdRequest);
11351         assertThat(outDocuments).hasSize(4);
11352         assertThat(outDocuments).containsExactly(person1, person2, org1, org2);
11353 
11354         // Indexed properties:
11355         // Person: 'name', 'address', 'worksFor.name', 'worksFor.notes', 'worksFor.funder.name'
11356         // Organization: 'name', 'notes', 'funder.name', 'funder.worksFor.name',
11357         //               'funder.worksFor.funder.address', 'funder.worksFor.funder.worksFor.notes'
11358         //
11359         // "Person1" should match person1 (name), org1 (funder.name) and person2
11360         // (worksFor.funder.name)
11361         SearchResultsShim searchResults =
11362                 mDb1.search(
11363                         "Person1",
11364                         new SearchSpec.Builder()
11365                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
11366                                 .build());
11367         outDocuments = convertSearchResultsToDocuments(searchResults);
11368         assertThat(outDocuments).hasSize(3);
11369         assertThat(outDocuments).containsExactly(person1, org1, person2);
11370 
11371         // "someAddress" should match person1 (address) and org2 (funder.worksFor.funder.address)
11372         searchResults =
11373                 mDb1.search(
11374                         "someAddress",
11375                         new SearchSpec.Builder()
11376                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
11377                                 .build());
11378         outDocuments = convertSearchResultsToDocuments(searchResults);
11379         assertThat(outDocuments).hasSize(2);
11380         assertThat(outDocuments).containsExactly(person1, org2);
11381 
11382         // "Org1" should match org1 (name), person2 (worksFor.name) and org2 (funder.worksFor.name)
11383         searchResults =
11384                 mDb1.search(
11385                         "Org1",
11386                         new SearchSpec.Builder()
11387                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
11388                                 .build());
11389         outDocuments = convertSearchResultsToDocuments(searchResults);
11390         assertThat(outDocuments).hasSize(3);
11391         assertThat(outDocuments).containsExactly(org1, person2, org2);
11392 
11393         // "someNote" should match org1 (notes) and person2 (worksFor.notes)
11394         searchResults =
11395                 mDb1.search(
11396                         "someNote",
11397                         new SearchSpec.Builder()
11398                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
11399                                 .build());
11400         outDocuments = convertSearchResultsToDocuments(searchResults);
11401         assertThat(outDocuments).hasSize(2);
11402         assertThat(outDocuments).containsExactly(org1, person2);
11403 
11404         // "Person2" should match person2 (name), org2 (funder.name)
11405         searchResults =
11406                 mDb1.search(
11407                         "Person2",
11408                         new SearchSpec.Builder()
11409                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
11410                                 .build());
11411         outDocuments = convertSearchResultsToDocuments(searchResults);
11412         assertThat(outDocuments).hasSize(2);
11413         assertThat(outDocuments).containsExactly(person2, org2);
11414 
11415         // "anotherAddress" should match only person2 (address)
11416         searchResults =
11417                 mDb1.search(
11418                         "anotherAddress",
11419                         new SearchSpec.Builder()
11420                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
11421                                 .build());
11422         outDocuments = convertSearchResultsToDocuments(searchResults);
11423         assertThat(outDocuments).hasSize(1);
11424         assertThat(outDocuments).containsExactly(person2);
11425 
11426         // "Org2" and "anotherNote" should both match only org2 (name, notes)
11427         searchResults =
11428                 mDb1.search(
11429                         "Org2",
11430                         new SearchSpec.Builder()
11431                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
11432                                 .build());
11433         outDocuments = convertSearchResultsToDocuments(searchResults);
11434         assertThat(outDocuments).hasSize(1);
11435         assertThat(outDocuments).containsExactly(org2);
11436 
11437         searchResults =
11438                 mDb1.search(
11439                         "anotherNote",
11440                         new SearchSpec.Builder()
11441                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
11442                                 .build());
11443         outDocuments = convertSearchResultsToDocuments(searchResults);
11444         assertThat(outDocuments).hasSize(1);
11445         assertThat(outDocuments).containsExactly(org2);
11446     }
11447 
11448     @Test
testSetSchema_toString_containsIndexableNestedPropsList()11449     public void testSetSchema_toString_containsIndexableNestedPropsList() throws Exception {
11450         assumeTrue(
11451                 mDb1.getFeatures()
11452                         .isFeatureSupported(Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES));
11453 
11454         AppSearchSchema emailSchema =
11455                 new AppSearchSchema.Builder("Email")
11456                         .addProperty(
11457                                 new StringPropertyConfig.Builder("subject")
11458                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
11459                                         .setIndexingType(
11460                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
11461                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
11462                                         .build())
11463                         .addProperty(
11464                                 new AppSearchSchema.DocumentPropertyConfig.Builder(
11465                                                 "sender", "Person")
11466                                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
11467                                         .setShouldIndexNestedProperties(false)
11468                                         .addIndexableNestedProperties(
11469                                                 Arrays.asList(
11470                                                         "name", "worksFor.name", "worksFor.notes"))
11471                                         .build())
11472                         .addProperty(
11473                                 new AppSearchSchema.DocumentPropertyConfig.Builder(
11474                                                 "recipient", "Person")
11475                                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
11476                                         .setShouldIndexNestedProperties(true)
11477                                         .build())
11478                         .build();
11479         String expectedIndexableNestedPropertyMessage =
11480                 "indexableNestedProperties: [name, worksFor.notes, worksFor.name]";
11481 
11482         assertThat(emailSchema.toString()).contains(expectedIndexableNestedPropertyMessage);
11483     }
11484 
11485     @Test
11486     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
testEmbeddingSearch_simple()11487     public void testEmbeddingSearch_simple() throws Exception {
11488         assumeTrue(
11489                 mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG));
11490 
11491         // Schema registration
11492         AppSearchSchema schema =
11493                 new AppSearchSchema.Builder("Email")
11494                         .addProperty(
11495                                 new StringPropertyConfig.Builder("body")
11496                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
11497                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
11498                                         .setIndexingType(
11499                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
11500                                         .build())
11501                         .addProperty(
11502                                 new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding1")
11503                                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
11504                                         .setIndexingType(
11505                                                 AppSearchSchema.EmbeddingPropertyConfig
11506                                                         .INDEXING_TYPE_SIMILARITY)
11507                                         .build())
11508                         .addProperty(
11509                                 new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding2")
11510                                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
11511                                         .setIndexingType(
11512                                                 AppSearchSchema.EmbeddingPropertyConfig
11513                                                         .INDEXING_TYPE_SIMILARITY)
11514                                         .build())
11515                         .build();
11516         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
11517 
11518         // Index documents
11519         GenericDocument doc0 =
11520                 new GenericDocument.Builder<>("namespace", "id0", "Email")
11521                         .setPropertyString("body", "foo")
11522                         .setCreationTimestampMillis(1000)
11523                         .setPropertyEmbedding(
11524                                 "embedding1",
11525                                 new EmbeddingVector(
11526                                         new float[] {0.1f, 0.2f, 0.3f, 0.4f, 0.5f}, "my_model_v1"))
11527                         .setPropertyEmbedding(
11528                                 "embedding2",
11529                                 new EmbeddingVector(
11530                                         new float[] {-0.1f, -0.2f, -0.3f, 0.4f, 0.5f},
11531                                         "my_model_v1"),
11532                                 new EmbeddingVector(new float[] {0.6f, 0.7f, 0.8f}, "my_model_v2"))
11533                         .build();
11534         GenericDocument doc1 =
11535                 new GenericDocument.Builder<>("namespace", "id1", "Email")
11536                         .setPropertyString("body", "bar")
11537                         .setCreationTimestampMillis(1000)
11538                         .setPropertyEmbedding(
11539                                 "embedding1",
11540                                 new EmbeddingVector(
11541                                         new float[] {-0.1f, 0.2f, -0.3f, -0.4f, 0.5f},
11542                                         "my_model_v1"))
11543                         .setPropertyEmbedding(
11544                                 "embedding2",
11545                                 new EmbeddingVector(new float[] {0.6f, 0.7f, -0.8f}, "my_model_v2"))
11546                         .build();
11547         checkIsBatchResultSuccess(
11548                 mDb1.putAsync(
11549                         new PutDocumentsRequest.Builder().addGenericDocuments(doc0, doc1).build()));
11550 
11551         // Add an embedding search with dot product semantic scores:
11552         // - document 0: -0.5 (embedding1), 0.3 (embedding2)
11553         // - document 1: -0.9 (embedding1)
11554         EmbeddingVector searchEmbedding =
11555                 new EmbeddingVector(new float[] {1, -1, -1, 1, -1}, "my_model_v1");
11556 
11557         // Match documents that have embeddings with a similarity closer to 0 that is
11558         // greater than -1.
11559         //
11560         // The matched embeddings for each doc are:
11561         // - document 0: -0.5 (embedding1), 0.3 (embedding2)
11562         // - document 1: -0.9 (embedding1)
11563         // The scoring expression for each doc will be evaluated as:
11564         // - document 0: sum({-0.5, 0.3}) + sum({}) = -0.2
11565         // - document 1: sum({-0.9}) + sum({}) = -0.9
11566         SearchSpec searchSpec =
11567                 new SearchSpec.Builder()
11568                         .setDefaultEmbeddingSearchMetricType(
11569                                 SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT)
11570                         .addEmbeddingParameters(searchEmbedding)
11571                         .setRankingStrategy(
11572                                 "sum(this.matchedSemanticScores(getEmbeddingParameter(0)))")
11573                         .setListFilterQueryLanguageEnabled(true)
11574                         .build();
11575         SearchResultsShim searchResults =
11576                 mDb1.search("semanticSearch(getEmbeddingParameter(0), -1, 1)", searchSpec);
11577         List<SearchResult> results = retrieveAllSearchResults(searchResults);
11578         assertThat(results).hasSize(2);
11579         assertThat(results.get(0).getGenericDocument()).isEqualTo(doc0);
11580         assertThat(results.get(0).getRankingSignal()).isWithin(0.00001).of(-0.2);
11581         assertThat(results.get(1).getGenericDocument()).isEqualTo(doc1);
11582         assertThat(results.get(1).getRankingSignal()).isWithin(0.00001).of(-0.9);
11583     }
11584 
11585     @Test
11586     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
testEmbeddingSearch_propertyRestriction()11587     public void testEmbeddingSearch_propertyRestriction() throws Exception {
11588         assumeTrue(
11589                 mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG));
11590 
11591         // Schema registration
11592         AppSearchSchema schema =
11593                 new AppSearchSchema.Builder("Email")
11594                         .addProperty(
11595                                 new StringPropertyConfig.Builder("body")
11596                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
11597                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
11598                                         .setIndexingType(
11599                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
11600                                         .build())
11601                         .addProperty(
11602                                 new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding1")
11603                                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
11604                                         .setIndexingType(
11605                                                 AppSearchSchema.EmbeddingPropertyConfig
11606                                                         .INDEXING_TYPE_SIMILARITY)
11607                                         .build())
11608                         .addProperty(
11609                                 new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding2")
11610                                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
11611                                         .setIndexingType(
11612                                                 AppSearchSchema.EmbeddingPropertyConfig
11613                                                         .INDEXING_TYPE_SIMILARITY)
11614                                         .build())
11615                         .build();
11616         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
11617 
11618         // Index documents
11619         GenericDocument doc0 =
11620                 new GenericDocument.Builder<>("namespace", "id0", "Email")
11621                         .setPropertyString("body", "foo")
11622                         .setCreationTimestampMillis(1000)
11623                         .setPropertyEmbedding(
11624                                 "embedding1",
11625                                 new EmbeddingVector(
11626                                         new float[] {0.1f, 0.2f, 0.3f, 0.4f, 0.5f}, "my_model_v1"))
11627                         .setPropertyEmbedding(
11628                                 "embedding2",
11629                                 new EmbeddingVector(
11630                                         new float[] {-0.1f, -0.2f, -0.3f, 0.4f, 0.5f},
11631                                         "my_model_v1"),
11632                                 new EmbeddingVector(new float[] {0.6f, 0.7f, 0.8f}, "my_model_v2"))
11633                         .build();
11634         GenericDocument doc1 =
11635                 new GenericDocument.Builder<>("namespace", "id1", "Email")
11636                         .setPropertyString("body", "bar")
11637                         .setCreationTimestampMillis(1000)
11638                         .setPropertyEmbedding(
11639                                 "embedding1",
11640                                 new EmbeddingVector(
11641                                         new float[] {-0.1f, 0.2f, -0.3f, -0.4f, 0.5f},
11642                                         "my_model_v1"))
11643                         .setPropertyEmbedding(
11644                                 "embedding2",
11645                                 new EmbeddingVector(new float[] {0.6f, 0.7f, -0.8f}, "my_model_v2"))
11646                         .build();
11647         checkIsBatchResultSuccess(
11648                 mDb1.putAsync(
11649                         new PutDocumentsRequest.Builder().addGenericDocuments(doc0, doc1).build()));
11650 
11651         // Add an embedding search with dot product semantic scores:
11652         // - document 0: -0.5 (embedding1), 0.3 (embedding2)
11653         // - document 1: -0.9 (embedding1)
11654         EmbeddingVector searchEmbedding =
11655                 new EmbeddingVector(new float[] {1, -1, -1, 1, -1}, "my_model_v1");
11656 
11657         // Create a query similar as above but with a property restriction, which still matches
11658         // document 0 and document 1 but the semantic score 0.3 should be removed from document 0.
11659         //
11660         // The matched embeddings for each doc are:
11661         // - document 0: -0.5 (embedding1)
11662         // - document 1: -0.9 (embedding1)
11663         // The scoring expression for each doc will be evaluated as:
11664         // - document 0: sum({-0.5}) = -0.5
11665         // - document 1: sum({-0.9}) = -0.9
11666         SearchSpec searchSpec =
11667                 new SearchSpec.Builder()
11668                         .setDefaultEmbeddingSearchMetricType(
11669                                 SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT)
11670                         .addEmbeddingParameters(searchEmbedding)
11671                         .setRankingStrategy(
11672                                 "sum(this.matchedSemanticScores(getEmbeddingParameter(0)))")
11673                         .setListFilterQueryLanguageEnabled(true)
11674                         .build();
11675         SearchResultsShim searchResults =
11676                 mDb1.search(
11677                         "embedding1:semanticSearch(getEmbeddingParameter(0), -1, 1)", searchSpec);
11678         List<SearchResult> results = retrieveAllSearchResults(searchResults);
11679         assertThat(results).hasSize(2);
11680         assertThat(results.get(0).getGenericDocument()).isEqualTo(doc0);
11681         assertThat(results.get(0).getRankingSignal()).isWithin(0.00001).of(-0.5);
11682         assertThat(results.get(1).getGenericDocument()).isEqualTo(doc1);
11683         assertThat(results.get(1).getRankingSignal()).isWithin(0.00001).of(-0.9);
11684     }
11685 
11686     @Test
11687     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
testEmbeddingSearch_multipleSearchEmbeddings()11688     public void testEmbeddingSearch_multipleSearchEmbeddings() throws Exception {
11689         assumeTrue(
11690                 mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG));
11691 
11692         // Schema registration
11693         AppSearchSchema schema =
11694                 new AppSearchSchema.Builder("Email")
11695                         .addProperty(
11696                                 new StringPropertyConfig.Builder("body")
11697                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
11698                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
11699                                         .setIndexingType(
11700                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
11701                                         .build())
11702                         .addProperty(
11703                                 new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding1")
11704                                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
11705                                         .setIndexingType(
11706                                                 AppSearchSchema.EmbeddingPropertyConfig
11707                                                         .INDEXING_TYPE_SIMILARITY)
11708                                         .build())
11709                         .addProperty(
11710                                 new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding2")
11711                                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
11712                                         .setIndexingType(
11713                                                 AppSearchSchema.EmbeddingPropertyConfig
11714                                                         .INDEXING_TYPE_SIMILARITY)
11715                                         .build())
11716                         .build();
11717         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
11718 
11719         // Index documents
11720         GenericDocument doc0 =
11721                 new GenericDocument.Builder<>("namespace", "id0", "Email")
11722                         .setPropertyString("body", "foo")
11723                         .setCreationTimestampMillis(1000)
11724                         .setPropertyEmbedding(
11725                                 "embedding1",
11726                                 new EmbeddingVector(
11727                                         new float[] {0.1f, 0.2f, 0.3f, 0.4f, 0.5f}, "my_model_v1"))
11728                         .setPropertyEmbedding(
11729                                 "embedding2",
11730                                 new EmbeddingVector(
11731                                         new float[] {-0.1f, -0.2f, -0.3f, 0.4f, 0.5f},
11732                                         "my_model_v1"),
11733                                 new EmbeddingVector(new float[] {0.6f, 0.7f, 0.8f}, "my_model_v2"))
11734                         .build();
11735         GenericDocument doc1 =
11736                 new GenericDocument.Builder<>("namespace", "id1", "Email")
11737                         .setPropertyString("body", "bar")
11738                         .setCreationTimestampMillis(1000)
11739                         .setPropertyEmbedding(
11740                                 "embedding1",
11741                                 new EmbeddingVector(
11742                                         new float[] {-0.1f, 0.2f, -0.3f, -0.4f, 0.5f},
11743                                         "my_model_v1"))
11744                         .setPropertyEmbedding(
11745                                 "embedding2",
11746                                 new EmbeddingVector(new float[] {0.6f, 0.7f, -0.8f}, "my_model_v2"))
11747                         .build();
11748         checkIsBatchResultSuccess(
11749                 mDb1.putAsync(
11750                         new PutDocumentsRequest.Builder().addGenericDocuments(doc0, doc1).build()));
11751 
11752         // Add an embedding search with dot product semantic scores:
11753         // - document 0: -0.5 (embedding1), 0.3 (embedding2)
11754         // - document 1: -0.9 (embedding1)
11755         EmbeddingVector searchEmbedding1 =
11756                 new EmbeddingVector(new float[] {1, -1, -1, 1, -1}, "my_model_v1");
11757         // Add an embedding search with dot product semantic scores:
11758         // - document 0: -0.5 (embedding2)
11759         // - document 1: -2.1 (embedding2)
11760         EmbeddingVector searchEmbedding2 =
11761                 new EmbeddingVector(new float[] {-1, -1, 1}, "my_model_v2");
11762 
11763         // Create a complex query that matches all hits from all documents.
11764         //
11765         // The matched embeddings for each doc are:
11766         // - document 0: -0.5 (embedding1), 0.3 (embedding2), -0.5 (embedding2)
11767         // - document 1: -0.9 (embedding1), -2.1 (embedding2)
11768         // The scoring expression for each doc will be evaluated as:
11769         // - document 0: sum({-0.5, 0.3}) + sum({-0.5}) = -0.7
11770         // - document 1: sum({-0.9}) + sum({-2.1}) = -3
11771         SearchSpec searchSpec =
11772                 new SearchSpec.Builder()
11773                         .setDefaultEmbeddingSearchMetricType(
11774                                 SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT)
11775                         .addEmbeddingParameters(searchEmbedding1, searchEmbedding2)
11776                         .setRankingStrategy(
11777                                 "sum(this.matchedSemanticScores(getEmbeddingParameter(0))) + "
11778                                     + "sum(this.matchedSemanticScores(getEmbeddingParameter(1)))")
11779                         .setListFilterQueryLanguageEnabled(true)
11780                         .build();
11781         SearchResultsShim searchResults =
11782                 mDb1.search(
11783                         "semanticSearch(getEmbeddingParameter(0)) OR "
11784                                 + "semanticSearch(getEmbeddingParameter(1))",
11785                         searchSpec);
11786         List<SearchResult> results = retrieveAllSearchResults(searchResults);
11787         assertThat(results).hasSize(2);
11788         assertThat(results.get(0).getGenericDocument()).isEqualTo(doc0);
11789         assertThat(results.get(0).getRankingSignal()).isWithin(0.00001).of(-0.7);
11790         assertThat(results.get(1).getGenericDocument()).isEqualTo(doc1);
11791         assertThat(results.get(1).getRankingSignal()).isWithin(0.00001).of(-3);
11792     }
11793 
11794     @Test
11795     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
testEmbeddingSearch_hybrid()11796     public void testEmbeddingSearch_hybrid() throws Exception {
11797         assumeTrue(
11798                 mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG));
11799 
11800         // Schema registration
11801         AppSearchSchema schema =
11802                 new AppSearchSchema.Builder("Email")
11803                         .addProperty(
11804                                 new StringPropertyConfig.Builder("body")
11805                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
11806                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
11807                                         .setIndexingType(
11808                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
11809                                         .build())
11810                         .addProperty(
11811                                 new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding1")
11812                                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
11813                                         .setIndexingType(
11814                                                 AppSearchSchema.EmbeddingPropertyConfig
11815                                                         .INDEXING_TYPE_SIMILARITY)
11816                                         .build())
11817                         .addProperty(
11818                                 new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding2")
11819                                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
11820                                         .setIndexingType(
11821                                                 AppSearchSchema.EmbeddingPropertyConfig
11822                                                         .INDEXING_TYPE_SIMILARITY)
11823                                         .build())
11824                         .build();
11825         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
11826 
11827         // Index documents
11828         GenericDocument doc0 =
11829                 new GenericDocument.Builder<>("namespace", "id0", "Email")
11830                         .setPropertyString("body", "foo")
11831                         .setCreationTimestampMillis(1000)
11832                         .setPropertyEmbedding(
11833                                 "embedding1",
11834                                 new EmbeddingVector(
11835                                         new float[] {0.1f, 0.2f, 0.3f, 0.4f, 0.5f}, "my_model_v1"))
11836                         .setPropertyEmbedding(
11837                                 "embedding2",
11838                                 new EmbeddingVector(
11839                                         new float[] {-0.1f, -0.2f, -0.3f, 0.4f, 0.5f},
11840                                         "my_model_v1"),
11841                                 new EmbeddingVector(new float[] {0.6f, 0.7f, 0.8f}, "my_model_v2"))
11842                         .build();
11843         GenericDocument doc1 =
11844                 new GenericDocument.Builder<>("namespace", "id1", "Email")
11845                         .setPropertyString("body", "bar")
11846                         .setCreationTimestampMillis(1000)
11847                         .setPropertyEmbedding(
11848                                 "embedding1",
11849                                 new EmbeddingVector(
11850                                         new float[] {-0.1f, 0.2f, -0.3f, -0.4f, 0.5f},
11851                                         "my_model_v1"))
11852                         .setPropertyEmbedding(
11853                                 "embedding2",
11854                                 new EmbeddingVector(new float[] {0.6f, 0.7f, -0.8f}, "my_model_v2"))
11855                         .build();
11856         checkIsBatchResultSuccess(
11857                 mDb1.putAsync(
11858                         new PutDocumentsRequest.Builder().addGenericDocuments(doc0, doc1).build()));
11859 
11860         // Add an embedding search with dot product semantic scores:
11861         // - document 0: -0.5 (embedding2)
11862         // - document 1: -2.1 (embedding2)
11863         EmbeddingVector searchEmbedding =
11864                 new EmbeddingVector(new float[] {-1, -1, 1}, "my_model_v2");
11865 
11866         // Create a hybrid query that matches document 0 because of term-based search
11867         // and document 1 because of embedding-based search.
11868         //
11869         // The matched embeddings for each doc are:
11870         // - document 1: -2.1 (embedding2)
11871         // The scoring expression for each doc will be evaluated as:
11872         // - document 0: sum({}) = 0
11873         // - document 1: sum({-2.1}) = -2.1
11874         SearchSpec searchSpec =
11875                 new SearchSpec.Builder()
11876                         .setDefaultEmbeddingSearchMetricType(
11877                                 SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT)
11878                         .addEmbeddingParameters(searchEmbedding)
11879                         .setRankingStrategy(
11880                                 "sum(this.matchedSemanticScores(getEmbeddingParameter(0)))")
11881                         .setListFilterQueryLanguageEnabled(true)
11882                         .build();
11883         SearchResultsShim searchResults =
11884                 mDb1.search("foo OR semanticSearch(getEmbeddingParameter(0), -10, -1)", searchSpec);
11885         List<SearchResult> results = retrieveAllSearchResults(searchResults);
11886         assertThat(results).hasSize(2);
11887         assertThat(results.get(0).getGenericDocument()).isEqualTo(doc0);
11888         assertThat(results.get(0).getRankingSignal()).isWithin(0.00001).of(0);
11889         assertThat(results.get(1).getGenericDocument()).isEqualTo(doc1);
11890         assertThat(results.get(1).getRankingSignal()).isWithin(0.00001).of(-2.1);
11891     }
11892 
11893     @Test
11894     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
testEmbeddingSearch_notSupported()11895     public void testEmbeddingSearch_notSupported() throws Exception {
11896         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_QUERY_LANGUAGE));
11897         assumeFalse(
11898                 mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG));
11899 
11900         EmbeddingVector searchEmbedding =
11901                 new EmbeddingVector(new float[] {-1, -1, 1}, "my_model_v2");
11902         SearchSpec searchSpec =
11903                 new SearchSpec.Builder()
11904                         .setListFilterQueryLanguageEnabled(true)
11905                         .addEmbeddingParameters(searchEmbedding)
11906                         .build();
11907         SearchResultsShim results =
11908                 mDb1.search("semanticSearch(getEmbeddingParameter(0), -1, 1)", searchSpec);
11909         UnsupportedOperationException exception =
11910                 assertThrows(
11911                         UnsupportedOperationException.class,
11912                         () -> results.getNextPageAsync().get());
11913         assertThat(exception)
11914                 .hasMessageThat()
11915                 .contains(
11916                         Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG
11917                                 + " is not available on this AppSearch implementation.");
11918     }
11919 
11920     @Test
11921     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SEARCH_SPEC_SEARCH_STRING_PARAMETERS)
testSearchSpecStrings_simple()11922     public void testSearchSpecStrings_simple() throws Exception {
11923         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_QUERY_LANGUAGE));
11924         assumeTrue(
11925                 mDb1.getFeatures()
11926                         .isFeatureSupported(Features.SEARCH_SPEC_SEARCH_STRING_PARAMETERS));
11927 
11928         // Schema registration
11929         AppSearchSchema schema =
11930                 new AppSearchSchema.Builder("Email")
11931                         .addProperty(
11932                                 new StringPropertyConfig.Builder("body")
11933                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
11934                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
11935                                         .setIndexingType(
11936                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
11937                                         .build())
11938                         .build();
11939         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
11940 
11941         // Index documents
11942         GenericDocument doc0 =
11943                 new GenericDocument.Builder<>("namespace", "id0", "Email")
11944                         .setPropertyString("body", "foo bar")
11945                         .setCreationTimestampMillis(1000)
11946                         .build();
11947         GenericDocument doc1 =
11948                 new GenericDocument.Builder<>("namespace", "id1", "Email")
11949                         .setPropertyString("body", "bar")
11950                         .setCreationTimestampMillis(1000)
11951                         .build();
11952         GenericDocument doc2 =
11953                 new GenericDocument.Builder<>("namespace", "id2", "Email")
11954                         .setPropertyString("body", "foo")
11955                         .setCreationTimestampMillis(1000)
11956                         .build();
11957         checkIsBatchResultSuccess(
11958                 mDb1.putAsync(
11959                         new PutDocumentsRequest.Builder()
11960                                 .addGenericDocuments(doc0, doc1, doc2)
11961                                 .build()));
11962 
11963         SearchSpec searchSpec =
11964                 new SearchSpec.Builder()
11965                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
11966                         .setListFilterQueryLanguageEnabled(true)
11967                         .addSearchStringParameters("foo.")
11968                         .build();
11969         SearchResultsShim searchResults = mDb1.search("getSearchStringParameter(0)", searchSpec);
11970         List<GenericDocument> results = convertSearchResultsToDocuments(searchResults);
11971         assertThat(results).containsExactly(doc2, doc0);
11972 
11973         searchSpec =
11974                 new SearchSpec.Builder()
11975                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
11976                         .setListFilterQueryLanguageEnabled(true)
11977                         .addSearchStringParameters("bar, foo")
11978                         .build();
11979         searchResults = mDb1.search("getSearchStringParameter(0)", searchSpec);
11980         results = convertSearchResultsToDocuments(searchResults);
11981         assertThat(results).containsExactly(doc0);
11982 
11983         searchSpec =
11984                 new SearchSpec.Builder()
11985                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
11986                         .setListFilterQueryLanguageEnabled(true)
11987                         .addSearchStringParameters("\\\"bar, \\\"foo\\\"")
11988                         .build();
11989         searchResults = mDb1.search("getSearchStringParameter(0)", searchSpec);
11990         results = convertSearchResultsToDocuments(searchResults);
11991         assertThat(results).containsExactly(doc0);
11992 
11993         searchSpec =
11994                 new SearchSpec.Builder()
11995                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
11996                         .setListFilterQueryLanguageEnabled(true)
11997                         .addSearchStringParameters("bar ) foo")
11998                         .build();
11999         searchResults = mDb1.search("getSearchStringParameter(0)", searchSpec);
12000         results = convertSearchResultsToDocuments(searchResults);
12001         assertThat(results).containsExactly(doc0);
12002 
12003         searchSpec =
12004                 new SearchSpec.Builder()
12005                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
12006                         .setListFilterQueryLanguageEnabled(true)
12007                         .addSearchStringParameters("bar foo(")
12008                         .build();
12009         searchResults = mDb1.search("getSearchStringParameter(0)", searchSpec);
12010         results = convertSearchResultsToDocuments(searchResults);
12011         assertThat(results).containsExactly(doc0);
12012     }
12013 
12014     @Test
12015     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SEARCH_SPEC_SEARCH_STRING_PARAMETERS)
testSearchSpecString_notSupported()12016     public void testSearchSpecString_notSupported() throws Exception {
12017         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_QUERY_LANGUAGE));
12018         assumeFalse(
12019                 mDb1.getFeatures()
12020                         .isFeatureSupported(Features.SEARCH_SPEC_SEARCH_STRING_PARAMETERS));
12021 
12022         SearchSpec searchSpec =
12023                 new SearchSpec.Builder()
12024                         .setListFilterQueryLanguageEnabled(true)
12025                         .addSearchStringParameters("bar foo(")
12026                         .build();
12027         UnsupportedOperationException exception =
12028                 assertThrows(
12029                         UnsupportedOperationException.class,
12030                         () -> mDb1.search("getSearchStringParameter(0)", searchSpec));
12031         assertThat(exception)
12032                 .hasMessageThat()
12033                 .contains(
12034                         Features.SEARCH_SPEC_SEARCH_STRING_PARAMETERS
12035                                 + " is not available on this AppSearch implementation.");
12036     }
12037 
12038     @Test
12039     @RequiresFlagsEnabled({
12040         Flags.FLAG_ENABLE_INFORMATIONAL_RANKING_EXPRESSIONS,
12041         Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG
12042     })
testInformationalRankingExpressions()12043     public void testInformationalRankingExpressions() throws Exception {
12044         assumeTrue(
12045                 mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG));
12046         assumeTrue(
12047                 mDb1.getFeatures()
12048                         .isFeatureSupported(
12049                                 Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS));
12050 
12051         // Schema registration
12052         AppSearchSchema schema =
12053                 new AppSearchSchema.Builder("Email")
12054                         .addProperty(
12055                                 new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding")
12056                                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
12057                                         .setIndexingType(
12058                                                 AppSearchSchema.EmbeddingPropertyConfig
12059                                                         .INDEXING_TYPE_SIMILARITY)
12060                                         .build())
12061                         .build();
12062         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
12063 
12064         // Index documents
12065         final int doc0DocScore = 2;
12066         GenericDocument doc0 =
12067                 new GenericDocument.Builder<>("namespace", "id0", "Email")
12068                         .setScore(doc0DocScore)
12069                         .setCreationTimestampMillis(1000)
12070                         .setPropertyEmbedding(
12071                                 "embedding",
12072                                 new EmbeddingVector(
12073                                         new float[] {-0.1f, -0.2f, -0.3f, -0.4f, -0.5f},
12074                                         "my_model"))
12075                         .build();
12076         final int doc1DocScore = 3;
12077         GenericDocument doc1 =
12078                 new GenericDocument.Builder<>("namespace", "id1", "Email")
12079                         .setScore(doc1DocScore)
12080                         .setCreationTimestampMillis(1000)
12081                         .setPropertyEmbedding(
12082                                 "embedding",
12083                                 new EmbeddingVector(
12084                                         new float[] {-0.1f, 0.2f, -0.3f, -0.4f, 0.5f}, "my_model"),
12085                                 new EmbeddingVector(
12086                                         new float[] {-0.1f, -0.2f, -0.3f, -0.4f, -0.5f},
12087                                         "my_model"))
12088                         .build();
12089         checkIsBatchResultSuccess(
12090                 mDb1.putAsync(
12091                         new PutDocumentsRequest.Builder().addGenericDocuments(doc0, doc1).build()));
12092 
12093         // Add an embedding search with dot product semantic scores:
12094         // - document 0: 0.5
12095         // - document 1: -0.9, 0.5
12096         EmbeddingVector searchEmbedding =
12097                 new EmbeddingVector(new float[] {1, -1, -1, 1, -1}, "my_model");
12098 
12099         // Make an embedding query that matches all documents.
12100         SearchSpec searchSpec =
12101                 new SearchSpec.Builder()
12102                         .setDefaultEmbeddingSearchMetricType(
12103                                 SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT)
12104                         .addEmbeddingParameters(searchEmbedding)
12105                         .setRankingStrategy(
12106                                 "sum(this.matchedSemanticScores(getEmbeddingParameter(0)))")
12107                         .addInformationalRankingExpressions(
12108                                 "len(this.matchedSemanticScores(getEmbeddingParameter(0)))")
12109                         .addInformationalRankingExpressions("this.documentScore()")
12110                         .setListFilterQueryLanguageEnabled(true)
12111                         .build();
12112         SearchResultsShim searchResults =
12113                 mDb1.search("semanticSearch(getEmbeddingParameter(0))", searchSpec);
12114         List<SearchResult> results = retrieveAllSearchResults(searchResults);
12115         assertThat(results).hasSize(2);
12116         // doc0:
12117         assertThat(results.get(0).getGenericDocument()).isEqualTo(doc0);
12118         assertThat(results.get(0).getRankingSignal()).isWithin(0.00001).of(0.5);
12119         // doc0 has 1 embedding vector and a document score of 2.
12120         assertThat(results.get(0).getInformationalRankingSignals())
12121                 .containsExactly(1.0, (double) doc0DocScore)
12122                 .inOrder();
12123 
12124         // doc1:
12125         assertThat(results.get(1).getGenericDocument()).isEqualTo(doc1);
12126         assertThat(results.get(1).getRankingSignal()).isWithin(0.00001).of(-0.9 + 0.5);
12127         // doc1 has 2 embedding vectors and a document score of 3.
12128         assertThat(results.get(1).getInformationalRankingSignals())
12129                 .containsExactly(2.0, (double) doc1DocScore)
12130                 .inOrder();
12131     }
12132 
12133     @Test
12134     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_INFORMATIONAL_RANKING_EXPRESSIONS)
testInformationalRankingExpressions_notSupported()12135     public void testInformationalRankingExpressions_notSupported() throws Exception {
12136         assumeTrue(
12137                 mDb1.getFeatures()
12138                         .isFeatureSupported(Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION));
12139         assumeFalse(
12140                 mDb1.getFeatures()
12141                         .isFeatureSupported(
12142                                 Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS));
12143 
12144         SearchSpec searchSpec =
12145                 new SearchSpec.Builder()
12146                         .setRankingStrategy("this.documentScore() + 1")
12147                         .addInformationalRankingExpressions("this.documentScore()")
12148                         .build();
12149         UnsupportedOperationException exception =
12150                 assertThrows(
12151                         UnsupportedOperationException.class, () -> mDb1.search("foo", searchSpec));
12152         assertThat(exception)
12153                 .hasMessageThat()
12154                 .contains(
12155                         Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS
12156                                 + " are not available on this AppSearch implementation.");
12157     }
12158 
12159     @Test
testPutDocuments_emptyBytesAndDocuments()12160     public void testPutDocuments_emptyBytesAndDocuments() throws Exception {
12161         // Schema registration
12162         AppSearchSchema schema =
12163                 new AppSearchSchema.Builder("testSchema")
12164                         .addProperty(
12165                                 new AppSearchSchema.BytesPropertyConfig.Builder("bytes")
12166                                         .setCardinality(
12167                                                 AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
12168                                         .build())
12169                         .addProperty(
12170                                 new AppSearchSchema.DocumentPropertyConfig.Builder(
12171                                                 "document", AppSearchEmail.SCHEMA_TYPE)
12172                                         .setCardinality(
12173                                                 AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
12174                                         .setShouldIndexNestedProperties(true)
12175                                         .build())
12176                         .build();
12177         mDb1.setSchemaAsync(
12178                         new SetSchemaRequest.Builder()
12179                                 .addSchemas(schema, AppSearchEmail.SCHEMA)
12180                                 .build())
12181                 .get();
12182 
12183         // Index a document
12184         GenericDocument document =
12185                 new GenericDocument.Builder<>("namespace", "id1", "testSchema")
12186                         .setPropertyBytes("bytes")
12187                         .setPropertyDocument("document")
12188                         .build();
12189 
12190         AppSearchBatchResult<String, Void> result =
12191                 checkIsBatchResultSuccess(
12192                         mDb1.putAsync(
12193                                 new PutDocumentsRequest.Builder()
12194                                         .addGenericDocuments(document)
12195                                         .build()));
12196         assertThat(result.getSuccesses()).containsExactly("id1", null);
12197         assertThat(result.getFailures()).isEmpty();
12198 
12199         GetByDocumentIdRequest request =
12200                 new GetByDocumentIdRequest.Builder("namespace").addIds("id1").build();
12201         List<GenericDocument> outDocuments = doGet(mDb1, request);
12202         assertThat(outDocuments).hasSize(1);
12203         GenericDocument outDocument = outDocuments.get(0);
12204         assertThat(outDocument.getPropertyBytesArray("bytes")).isEmpty();
12205         assertThat(outDocument.getPropertyDocumentArray("document")).isEmpty();
12206     }
12207 
12208     @Test
12209     @RequiresFlagsEnabled({
12210         Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG,
12211         Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_QUANTIZATION
12212     })
testEmbeddingQuantization()12213     public void testEmbeddingQuantization() throws Exception {
12214         assumeTrue(
12215                 mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG));
12216         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_QUANTIZATION));
12217 
12218         // Schema registration
12219         AppSearchSchema schema =
12220                 new AppSearchSchema.Builder("Email")
12221                         .addProperty(
12222                                 new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding")
12223                                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
12224                                         .setIndexingType(
12225                                                 AppSearchSchema.EmbeddingPropertyConfig
12226                                                         .INDEXING_TYPE_SIMILARITY)
12227                                         .setQuantizationType(
12228                                                 AppSearchSchema.EmbeddingPropertyConfig
12229                                                         .QUANTIZATION_TYPE_8_BIT)
12230                                         .build())
12231                         .build();
12232         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
12233 
12234         // Index a document
12235         GenericDocument doc =
12236                 new GenericDocument.Builder<>("namespace", "id", "Email")
12237                         .setCreationTimestampMillis(1000)
12238                         // Since quantization is enabled, this vector will be quantized to
12239                         // {0, 1, 255}.
12240                         .setPropertyEmbedding(
12241                                 "embedding",
12242                                 new EmbeddingVector(new float[] {0, 1.45f, 255}, "my_model"))
12243                         .build();
12244         checkIsBatchResultSuccess(
12245                 mDb1.putAsync(new PutDocumentsRequest.Builder().addGenericDocuments(doc).build()));
12246 
12247         // Verify the embedding will be quantized, so that the embedding score would be
12248         // 0 + 1 + 255 = 256, instead of 0 + 1.45 + 255 = 256.45.
12249         SearchSpec searchSpec =
12250                 new SearchSpec.Builder()
12251                         .setDefaultEmbeddingSearchMetricType(
12252                                 SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT)
12253                         .addEmbeddingParameters(
12254                                 new EmbeddingVector(new float[] {1, 1, 1}, "my_model"))
12255                         .setRankingStrategy(
12256                                 "sum(this.matchedSemanticScores(getEmbeddingParameter(0)))")
12257                         .setListFilterQueryLanguageEnabled(true)
12258                         .build();
12259         SearchResultsShim searchResults =
12260                 mDb1.search("semanticSearch(getEmbeddingParameter(0), -1000, 1000)", searchSpec);
12261         List<SearchResult> results = retrieveAllSearchResults(searchResults);
12262         assertThat(results).hasSize(1);
12263         assertThat(results.get(0).getGenericDocument()).isEqualTo(doc);
12264         assertThat(results.get(0).getRankingSignal()).isWithin(0.0001).of(256);
12265     }
12266 
12267     @Test
12268     @RequiresFlagsEnabled({
12269         Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG,
12270         Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_QUANTIZATION
12271     })
testEmbeddingQuantization_changeSchema()12272     public void testEmbeddingQuantization_changeSchema() throws Exception {
12273         assumeTrue(
12274                 mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG));
12275         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_QUANTIZATION));
12276 
12277         // Set Schema with an embedding property for QUANTIZATION_TYPE_NONE
12278         AppSearchSchema schema =
12279                 new AppSearchSchema.Builder("Email")
12280                         .addProperty(
12281                                 new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding")
12282                                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
12283                                         .setIndexingType(
12284                                                 AppSearchSchema.EmbeddingPropertyConfig
12285                                                         .INDEXING_TYPE_SIMILARITY)
12286                                         .setQuantizationType(
12287                                                 AppSearchSchema.EmbeddingPropertyConfig
12288                                                         .QUANTIZATION_TYPE_NONE)
12289                                         .build())
12290                         .build();
12291         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
12292 
12293         // Index a document
12294         GenericDocument doc =
12295                 new GenericDocument.Builder<>("namespace", "id", "Email")
12296                         .setCreationTimestampMillis(1000)
12297                         // Since quantization is enabled, this vector will be quantized to
12298                         // {0, 1, 255}.
12299                         .setPropertyEmbedding(
12300                                 "embedding",
12301                                 new EmbeddingVector(new float[] {0, 1.45f, 255}, "my_model"))
12302                         .build();
12303         checkIsBatchResultSuccess(
12304                 mDb1.putAsync(new PutDocumentsRequest.Builder().addGenericDocuments(doc).build()));
12305 
12306         // Update the embedding property to QUANTIZATION_TYPE_8_BIT
12307         schema =
12308                 new AppSearchSchema.Builder("Email")
12309                         .addProperty(
12310                                 new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding")
12311                                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
12312                                         .setIndexingType(
12313                                                 AppSearchSchema.EmbeddingPropertyConfig
12314                                                         .INDEXING_TYPE_SIMILARITY)
12315                                         .setQuantizationType(
12316                                                 AppSearchSchema.EmbeddingPropertyConfig
12317                                                         .QUANTIZATION_TYPE_8_BIT)
12318                                         .build())
12319                         .build();
12320         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
12321 
12322         // Verify the embedding will be quantized, so that the embedding score would be
12323         // 0 + 1 + 255 = 256, instead of 0 + 1.45 + 255 = 256.45.
12324         SearchSpec searchSpec =
12325                 new SearchSpec.Builder()
12326                         .setDefaultEmbeddingSearchMetricType(
12327                                 SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT)
12328                         .addEmbeddingParameters(
12329                                 new EmbeddingVector(new float[] {1, 1, 1}, "my_model"))
12330                         .setRankingStrategy(
12331                                 "sum(this.matchedSemanticScores(getEmbeddingParameter(0)))")
12332                         .setListFilterQueryLanguageEnabled(true)
12333                         .build();
12334         SearchResultsShim searchResults =
12335                 mDb1.search("semanticSearch(getEmbeddingParameter(0), -1000, 1000)", searchSpec);
12336         List<SearchResult> results = retrieveAllSearchResults(searchResults);
12337         assertThat(results).hasSize(1);
12338         assertThat(results.get(0).getGenericDocument()).isEqualTo(doc);
12339         assertThat(results.get(0).getRankingSignal()).isWithin(0.0001).of(256);
12340 
12341         // Update the embedding property back to QUANTIZATION_TYPE_NONE
12342         schema =
12343                 new AppSearchSchema.Builder("Email")
12344                         .addProperty(
12345                                 new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding")
12346                                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
12347                                         .setIndexingType(
12348                                                 AppSearchSchema.EmbeddingPropertyConfig
12349                                                         .INDEXING_TYPE_SIMILARITY)
12350                                         .setQuantizationType(
12351                                                 AppSearchSchema.EmbeddingPropertyConfig
12352                                                         .QUANTIZATION_TYPE_NONE)
12353                                         .build())
12354                         .build();
12355         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
12356 
12357         // Verify the embedding will not be quantized, so that the embedding score would be
12358         // 0 + 1.45 + 255 = 256.45.
12359         searchResults =
12360                 mDb1.search("semanticSearch(getEmbeddingParameter(0), -1000, 1000)", searchSpec);
12361         results = retrieveAllSearchResults(searchResults);
12362         assertThat(results).hasSize(1);
12363         assertThat(results.get(0).getGenericDocument()).isEqualTo(doc);
12364         assertThat(results.get(0).getRankingSignal()).isWithin(0.0001).of(256.45);
12365     }
12366 
12367     @Test
12368     @RequiresFlagsEnabled({
12369         Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG,
12370         Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_QUANTIZATION
12371     })
testEmbeddingQuantization_notSupported()12372     public void testEmbeddingQuantization_notSupported() throws Exception {
12373         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_QUERY_LANGUAGE));
12374         assumeTrue(
12375                 mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG));
12376         assumeFalse(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_QUANTIZATION));
12377 
12378         AppSearchSchema schema =
12379                 new AppSearchSchema.Builder("Email")
12380                         .addProperty(
12381                                 new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding")
12382                                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
12383                                         .setIndexingType(
12384                                                 AppSearchSchema.EmbeddingPropertyConfig
12385                                                         .INDEXING_TYPE_SIMILARITY)
12386                                         .setQuantizationType(
12387                                                 AppSearchSchema.EmbeddingPropertyConfig
12388                                                         .QUANTIZATION_TYPE_8_BIT)
12389                                         .build())
12390                         .build();
12391 
12392         UnsupportedOperationException e =
12393                 assertThrows(
12394                         UnsupportedOperationException.class,
12395                         () ->
12396                                 mDb1.setSchemaAsync(
12397                                                 new SetSchemaRequest.Builder()
12398                                                         .addSchemas(schema)
12399                                                         .build())
12400                                         .get());
12401         assertThat(e)
12402                 .hasMessageThat()
12403                 .contains(
12404                         Features.SCHEMA_EMBEDDING_QUANTIZATION
12405                                 + " is not available on this AppSearch implementation.");
12406     }
12407 
12408     @Test
12409     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCORABLE_PROPERTY)
testRankWithScorableProperty_getScorablePropertyFunction_notSupported()12410     public void testRankWithScorableProperty_getScorablePropertyFunction_notSupported()
12411             throws Exception {
12412         assumeTrue(
12413                 mDb1.getFeatures()
12414                         .isFeatureSupported(Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION));
12415         assumeFalse(
12416                 mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_SCORABLE_PROPERTY_CONFIG));
12417 
12418         UnsupportedOperationException exception =
12419                 assertThrows(
12420                         UnsupportedOperationException.class,
12421                         () ->
12422                                 mDb1.search(
12423                                         "body",
12424                                         new SearchSpec.Builder()
12425                                                 .setScorablePropertyRankingEnabled(true)
12426                                                 .setRankingStrategy(
12427                                                         "sum(getScorableProperty(\"Gmail\","
12428                                                             + " \"invalid\"))")
12429                                                 .build()));
12430         assertThat(exception)
12431                 .hasMessageThat()
12432                 .contains(
12433                         Features.SCHEMA_SCORABLE_PROPERTY_CONFIG
12434                                 + " is not available on this AppSearch implementation.");
12435     }
12436 
12437     @Test
12438     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCORABLE_PROPERTY)
testRankWithScorableProperty_setScoringEnabledInSchema_notSupported()12439     public void testRankWithScorableProperty_setScoringEnabledInSchema_notSupported()
12440             throws Exception {
12441         assumeTrue(
12442                 mDb1.getFeatures()
12443                         .isFeatureSupported(Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION));
12444         assumeFalse(
12445                 mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_SCORABLE_PROPERTY_CONFIG));
12446         AppSearchSchema schema =
12447                 new AppSearchSchema.Builder("Gmail")
12448                         .addProperty(
12449                                 new BooleanPropertyConfig.Builder("important")
12450                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
12451                                         .setScoringEnabled(true)
12452                                         .build())
12453                         .build();
12454         UnsupportedOperationException exception =
12455                 assertThrows(
12456                         UnsupportedOperationException.class,
12457                         () ->
12458                                 mDb1.setSchemaAsync(
12459                                                 new SetSchemaRequest.Builder()
12460                                                         .addSchemas(schema)
12461                                                         .build())
12462                                         .get());
12463         assertThat(exception)
12464                 .hasMessageThat()
12465                 .contains(
12466                         Features.SCHEMA_SCORABLE_PROPERTY_CONFIG
12467                                 + " is not available on this AppSearch implementation.");
12468     }
12469 
12470     @Test
12471     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCORABLE_PROPERTY)
testRankWithScorableProperty_simple()12472     public void testRankWithScorableProperty_simple() throws Exception {
12473         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_SCORABLE_PROPERTY_CONFIG));
12474         AppSearchSchema schema =
12475                 new AppSearchSchema.Builder("Gmail")
12476                         .addProperty(
12477                                 new StringPropertyConfig.Builder("subject")
12478                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
12479                                         .setIndexingType(
12480                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
12481                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
12482                                         .build())
12483                         .addProperty(
12484                                 new BooleanPropertyConfig.Builder("important")
12485                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
12486                                         .setScoringEnabled(true)
12487                                         .build())
12488                         .build();
12489         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
12490 
12491         GenericDocument doc1 =
12492                 new GenericDocument.Builder<>("namespace", "id1", "Gmail")
12493                         .setPropertyString("subject", "bar")
12494                         .setPropertyBoolean("important", true)
12495                         .setScore(1)
12496                         .build();
12497         int rankingScoreOfDoc1 = 2;
12498         GenericDocument doc2 =
12499                 new GenericDocument.Builder<>("namespace", "id2", "Gmail")
12500                         .setPropertyString("subject", "bar 2")
12501                         .setPropertyBoolean("important", true)
12502                         .setScore(2)
12503                         .build();
12504         int rankingScoreOfDoc2 = 3;
12505         checkIsBatchResultSuccess(
12506                 mDb1.putAsync(
12507                         new PutDocumentsRequest.Builder().addGenericDocuments(doc1, doc2).build()));
12508 
12509         SearchSpec searchSpec =
12510                 new SearchSpec.Builder()
12511                         .setScorablePropertyRankingEnabled(true)
12512                         .setRankingStrategy(
12513                                 "this.documentScore() + sum(getScorableProperty(\"Gmail\","
12514                                     + " \"important\"))")
12515                         .build();
12516 
12517         SearchResultsShim searchResults = mDb1.search("", searchSpec);
12518         List<SearchResult> results = retrieveAllSearchResults(searchResults);
12519         assertThat(results).hasSize(2);
12520         assertThat(results.get(0).getGenericDocument()).isEqualTo(doc2);
12521         assertThat(results.get(0).getRankingSignal()).isWithin(0.00001).of(rankingScoreOfDoc2);
12522         assertThat(results.get(1).getGenericDocument()).isEqualTo(doc1);
12523         assertThat(results.get(1).getRankingSignal()).isWithin(0.00001).of(rankingScoreOfDoc1);
12524     }
12525 
12526     @Test
12527     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCORABLE_PROPERTY)
testRankWithScorableProperty_withNestedSchema()12528     public void testRankWithScorableProperty_withNestedSchema() throws Exception {
12529         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_SCORABLE_PROPERTY_CONFIG));
12530         AppSearchSchema personSchema =
12531                 new AppSearchSchema.Builder("Person")
12532                         .addProperty(
12533                                 new DoublePropertyConfig.Builder("income")
12534                                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
12535                                         .setScoringEnabled(true)
12536                                         .build())
12537                         .addProperty(
12538                                 new LongPropertyConfig.Builder("age")
12539                                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
12540                                         .setScoringEnabled(true)
12541                                         .build())
12542                         .addProperty(
12543                                 new BooleanPropertyConfig.Builder("isStarred")
12544                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
12545                                         .setScoringEnabled(true)
12546                                         .build())
12547                         .build();
12548         AppSearchSchema emailSchema =
12549                 new AppSearchSchema.Builder("Email")
12550                         .addProperty(
12551                                 new DocumentPropertyConfig.Builder("sender", "Person")
12552                                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
12553                                         .build())
12554                         .addProperty(
12555                                 new DocumentPropertyConfig.Builder("recipient", "Person")
12556                                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
12557                                         .build())
12558                         .addProperty(
12559                                 new LongPropertyConfig.Builder("viewTimes")
12560                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
12561                                         .setScoringEnabled(true)
12562                                         .build())
12563                         .build();
12564         mDb1.setSchemaAsync(
12565                         new SetSchemaRequest.Builder()
12566                                 .addSchemas(personSchema, emailSchema)
12567                                 .build())
12568                 .get();
12569 
12570         GenericDocument sender1 =
12571                 new GenericDocument.Builder<>("namespace", "person1", "Person")
12572                         .setPropertyBoolean("isStarred", true)
12573                         .setPropertyDouble("income", 1000, 2000)
12574                         .setPropertyLong("age", 30)
12575                         .build();
12576         GenericDocument sender2 =
12577                 new GenericDocument.Builder<>("namespace", "person2", "Person")
12578                         .setPropertyBoolean("isStarred", true)
12579                         .setPropertyDouble("income", 5000, 3000)
12580                         .setPropertyLong("age", 40)
12581                         .build();
12582         GenericDocument recipient =
12583                 new GenericDocument.Builder<>("namespace", "person2", "Person")
12584                         .setPropertyBoolean("isStarred", true)
12585                         .setPropertyDouble("income", 2000, 3000)
12586                         .setPropertyLong("age", 50)
12587                         .build();
12588 
12589         GenericDocument email =
12590                 new GenericDocument.Builder<>("namespace", "email1", "Email")
12591                         .setPropertyDocument("sender", sender1, sender2)
12592                         .setPropertyDocument("recipient", recipient)
12593                         .setPropertyLong("viewTimes", 10)
12594                         .build();
12595 
12596         // Put the email document to AppSearch and verify its success.
12597         AppSearchBatchResult<String, Void> result =
12598                 checkIsBatchResultSuccess(
12599                         mDb1.putAsync(
12600                                 new PutDocumentsRequest.Builder()
12601                                         .addGenericDocuments(email)
12602                                         .build()));
12603         assertThat(result.getSuccesses()).containsExactly("email1", null);
12604         assertThat(result.getFailures()).isEmpty();
12605 
12606         // Search and ranking with the scorable properties
12607         String rankingStrategy =
12608                 "sum(getScorableProperty(\"Email\", \"viewTimes\")) + "
12609                         + "max(getScorableProperty(\"Email\", \"recipient.age\")) + "
12610                         + "100 * max(getScorableProperty(\"Email\", \"recipient.isStarred\")) + "
12611                         + "5 * sum(getScorableProperty(\"Email\", \"sender.income\"))";
12612         SearchSpec searchSpec =
12613                 new SearchSpec.Builder()
12614                         .setScorablePropertyRankingEnabled(true)
12615                         .setRankingStrategy(rankingStrategy)
12616                         .build();
12617         double expectedScore =
12618                 /* viewTimes= */ 10
12619                         +
12620                         /* age= */ 50
12621                         + 100 * /* isStarred= */ 1
12622                         + 5 * (1000 + 2000 + 5000 + 3000);
12623         SearchResultsShim searchResults = mDb1.search("", searchSpec);
12624         List<SearchResult> results = retrieveAllSearchResults(searchResults);
12625         assertThat(results).hasSize(1);
12626         assertThat(results.get(0).getGenericDocument()).isEqualTo(email);
12627         assertThat(results.get(0).getRankingSignal()).isWithin(0.00001).of(expectedScore);
12628     }
12629 
12630     @Test
12631     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCORABLE_PROPERTY)
testRankWithScorableProperty_updateSchemaByAddScorableProperty()12632     public void testRankWithScorableProperty_updateSchemaByAddScorableProperty() throws Exception {
12633         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_SCORABLE_PROPERTY_CONFIG));
12634 
12635         AppSearchSchema schema =
12636                 new AppSearchSchema.Builder("Gmail")
12637                         .addProperty(
12638                                 new StringPropertyConfig.Builder("subject")
12639                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
12640                                         .setIndexingType(
12641                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
12642                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
12643                                         .build())
12644                         .build();
12645         GenericDocument gmailDoc =
12646                 new GenericDocument.Builder<>("namespace", "id", "Gmail")
12647                         .setPropertyString("subject", "foo")
12648                         .build();
12649         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
12650 
12651         // Put the email document to AppSearch and verify its success.
12652         AppSearchBatchResult<String, Void> result =
12653                 checkIsBatchResultSuccess(
12654                         mDb1.putAsync(
12655                                 new PutDocumentsRequest.Builder()
12656                                         .addGenericDocuments(gmailDoc)
12657                                         .build()));
12658         assertThat(result.getSuccesses()).containsExactly("id", null);
12659         assertThat(result.getFailures()).isEmpty();
12660 
12661         // Update the schema by adding a scorable property.
12662         schema =
12663                 new AppSearchSchema.Builder("Gmail")
12664                         .addProperty(
12665                                 new StringPropertyConfig.Builder("subject")
12666                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
12667                                         .setIndexingType(
12668                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
12669                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
12670                                         .build())
12671                         .addProperty(
12672                                 new BooleanPropertyConfig.Builder("important")
12673                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
12674                                         .setScoringEnabled(true)
12675                                         .build())
12676                         .build();
12677         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
12678 
12679         // Search and rank over the existing doc.
12680         // The existing document's scorable property has been populated with the default values.
12681         SearchSpec searchSpec =
12682                 new SearchSpec.Builder()
12683                         .setScorablePropertyRankingEnabled(true)
12684                         .setRankingStrategy("sum(getScorableProperty(\"Gmail\", \"important\"))")
12685                         .build();
12686         SearchResultsShim searchResults = mDb1.search("", searchSpec);
12687         List<SearchResult> results = retrieveAllSearchResults(searchResults);
12688         assertThat(results).hasSize(1);
12689         assertThat(results.get(0).getRankingSignal()).isWithin(0.00001).of(0);
12690     }
12691 
12692     @Test
12693     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCORABLE_PROPERTY)
testRankWithScorableProperty_updateScorableTypeInNestedSchema()12694     public void testRankWithScorableProperty_updateScorableTypeInNestedSchema() throws Exception {
12695         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_SCORABLE_PROPERTY_CONFIG));
12696 
12697         AppSearchSchema personSchema =
12698                 new AppSearchSchema.Builder("Person")
12699                         .addProperty(
12700                                 new DoublePropertyConfig.Builder("income")
12701                                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
12702                                         .setScoringEnabled(false)
12703                                         .build())
12704                         .build();
12705         AppSearchSchema emailSchema =
12706                 new AppSearchSchema.Builder("Email")
12707                         .addProperty(
12708                                 new DocumentPropertyConfig.Builder("sender", "Person")
12709                                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
12710                                         .build())
12711                         .addProperty(
12712                                 new DocumentPropertyConfig.Builder("recipient", "Person")
12713                                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
12714                                         .build())
12715                         .build();
12716         mDb1.setSchemaAsync(
12717                         new SetSchemaRequest.Builder()
12718                                 .addSchemas(personSchema, emailSchema)
12719                                 .build())
12720                 .get();
12721 
12722         GenericDocument sender =
12723                 new GenericDocument.Builder<>("namespace", "person1", "Person")
12724                         .setPropertyDouble("income", 1000)
12725                         .build();
12726         GenericDocument recipient =
12727                 new GenericDocument.Builder<>("namespace", "person2", "Person")
12728                         .setPropertyDouble("income", 5000)
12729                         .build();
12730         GenericDocument email =
12731                 new GenericDocument.Builder<>("namespace", "email1", "Email")
12732                         .setPropertyDocument("sender", sender)
12733                         .setPropertyDocument("recipient", recipient)
12734                         .build();
12735         checkIsBatchResultSuccess(
12736                 mDb1.putAsync(
12737                         new PutDocumentsRequest.Builder().addGenericDocuments(email).build()));
12738 
12739         // Update the 'Person' schema by setting Person.income as scorable.
12740         // It would trigger the re-generation of the scorable property cache for the
12741         // the schema 'Email', as it is a parent schema of 'Person'.
12742         personSchema =
12743                 new AppSearchSchema.Builder("Person")
12744                         .addProperty(
12745                                 new DoublePropertyConfig.Builder("income")
12746                                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
12747                                         .setScoringEnabled(true)
12748                                         .build())
12749                         .build();
12750         mDb1.setSchemaAsync(
12751                         new SetSchemaRequest.Builder()
12752                                 .addSchemas(personSchema, emailSchema)
12753                                 .build())
12754                 .get();
12755 
12756         // Search and ranking with the Email.Person.income
12757         String rankingStrategy =
12758                 "sum(getScorableProperty(\"Email\", \"sender.income\")) + "
12759                         + "max(getScorableProperty(\"Email\", \"recipient.income\"))";
12760         SearchSpec searchSpec =
12761                 new SearchSpec.Builder()
12762                         .setScorablePropertyRankingEnabled(true)
12763                         .setRankingStrategy(rankingStrategy)
12764                         .build();
12765         double expectedScore = 1000 + 5000;
12766         SearchResultsShim searchResults = mDb1.search("", searchSpec);
12767         List<SearchResult> results = retrieveAllSearchResults(searchResults);
12768         assertThat(results).hasSize(1);
12769         assertThat(results.get(0).getGenericDocument()).isEqualTo(email);
12770         assertThat(results.get(0).getRankingSignal()).isWithin(0.00001).of(expectedScore);
12771     }
12772 
12773     @Test
12774     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCORABLE_PROPERTY)
testRankWithScorableProperty_updateSchemaByFlippingScorableType()12775     public void testRankWithScorableProperty_updateSchemaByFlippingScorableType() throws Exception {
12776         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_SCORABLE_PROPERTY_CONFIG));
12777 
12778         AppSearchSchema schemaWithPropertyScorable =
12779                 new AppSearchSchema.Builder("Gmail")
12780                         .addProperty(
12781                                 new BooleanPropertyConfig.Builder("important")
12782                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
12783                                         .setScoringEnabled(true)
12784                                         .build())
12785                         .build();
12786         GenericDocument doc =
12787                 new GenericDocument.Builder<>("namespace", "id", "Gmail")
12788                         .setPropertyBoolean("important", true)
12789                         .build();
12790         mDb1.setSchemaAsync(
12791                         new SetSchemaRequest.Builder()
12792                                 .addSchemas(schemaWithPropertyScorable)
12793                                 .build())
12794                 .get();
12795 
12796         checkIsBatchResultSuccess(
12797                 mDb1.putAsync(new PutDocumentsRequest.Builder().addGenericDocuments(doc).build()));
12798         SearchSpec searchSpec =
12799                 new SearchSpec.Builder()
12800                         .setScorablePropertyRankingEnabled(true)
12801                         .setRankingStrategy("sum(getScorableProperty(\"Gmail\", \"important\"))")
12802                         .build();
12803         SearchResultsShim searchResults = mDb1.search("", searchSpec);
12804         List<SearchResult> results = retrieveAllSearchResults(searchResults);
12805         assertThat(results).hasSize(1);
12806         assertThat(results.get(0).getGenericDocument()).isEqualTo(doc);
12807         assertThat(results.get(0).getRankingSignal()).isWithin(0.00001).of(1);
12808 
12809         // Update the Schema by updating the property as not scorable
12810         AppSearchSchema schemaWithPropertyNotScorable =
12811                 new AppSearchSchema.Builder("Gmail")
12812                         .addProperty(
12813                                 new BooleanPropertyConfig.Builder("important")
12814                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
12815                                         .setScoringEnabled(false)
12816                                         .build())
12817                         .build();
12818         mDb1.setSchemaAsync(
12819                         new SetSchemaRequest.Builder()
12820                                 .addSchemas(schemaWithPropertyNotScorable)
12821                                 .build())
12822                 .get();
12823 
12824         // Update the schema by updating the property to scorable again.
12825         mDb1.setSchemaAsync(
12826                         new SetSchemaRequest.Builder()
12827                                 .addSchemas(schemaWithPropertyScorable)
12828                                 .build())
12829                 .get();
12830         // Verify that the property can be used for scoring.
12831         searchResults = mDb1.search("", searchSpec);
12832         results = retrieveAllSearchResults(searchResults);
12833         assertThat(results).hasSize(1);
12834         assertThat(results.get(0).getGenericDocument()).isEqualTo(doc);
12835         assertThat(results.get(0).getRankingSignal()).isWithin(0.00001).of(1);
12836     }
12837 
12838     @Test
12839     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCORABLE_PROPERTY)
testRankWithScorableProperty_reorderSchemaProperties()12840     public void testRankWithScorableProperty_reorderSchemaProperties() throws Exception {
12841         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_SCORABLE_PROPERTY_CONFIG));
12842 
12843         AppSearchSchema personSchema =
12844                 new AppSearchSchema.Builder("Person")
12845                         .addProperty(
12846                                 new DoublePropertyConfig.Builder("income")
12847                                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
12848                                         .setScoringEnabled(true)
12849                                         .build())
12850                         .addProperty(
12851                                 new LongPropertyConfig.Builder("age")
12852                                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
12853                                         .setScoringEnabled(true)
12854                                         .build())
12855                         .build();
12856         GenericDocument person =
12857                 new GenericDocument.Builder<>("namespace", "person1", "Person")
12858                         .setPropertyDouble("income", 1000, 2000)
12859                         .setPropertyLong("age", 30)
12860                         .build();
12861         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(personSchema).build()).get();
12862         checkIsBatchResultSuccess(
12863                 mDb1.putAsync(
12864                         new PutDocumentsRequest.Builder().addGenericDocuments(person).build()));
12865         String rankingStrategy =
12866                 "sum(getScorableProperty(\"Person\", \"income\")) + "
12867                         + "sum(getScorableProperty(\"Person\", \"age\"))";
12868         SearchSpec searchSpec =
12869                 new SearchSpec.Builder()
12870                         .setScorablePropertyRankingEnabled(true)
12871                         .setRankingStrategy(rankingStrategy)
12872                         .build();
12873         double expectedRankingScore = 3030;
12874 
12875         SearchResultsShim searchResults = mDb1.search("", searchSpec);
12876         List<SearchResult> results = retrieveAllSearchResults(searchResults);
12877         assertThat(results.get(0).getRankingSignal()).isWithin(0.00001).of(expectedRankingScore);
12878 
12879         // Update the schema by swapping the order of property 'age' and 'income'
12880         personSchema =
12881                 new AppSearchSchema.Builder("Person")
12882                         .addProperty(
12883                                 new LongPropertyConfig.Builder("age")
12884                                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
12885                                         .setScoringEnabled(true)
12886                                         .build())
12887                         .addProperty(
12888                                 new DoublePropertyConfig.Builder("income")
12889                                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
12890                                         .setScoringEnabled(true)
12891                                         .build())
12892                         .build();
12893         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(personSchema).build()).get();
12894 
12895         // Verify that the ranking is still working as expected.
12896         searchResults = mDb1.search("", searchSpec);
12897         results = retrieveAllSearchResults(searchResults);
12898         assertThat(results.get(0).getRankingSignal()).isWithin(0.00001).of(expectedRankingScore);
12899     }
12900 
12901     @Test
12902     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCORABLE_PROPERTY)
testRankWithScorableProperty_matchedDocumentHasDifferentSchemaType()12903     public void testRankWithScorableProperty_matchedDocumentHasDifferentSchemaType()
12904             throws Exception {
12905         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_SCORABLE_PROPERTY_CONFIG));
12906         AppSearchSchema gmailSchema =
12907                 new AppSearchSchema.Builder("Gmail")
12908                         .addProperty(
12909                                 new BooleanPropertyConfig.Builder("important")
12910                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
12911                                         .setScoringEnabled(true)
12912                                         .build())
12913                         .build();
12914         AppSearchSchema personSchema =
12915                 new AppSearchSchema.Builder("Person")
12916                         .addProperty(
12917                                 new LongPropertyConfig.Builder("income")
12918                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
12919                                         .setScoringEnabled(true)
12920                                         .build())
12921                         .build();
12922         mDb1.setSchemaAsync(
12923                         new SetSchemaRequest.Builder()
12924                                 .addSchemas(gmailSchema, personSchema)
12925                                 .build())
12926                 .get();
12927 
12928         GenericDocument gmailDoc =
12929                 new GenericDocument.Builder<>("namespace", "id1", "Gmail")
12930                         .setPropertyBoolean("important", true)
12931                         .setScore(1)
12932                         .build();
12933         GenericDocument personDoc =
12934                 new GenericDocument.Builder<>("namespace", "id2", "Person")
12935                         .setPropertyLong("income", 100)
12936                         .setScore(1)
12937                         .build();
12938         checkIsBatchResultSuccess(
12939                 mDb1.putAsync(
12940                         new PutDocumentsRequest.Builder()
12941                                 .addGenericDocuments(gmailDoc, personDoc)
12942                                 .build()));
12943 
12944         SearchSpec searchSpec =
12945                 new SearchSpec.Builder()
12946                         .setScorablePropertyRankingEnabled(true)
12947                         .setRankingStrategy(
12948                                 "this.documentScore() + sum(getScorableProperty(\"Gmail\","
12949                                     + " \"important\"))")
12950                         .build();
12951         double expectedGmailDocScore = 2;
12952         double expectedPersonDocScore = 1;
12953 
12954         SearchResultsShim searchResults = mDb1.search("", searchSpec);
12955         List<SearchResult> results = retrieveAllSearchResults(searchResults);
12956         assertThat(results).hasSize(2);
12957         assertThat(results.get(0).getGenericDocument()).isEqualTo(gmailDoc);
12958         assertThat(results.get(0).getRankingSignal()).isWithin(0.00001).of(expectedGmailDocScore);
12959         assertThat(results.get(1).getGenericDocument()).isEqualTo(personDoc);
12960         assertThat(results.get(1).getRankingSignal()).isWithin(0.00001).of(expectedPersonDocScore);
12961     }
12962 
12963     @Test
12964     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCORABLE_PROPERTY)
testRankWithScorableProperty_matchedDocumentHasNoDataUnderTheRankingProperty()12965     public void testRankWithScorableProperty_matchedDocumentHasNoDataUnderTheRankingProperty()
12966             throws Exception {
12967         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_SCORABLE_PROPERTY_CONFIG));
12968         AppSearchSchema gmailSchema =
12969                 new AppSearchSchema.Builder("Gmail")
12970                         .addProperty(
12971                                 new LongPropertyConfig.Builder("viewTimes")
12972                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
12973                                         .setScoringEnabled(true)
12974                                         .build())
12975                         .build();
12976         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(gmailSchema).build()).get();
12977 
12978         GenericDocument gmailDoc1 =
12979                 new GenericDocument.Builder<>("namespace", "id1", "Gmail")
12980                         .setPropertyLong("viewTimes", 100)
12981                         .setScore(1)
12982                         .build();
12983         GenericDocument gmailDoc2 =
12984                 new GenericDocument.Builder<>("namespace", "id2", "Gmail").setScore(1).build();
12985         checkIsBatchResultSuccess(
12986                 mDb1.putAsync(
12987                         new PutDocumentsRequest.Builder()
12988                                 .addGenericDocuments(gmailDoc1, gmailDoc2)
12989                                 .build()));
12990 
12991         SearchSpec searchSpec =
12992                 new SearchSpec.Builder()
12993                         .setScorablePropertyRankingEnabled(true)
12994                         .setRankingStrategy(
12995                                 "this.documentScore() + sum(getScorableProperty(\"Gmail\","
12996                                     + " \"viewTimes\"))")
12997                         .build();
12998         double expectedGmailDoc1Score = 101;
12999         double expectedGmailDoc2Score = 1;
13000 
13001         SearchResultsShim searchResults = mDb1.search("", searchSpec);
13002         List<SearchResult> results = retrieveAllSearchResults(searchResults);
13003         assertThat(results).hasSize(2);
13004         assertThat(results.get(0).getGenericDocument()).isEqualTo(gmailDoc1);
13005         assertThat(results.get(0).getRankingSignal()).isWithin(0.00001).of(expectedGmailDoc1Score);
13006         assertThat(results.get(1).getGenericDocument()).isEqualTo(gmailDoc2);
13007         assertThat(results.get(1).getRankingSignal()).isWithin(0.00001).of(expectedGmailDoc2Score);
13008     }
13009 
13010     @Test
13011     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCORABLE_PROPERTY)
testRankWithScorableProperty_joinWithChildQuery()13012     public void testRankWithScorableProperty_joinWithChildQuery() throws Exception {
13013         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_SCORABLE_PROPERTY_CONFIG));
13014         assumeTrue(
13015                 mDb1.getFeatures()
13016                         .isFeatureSupported(Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION));
13017         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
13018 
13019         AppSearchSchema personSchema =
13020                 new AppSearchSchema.Builder("Person")
13021                         .addProperty(
13022                                 new StringPropertyConfig.Builder("name")
13023                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
13024                                         .setIndexingType(
13025                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
13026                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
13027                                         .build())
13028                         .addProperty(
13029                                 new DoublePropertyConfig.Builder("income")
13030                                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
13031                                         .setScoringEnabled(true)
13032                                         .build())
13033                         .addProperty(
13034                                 new BooleanPropertyConfig.Builder("isStarred")
13035                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
13036                                         .setScoringEnabled(true)
13037                                         .build())
13038                         .build();
13039         AppSearchSchema callLogSchema =
13040                 new AppSearchSchema.Builder("CallLog")
13041                         .addProperty(
13042                                 new StringPropertyConfig.Builder("personQualifiedId")
13043                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
13044                                         .setJoinableValueType(
13045                                                 StringPropertyConfig
13046                                                         .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
13047                                         .build())
13048                         .addProperty(
13049                                 new DoublePropertyConfig.Builder("rfsScore")
13050                                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
13051                                         .setScoringEnabled(true)
13052                                         .build())
13053                         .build();
13054         AppSearchSchema smsLogSchema =
13055                 new AppSearchSchema.Builder("SmsLog")
13056                         .addProperty(
13057                                 new StringPropertyConfig.Builder("personQualifiedId")
13058                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
13059                                         .setJoinableValueType(
13060                                                 StringPropertyConfig
13061                                                         .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
13062                                         .build())
13063                         .addProperty(
13064                                 new DoublePropertyConfig.Builder("rfsScore")
13065                                         .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
13066                                         .setScoringEnabled(true)
13067                                         .build())
13068                         .build();
13069         mDb1.setSchemaAsync(
13070                         new SetSchemaRequest.Builder()
13071                                 .addSchemas(personSchema, callLogSchema, smsLogSchema)
13072                                 .build())
13073                 .get();
13074 
13075         // John will have two CallLog docs to join and one sms log to join.
13076         GenericDocument personJohn =
13077                 new GenericDocument.Builder<>("namespace", "johnId", "Person")
13078                         .setPropertyString("name", "John")
13079                         .setPropertyBoolean("isStarred", true)
13080                         .setPropertyDouble("income", 30)
13081                         .setScore(10)
13082                         .build();
13083         // Kevin will have two CallLog docs to join and one sms log to join.
13084         GenericDocument personKevin =
13085                 new GenericDocument.Builder<>("namespace", "kevinId", "Person")
13086                         .setPropertyString("name", "Kevin")
13087                         .setPropertyBoolean("isStarred", false)
13088                         .setPropertyDouble("income", 40)
13089                         .setScore(20)
13090                         .build();
13091         // Tim has no CallLog or SmsLog to join.
13092         GenericDocument personTim =
13093                 new GenericDocument.Builder<>("namespace", "timId", "Person")
13094                         .setPropertyString("name", "Tim")
13095                         .setPropertyDouble("income", 60)
13096                         .setPropertyBoolean("isStarred", true)
13097                         .setScore(50)
13098                         .build();
13099 
13100         GenericDocument johnCallLog1 =
13101                 new GenericDocument.Builder<>("namespace", "johnCallLog1", "CallLog")
13102                         .setScore(5)
13103                         .setPropertyDouble("rfsScore", 100, 200)
13104                         .setPropertyString(
13105                                 "personQualifiedId",
13106                                 DocumentIdUtil.createQualifiedId(
13107                                         mContext.getPackageName(), DB_NAME_1, personJohn))
13108                         .build();
13109         GenericDocument johnCallLog2 =
13110                 new GenericDocument.Builder<>("namespace", "johnCallLog2", "CallLog")
13111                         .setScore(5)
13112                         .setPropertyDouble("rfsScore", 300, 500)
13113                         .setPropertyString(
13114                                 "personQualifiedId",
13115                                 DocumentIdUtil.createQualifiedId(
13116                                         mContext.getPackageName(), DB_NAME_1, personJohn))
13117                         .build();
13118         GenericDocument kevinCallLog1 =
13119                 new GenericDocument.Builder<>("namespace", "kevinCallLog1", "CallLog")
13120                         .setScore(5)
13121                         .setPropertyDouble("rfsScore", 300, 400)
13122                         .setPropertyString(
13123                                 "personQualifiedId",
13124                                 DocumentIdUtil.createQualifiedId(
13125                                         mContext.getPackageName(), DB_NAME_1, personKevin))
13126                         .build();
13127         GenericDocument kevinCallLog2 =
13128                 new GenericDocument.Builder<>("namespace", "kevinCallLog2", "CallLog")
13129                         .setScore(5)
13130                         .setPropertyDouble("rfsScore", 500, 800)
13131                         .setPropertyString(
13132                                 "personQualifiedId",
13133                                 DocumentIdUtil.createQualifiedId(
13134                                         mContext.getPackageName(), DB_NAME_1, personKevin))
13135                         .build();
13136         GenericDocument johnSmsLog1 =
13137                 new GenericDocument.Builder<>("namespace", "johnSmsLog1", "SmsLog")
13138                         .setScore(5)
13139                         .setPropertyDouble("rfsScore", 1000, 2000)
13140                         .setPropertyString(
13141                                 "personQualifiedId",
13142                                 DocumentIdUtil.createQualifiedId(
13143                                         mContext.getPackageName(), DB_NAME_1, personJohn))
13144                         .build();
13145         GenericDocument kevinSmsLog1 =
13146                 new GenericDocument.Builder<>("namespace", "kevinSmsLog1", "SmsLog")
13147                         .setScore(5)
13148                         .setPropertyDouble("rfsScore", 2000, 3000)
13149                         .setPropertyString(
13150                                 "personQualifiedId",
13151                                 DocumentIdUtil.createQualifiedId(
13152                                         mContext.getPackageName(), DB_NAME_1, personKevin))
13153                         .build();
13154 
13155         // Put all documents to AppSearch and verify its success.
13156         AppSearchBatchResult<String, Void> result =
13157                 checkIsBatchResultSuccess(
13158                         mDb1.putAsync(
13159                                 new PutDocumentsRequest.Builder()
13160                                         .addGenericDocuments(
13161                                                 personTim,
13162                                                 personJohn,
13163                                                 personKevin,
13164                                                 kevinCallLog1,
13165                                                 kevinCallLog2,
13166                                                 kevinSmsLog1,
13167                                                 johnCallLog1,
13168                                                 johnCallLog2,
13169                                                 johnSmsLog1)
13170                                         .build()));
13171         assertThat(result.getSuccesses().size()).isEqualTo(9);
13172         assertThat(result.getFailures()).isEmpty();
13173 
13174         String childRankingStrategy =
13175                 "sum(getScorableProperty(\"CallLog\", \"rfsScore\")) + "
13176                         + "sum(getScorableProperty(\"SmsLog\", \"rfsScore\"))";
13177         double johnChildDocScore = 100 + 200 + 300 + 500 + 1000 + 2000;
13178         double kevinChildDocScore = 300 + 400 + 500 + 800 + 2000 + 3000;
13179         double timChildDocScore = 0;
13180 
13181         SearchSpec childSearchSpec =
13182                 new SearchSpec.Builder()
13183                         .setScorablePropertyRankingEnabled(true)
13184                         .setRankingStrategy(childRankingStrategy)
13185                         .build();
13186         JoinSpec js =
13187                 new JoinSpec.Builder("personQualifiedId")
13188                         .setNestedSearch("", childSearchSpec)
13189                         .build();
13190         String parentRankingStrategy =
13191                 "sum(getScorableProperty(\"Person\", \"income\")) + "
13192                         + "20 * sum(getScorableProperty(\"Person\", \"isStarred\")) + "
13193                         + "sum(this.childrenRankingSignals())";
13194         SearchSpec parentSearchSpec =
13195                 new SearchSpec.Builder()
13196                         .setScorablePropertyRankingEnabled(true)
13197                         .setJoinSpec(js)
13198                         .setRankingStrategy(parentRankingStrategy)
13199                         .addFilterSchemas("Person")
13200                         .build();
13201         double johnExpectScore = /* income= */ 30 + 20 * /* isStarred= */ 1 + johnChildDocScore;
13202         double kevinExpectScore = /* income= */ 40 + 20 * /* isStarred= */ 0 + kevinChildDocScore;
13203         double timExpectScore = /* income= */ 60 + 20 * /* isStarred= */ 1 + timChildDocScore;
13204 
13205         SearchResultsShim searchResults = mDb1.search("", parentSearchSpec);
13206         List<SearchResult> results = retrieveAllSearchResults(searchResults);
13207         assertThat(results).hasSize(3);
13208         assertThat(results.get(0).getRankingSignal()).isWithin(0.00001).of(kevinExpectScore);
13209         assertThat(results.get(1).getRankingSignal()).isWithin(0.00001).of(johnExpectScore);
13210         assertThat(results.get(2).getRankingSignal()).isWithin(0.00001).of(timExpectScore);
13211     }
13212 
13213     @Test
13214     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SEARCH_RESULT_PARENT_TYPES)
testQuery_typeFilterWithPolymorphism()13215     public void testQuery_typeFilterWithPolymorphism() throws Exception {
13216         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
13217         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_RESULT_PARENT_TYPES));
13218 
13219         // Schema registration
13220         AppSearchSchema personSchema =
13221                 new AppSearchSchema.Builder("Person")
13222                         .addProperty(
13223                                 new StringPropertyConfig.Builder("name")
13224                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
13225                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
13226                                         .setIndexingType(
13227                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
13228                                         .build())
13229                         .build();
13230         AppSearchSchema artistSchema =
13231                 new AppSearchSchema.Builder("Artist")
13232                         .addParentType("Person")
13233                         .addProperty(
13234                                 new StringPropertyConfig.Builder("name")
13235                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
13236                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
13237                                         .setIndexingType(
13238                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
13239                                         .build())
13240                         .build();
13241         mDb1.setSchemaAsync(
13242                         new SetSchemaRequest.Builder()
13243                                 .addSchemas(personSchema)
13244                                 .addSchemas(artistSchema)
13245                                 .addSchemas(AppSearchEmail.SCHEMA)
13246                                 .build())
13247                 .get();
13248 
13249         // Index some documents
13250         GenericDocument personDoc =
13251                 new GenericDocument.Builder<>("namespace", "id1", "Person")
13252                         .setPropertyString("name", "Foo")
13253                         .build();
13254         GenericDocument artistDoc =
13255                 new GenericDocument.Builder<>("namespace", "id2", "Artist")
13256                         .setPropertyString("name", "Foo")
13257                         .build();
13258         AppSearchEmail emailDoc =
13259                 new AppSearchEmail.Builder("namespace", "id3")
13260                         .setFrom("[email protected]")
13261                         .setTo("[email protected]", "[email protected]")
13262                         .setSubject("testPut example")
13263                         .setBody("Foo")
13264                         .build();
13265         checkIsBatchResultSuccess(
13266                 mDb1.putAsync(
13267                         new PutDocumentsRequest.Builder()
13268                                 .addGenericDocuments(personDoc, artistDoc, emailDoc)
13269                                 .build()));
13270 
13271         // Query for the documents
13272         SearchResultsShim searchResults =
13273                 mDb1.search(
13274                         "Foo",
13275                         new SearchSpec.Builder()
13276                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
13277                                 .build());
13278         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
13279         assertThat(documents).hasSize(3);
13280         assertThat(documents).containsExactly(personDoc, artistDoc, emailDoc);
13281 
13282         // Query with a filter for the "Person" type should also include the "Artist" type.
13283         searchResults =
13284                 mDb1.search(
13285                         "Foo",
13286                         new SearchSpec.Builder()
13287                                 .addFilterSchemas("Person")
13288                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
13289                                 .build());
13290         documents = convertSearchResultsToDocuments(searchResults);
13291         assertThat(documents).hasSize(2);
13292         assertThat(documents).containsExactly(personDoc, artistDoc);
13293 
13294         // Query with a filters for the "Artist" type should not include the "Person" type.
13295         searchResults =
13296                 mDb1.search(
13297                         "Foo",
13298                         new SearchSpec.Builder()
13299                                 .addFilterSchemas("Artist")
13300                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
13301                                 .build());
13302         documents = convertSearchResultsToDocuments(searchResults);
13303         assertThat(documents).hasSize(1);
13304         assertThat(documents).containsExactly(artistDoc);
13305     }
13306 
13307     @Test
13308     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SEARCH_RESULT_PARENT_TYPES)
testQuery_projectionWithPolymorphism()13309     public void testQuery_projectionWithPolymorphism() throws Exception {
13310         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
13311         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_RESULT_PARENT_TYPES));
13312 
13313         // Schema registration
13314         AppSearchSchema personSchema =
13315                 new AppSearchSchema.Builder("Person")
13316                         .addProperty(
13317                                 new StringPropertyConfig.Builder("name")
13318                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
13319                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
13320                                         .setIndexingType(
13321                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
13322                                         .build())
13323                         .addProperty(
13324                                 new StringPropertyConfig.Builder("emailAddress")
13325                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
13326                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
13327                                         .setIndexingType(
13328                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
13329                                         .build())
13330                         .build();
13331         AppSearchSchema artistSchema =
13332                 new AppSearchSchema.Builder("Artist")
13333                         .addParentType("Person")
13334                         .addProperty(
13335                                 new StringPropertyConfig.Builder("name")
13336                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
13337                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
13338                                         .setIndexingType(
13339                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
13340                                         .build())
13341                         .addProperty(
13342                                 new StringPropertyConfig.Builder("emailAddress")
13343                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
13344                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
13345                                         .setIndexingType(
13346                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
13347                                         .build())
13348                         .addProperty(
13349                                 new StringPropertyConfig.Builder("company")
13350                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
13351                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
13352                                         .setIndexingType(
13353                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
13354                                         .build())
13355                         .build();
13356         mDb1.setSchemaAsync(
13357                         new SetSchemaRequest.Builder()
13358                                 .addSchemas(personSchema)
13359                                 .addSchemas(artistSchema)
13360                                 .build())
13361                 .get();
13362 
13363         // Index two documents
13364         GenericDocument personDoc =
13365                 new GenericDocument.Builder<>("namespace", "id1", "Person")
13366                         .setCreationTimestampMillis(1000)
13367                         .setPropertyString("name", "Foo Person")
13368                         .setPropertyString("emailAddress", "[email protected]")
13369                         .build();
13370         GenericDocument artistDoc =
13371                 new GenericDocument.Builder<>("namespace", "id2", "Artist")
13372                         .setCreationTimestampMillis(1000)
13373                         .setPropertyString("name", "Foo Artist")
13374                         .setPropertyString("emailAddress", "[email protected]")
13375                         .setPropertyString("company", "Company")
13376                         .build();
13377         checkIsBatchResultSuccess(
13378                 mDb1.putAsync(
13379                         new PutDocumentsRequest.Builder()
13380                                 .addGenericDocuments(personDoc, artistDoc)
13381                                 .build()));
13382 
13383         // Query with type property paths {"Person", ["name"]}, {"Artist", ["emailAddress"]}
13384         // This will be expanded to paths {"Person", ["name"]}, {"Artist", ["name", "emailAddress"]}
13385         // via polymorphism.
13386         SearchResultsShim searchResults =
13387                 mDb1.search(
13388                         "Foo",
13389                         new SearchSpec.Builder()
13390                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
13391                                 .addProjection("Person", ImmutableList.of("name"))
13392                                 .addProjection("Artist", ImmutableList.of("emailAddress"))
13393                                 .build());
13394         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
13395 
13396         // The person document should have been returned with only the "name" property. The artist
13397         // document should have been returned with all of its properties.
13398         GenericDocument expectedPerson =
13399                 new GenericDocument.Builder<>("namespace", "id1", "Person")
13400                         .setCreationTimestampMillis(1000)
13401                         .setPropertyString("name", "Foo Person")
13402                         .build();
13403         GenericDocument expectedArtist =
13404                 new GenericDocument.Builder<>("namespace", "id2", "Artist")
13405                         .setCreationTimestampMillis(1000)
13406                         .setPropertyString("name", "Foo Artist")
13407                         .setPropertyString("emailAddress", "[email protected]")
13408                         .build();
13409         assertThat(documents).containsExactly(expectedPerson, expectedArtist);
13410     }
13411 
13412     @Test
13413     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SEARCH_RESULT_PARENT_TYPES)
testQuery_projectionWithPolymorphismAndSchemaFilter()13414     public void testQuery_projectionWithPolymorphismAndSchemaFilter() throws Exception {
13415         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
13416         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_RESULT_PARENT_TYPES));
13417 
13418         // Schema registration
13419         AppSearchSchema personSchema =
13420                 new AppSearchSchema.Builder("Person")
13421                         .addProperty(
13422                                 new StringPropertyConfig.Builder("name")
13423                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
13424                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
13425                                         .setIndexingType(
13426                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
13427                                         .build())
13428                         .addProperty(
13429                                 new StringPropertyConfig.Builder("emailAddress")
13430                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
13431                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
13432                                         .setIndexingType(
13433                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
13434                                         .build())
13435                         .build();
13436         AppSearchSchema artistSchema =
13437                 new AppSearchSchema.Builder("Artist")
13438                         .addParentType("Person")
13439                         .addProperty(
13440                                 new StringPropertyConfig.Builder("name")
13441                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
13442                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
13443                                         .setIndexingType(
13444                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
13445                                         .build())
13446                         .addProperty(
13447                                 new StringPropertyConfig.Builder("emailAddress")
13448                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
13449                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
13450                                         .setIndexingType(
13451                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
13452                                         .build())
13453                         .addProperty(
13454                                 new StringPropertyConfig.Builder("company")
13455                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
13456                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
13457                                         .setIndexingType(
13458                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
13459                                         .build())
13460                         .build();
13461         mDb1.setSchemaAsync(
13462                         new SetSchemaRequest.Builder()
13463                                 .addSchemas(personSchema)
13464                                 .addSchemas(artistSchema)
13465                                 .build())
13466                 .get();
13467 
13468         // Index two documents
13469         GenericDocument personDoc =
13470                 new GenericDocument.Builder<>("namespace", "id1", "Person")
13471                         .setCreationTimestampMillis(1000)
13472                         .setPropertyString("name", "Foo Person")
13473                         .setPropertyString("emailAddress", "[email protected]")
13474                         .build();
13475         GenericDocument artistDoc =
13476                 new GenericDocument.Builder<>("namespace", "id2", "Artist")
13477                         .setCreationTimestampMillis(1000)
13478                         .setPropertyString("name", "Foo Artist")
13479                         .setPropertyString("emailAddress", "[email protected]")
13480                         .setPropertyString("company", "Company")
13481                         .build();
13482         checkIsBatchResultSuccess(
13483                 mDb1.putAsync(
13484                         new PutDocumentsRequest.Builder()
13485                                 .addGenericDocuments(personDoc, artistDoc)
13486                                 .build()));
13487 
13488         // Query with type property paths {"Person", ["name"]} and {"Artist", ["emailAddress"]}, and
13489         // a schema filter for the "Person".
13490         // This will be expanded to paths {"Person", ["name"]} and
13491         // {"Artist", ["name", "emailAddress"]}, and filters for both "Person" and "Artist" via
13492         // polymorphism.
13493         SearchResultsShim searchResults =
13494                 mDb1.search(
13495                         "Foo",
13496                         new SearchSpec.Builder()
13497                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
13498                                 .addFilterSchemas("Person")
13499                                 .addProjection("Person", ImmutableList.of("name"))
13500                                 .addProjection("Artist", ImmutableList.of("emailAddress"))
13501                                 .build());
13502         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
13503 
13504         // The person document should have been returned with only the "name" property. The artist
13505         // document should have been returned with all of its properties.
13506         GenericDocument expectedPerson =
13507                 new GenericDocument.Builder<>("namespace", "id1", "Person")
13508                         .setCreationTimestampMillis(1000)
13509                         .setPropertyString("name", "Foo Person")
13510                         .build();
13511         GenericDocument expectedArtist =
13512                 new GenericDocument.Builder<>("namespace", "id2", "Artist")
13513                         .setCreationTimestampMillis(1000)
13514                         .setPropertyString("name", "Foo Artist")
13515                         .setPropertyString("emailAddress", "[email protected]")
13516                         .build();
13517         assertThat(documents).containsExactly(expectedPerson, expectedArtist);
13518     }
13519 
13520     @Test
13521     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SEARCH_RESULT_PARENT_TYPES)
testQuery_indexBasedOnParentTypePolymorphism()13522     public void testQuery_indexBasedOnParentTypePolymorphism() throws Exception {
13523         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
13524         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_RESULT_PARENT_TYPES));
13525 
13526         // Schema registration
13527         AppSearchSchema personSchema =
13528                 new AppSearchSchema.Builder("Person")
13529                         .addProperty(
13530                                 new StringPropertyConfig.Builder("name")
13531                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
13532                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
13533                                         .setIndexingType(
13534                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
13535                                         .build())
13536                         .build();
13537         AppSearchSchema artistSchema =
13538                 new AppSearchSchema.Builder("Artist")
13539                         .addParentType("Person")
13540                         .addProperty(
13541                                 new StringPropertyConfig.Builder("name")
13542                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
13543                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
13544                                         .setIndexingType(
13545                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
13546                                         .build())
13547                         .addProperty(
13548                                 new StringPropertyConfig.Builder("company")
13549                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
13550                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
13551                                         .setIndexingType(
13552                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
13553                                         .build())
13554                         .build();
13555         AppSearchSchema messageSchema =
13556                 new AppSearchSchema.Builder("Message")
13557                         .addProperty(
13558                                 new AppSearchSchema.DocumentPropertyConfig.Builder(
13559                                                 "sender", "Person")
13560                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
13561                                         .setShouldIndexNestedProperties(true)
13562                                         .build())
13563                         .build();
13564         mDb1.setSchemaAsync(
13565                         new SetSchemaRequest.Builder()
13566                                 .addSchemas(personSchema)
13567                                 .addSchemas(artistSchema)
13568                                 .addSchemas(messageSchema)
13569                                 .build())
13570                 .get();
13571 
13572         // Index some an artistDoc and a messageDoc
13573         GenericDocument artistDoc =
13574                 new GenericDocument.Builder<>("namespace", "id1", "Artist")
13575                         .setPropertyString("name", "Foo")
13576                         .setPropertyString("company", "Bar")
13577                         .build();
13578         GenericDocument messageDoc =
13579                 new GenericDocument.Builder<>("namespace", "id2", "Message")
13580                         // sender is defined as a Person, which accepts an Artist because Artist <:
13581                         // Person.
13582                         // However, indexing will be based on what's defined in Person, so the
13583                         // "company"
13584                         // property in artistDoc cannot be used to search this messageDoc.
13585                         .setPropertyDocument("sender", artistDoc)
13586                         .build();
13587         checkIsBatchResultSuccess(
13588                 mDb1.putAsync(
13589                         new PutDocumentsRequest.Builder()
13590                                 .addGenericDocuments(artistDoc, messageDoc)
13591                                 .build()));
13592 
13593         // Query for the documents
13594         SearchResultsShim searchResults =
13595                 mDb1.search(
13596                         "Foo",
13597                         new SearchSpec.Builder()
13598                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
13599                                 .build());
13600         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
13601         assertThat(documents).hasSize(2);
13602         assertThat(documents).containsExactly(artistDoc, messageDoc);
13603 
13604         // The "company" property in artistDoc cannot be used to search messageDoc.
13605         searchResults =
13606                 mDb1.search(
13607                         "Bar",
13608                         new SearchSpec.Builder()
13609                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
13610                                 .build());
13611         documents = convertSearchResultsToDocuments(searchResults);
13612         assertThat(documents).hasSize(1);
13613         assertThat(documents).containsExactly(artistDoc);
13614     }
13615 
13616     @Test
13617     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SEARCH_RESULT_PARENT_TYPES)
testQuery_parentTypeListIsTopologicalOrder()13618     public void testQuery_parentTypeListIsTopologicalOrder() throws Exception {
13619         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
13620         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_RESULT_PARENT_TYPES));
13621         // Create the following subtype relation graph, where
13622         // 1. A's direct parents are B and C.
13623         // 2. B's direct parent is D.
13624         // 3. C's direct parent is B and D.
13625         // DFS order from A: [A, B, D, C]. Not acceptable because B and D appear before C.
13626         // BFS order from A: [A, B, C, D]. Not acceptable because B appears before C.
13627         // Topological order (all subtypes appear before supertypes) from A: [A, C, B, D].
13628         AppSearchSchema schemaA =
13629                 new AppSearchSchema.Builder("A").addParentType("B").addParentType("C").build();
13630         AppSearchSchema schemaB = new AppSearchSchema.Builder("B").addParentType("D").build();
13631         AppSearchSchema schemaC =
13632                 new AppSearchSchema.Builder("C").addParentType("B").addParentType("D").build();
13633         AppSearchSchema schemaD = new AppSearchSchema.Builder("D").build();
13634         mDb1.setSchemaAsync(
13635                         new SetSchemaRequest.Builder()
13636                                 .addSchemas(schemaA)
13637                                 .addSchemas(schemaB)
13638                                 .addSchemas(schemaC)
13639                                 .addSchemas(schemaD)
13640                                 .build())
13641                 .get();
13642 
13643         // Index some documents
13644         GenericDocument docA = new GenericDocument.Builder<>("namespace", "id1", "A").build();
13645         GenericDocument docB = new GenericDocument.Builder<>("namespace", "id2", "B").build();
13646         GenericDocument docC = new GenericDocument.Builder<>("namespace", "id3", "C").build();
13647         GenericDocument docD = new GenericDocument.Builder<>("namespace", "id4", "D").build();
13648         checkIsBatchResultSuccess(
13649                 mDb1.putAsync(
13650                         new PutDocumentsRequest.Builder()
13651                                 .addGenericDocuments(docA, docB, docC, docD)
13652                                 .build()));
13653 
13654         Map<String, List<String>> expectedDocAParentTypeMap =
13655                 ImmutableMap.of("A", ImmutableList.of("C", "B", "D"));
13656         Map<String, List<String>> expectedDocBParentTypeMap =
13657                 ImmutableMap.of("B", ImmutableList.of("D"));
13658         Map<String, List<String>> expectedDocCParentTypeMap =
13659                 ImmutableMap.of("C", ImmutableList.of("B", "D"));
13660         Map<String, List<String>> expectedDocDParentTypeMap = Collections.emptyMap();
13661         // Query for the documents
13662         List<SearchResult> searchResults =
13663                 retrieveAllSearchResults(mDb1.search("", new SearchSpec.Builder().build()));
13664         assertThat(searchResults).hasSize(4);
13665         assertThat(searchResults.get(0).getGenericDocument()).isEqualTo(docD);
13666         assertThat(searchResults.get(0).getParentTypeMap()).isEqualTo(expectedDocDParentTypeMap);
13667 
13668         assertThat(searchResults.get(1).getGenericDocument()).isEqualTo(docC);
13669         assertThat(searchResults.get(1).getParentTypeMap()).isEqualTo(expectedDocCParentTypeMap);
13670 
13671         assertThat(searchResults.get(2).getGenericDocument()).isEqualTo(docB);
13672         assertThat(searchResults.get(2).getParentTypeMap()).isEqualTo(expectedDocBParentTypeMap);
13673 
13674         assertThat(searchResults.get(3).getGenericDocument()).isEqualTo(docA);
13675         assertThat(searchResults.get(3).getParentTypeMap()).isEqualTo(expectedDocAParentTypeMap);
13676     }
13677 
13678     @Test
13679     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SEARCH_RESULT_PARENT_TYPES)
testQuery_wildcardProjection_polymorphism()13680     public void testQuery_wildcardProjection_polymorphism() throws Exception {
13681         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
13682         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_RESULT_PARENT_TYPES));
13683 
13684         AppSearchSchema messageSchema =
13685                 new AppSearchSchema.Builder("Message")
13686                         .addProperty(
13687                                 new StringPropertyConfig.Builder("sender")
13688                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
13689                                         .setIndexingType(
13690                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
13691                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
13692                                         .build())
13693                         .addProperty(
13694                                 new StringPropertyConfig.Builder("content")
13695                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
13696                                         .setIndexingType(
13697                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
13698                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
13699                                         .build())
13700                         .build();
13701         AppSearchSchema textSchema =
13702                 new AppSearchSchema.Builder("Text")
13703                         .addParentType("Message")
13704                         .addProperty(
13705                                 new StringPropertyConfig.Builder("sender")
13706                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
13707                                         .setIndexingType(
13708                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
13709                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
13710                                         .build())
13711                         .addProperty(
13712                                 new StringPropertyConfig.Builder("content")
13713                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
13714                                         .setIndexingType(
13715                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
13716                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
13717                                         .build())
13718                         .build();
13719         AppSearchSchema emailSchema =
13720                 new AppSearchSchema.Builder("Email")
13721                         .addParentType("Message")
13722                         .addProperty(
13723                                 new StringPropertyConfig.Builder("sender")
13724                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
13725                                         .setIndexingType(
13726                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
13727                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
13728                                         .build())
13729                         .addProperty(
13730                                 new StringPropertyConfig.Builder("content")
13731                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
13732                                         .setIndexingType(
13733                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
13734                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
13735                                         .build())
13736                         .build();
13737 
13738         // Schema registration
13739         mDb1.setSchemaAsync(
13740                         new SetSchemaRequest.Builder()
13741                                 .addSchemas(messageSchema, textSchema, emailSchema)
13742                                 .build())
13743                 .get();
13744 
13745         // Index two child documents
13746         GenericDocument text =
13747                 new GenericDocument.Builder<>("namespace", "id1", "Text")
13748                         .setCreationTimestampMillis(1000)
13749                         .setPropertyString("sender", "Some sender")
13750                         .setPropertyString("content", "Some note")
13751                         .build();
13752         GenericDocument email =
13753                 new GenericDocument.Builder<>("namespace", "id2", "Email")
13754                         .setCreationTimestampMillis(1000)
13755                         .setPropertyString("sender", "Some sender")
13756                         .setPropertyString("content", "Some note")
13757                         .build();
13758         checkIsBatchResultSuccess(
13759                 mDb1.putAsync(
13760                         new PutDocumentsRequest.Builder()
13761                                 .addGenericDocuments(email, text)
13762                                 .build()));
13763 
13764         SearchResultsShim searchResults =
13765                 mDb1.search(
13766                         "Some",
13767                         new SearchSpec.Builder()
13768                                 .addFilterSchemas("Message")
13769                                 .addProjection(
13770                                         SearchSpec.SCHEMA_TYPE_WILDCARD, ImmutableList.of("sender"))
13771                                 .addFilterProperties(
13772                                         SearchSpec.SCHEMA_TYPE_WILDCARD,
13773                                         ImmutableList.of("content"))
13774                                 .build());
13775         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
13776 
13777         // We specified the parent document in the filter schemas, but only indexed child documents.
13778         // As we also specified a wildcard schema type projection, it should apply to the child docs
13779         // The content property must not appear. Also emailNoContent should not appear as we are
13780         // filter on the content property
13781         GenericDocument expectedText =
13782                 new GenericDocument.Builder<>("namespace", "id1", "Text")
13783                         .setCreationTimestampMillis(1000)
13784                         .setPropertyString("sender", "Some sender")
13785                         .build();
13786         GenericDocument expectedEmail =
13787                 new GenericDocument.Builder<>("namespace", "id2", "Email")
13788                         .setCreationTimestampMillis(1000)
13789                         .setPropertyString("sender", "Some sender")
13790                         .build();
13791         assertThat(documents).containsExactly(expectedText, expectedEmail);
13792     }
13793 
13794     @Test
13795     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SEARCH_RESULT_PARENT_TYPES)
testQuery_wildcardFilterSchema_polymorphism()13796     public void testQuery_wildcardFilterSchema_polymorphism() throws Exception {
13797         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
13798         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_RESULT_PARENT_TYPES));
13799 
13800         AppSearchSchema messageSchema =
13801                 new AppSearchSchema.Builder("Message")
13802                         .addProperty(
13803                                 new StringPropertyConfig.Builder("content")
13804                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
13805                                         .setIndexingType(
13806                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
13807                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
13808                                         .build())
13809                         .build();
13810         AppSearchSchema textSchema =
13811                 new AppSearchSchema.Builder("Text")
13812                         .addParentType("Message")
13813                         .addProperty(
13814                                 new StringPropertyConfig.Builder("content")
13815                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
13816                                         .setIndexingType(
13817                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
13818                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
13819                                         .build())
13820                         .addProperty(
13821                                 new StringPropertyConfig.Builder("carrier")
13822                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
13823                                         .setIndexingType(
13824                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
13825                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
13826                                         .build())
13827                         .build();
13828         AppSearchSchema emailSchema =
13829                 new AppSearchSchema.Builder("Email")
13830                         .addParentType("Message")
13831                         .addProperty(
13832                                 new StringPropertyConfig.Builder("content")
13833                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
13834                                         .setIndexingType(
13835                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
13836                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
13837                                         .build())
13838                         .addProperty(
13839                                 new StringPropertyConfig.Builder("attachment")
13840                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
13841                                         .setIndexingType(
13842                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
13843                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
13844                                         .build())
13845                         .build();
13846 
13847         // Schema registration
13848         mDb1.setSchemaAsync(
13849                         new SetSchemaRequest.Builder()
13850                                 .addSchemas(messageSchema, textSchema, emailSchema)
13851                                 .build())
13852                 .get();
13853 
13854         // Index two child documents
13855         GenericDocument text =
13856                 new GenericDocument.Builder<>("namespace", "id1", "Text")
13857                         .setCreationTimestampMillis(1000)
13858                         .setPropertyString("content", "Some note")
13859                         .setPropertyString("carrier", "Network Inc")
13860                         .build();
13861         GenericDocument email =
13862                 new GenericDocument.Builder<>("namespace", "id2", "Email")
13863                         .setCreationTimestampMillis(1000)
13864                         .setPropertyString("content", "Some note")
13865                         .setPropertyString("attachment", "Network report")
13866                         .build();
13867 
13868         checkIsBatchResultSuccess(
13869                 mDb1.putAsync(
13870                         new PutDocumentsRequest.Builder()
13871                                 .addGenericDocuments(email, text)
13872                                 .build()));
13873 
13874         // Both email and text would match for "Network", but only text should match as it is in the
13875         // right property
13876         SearchResultsShim searchResults =
13877                 mDb1.search(
13878                         "Network",
13879                         new SearchSpec.Builder()
13880                                 .addFilterSchemas("Message")
13881                                 .addFilterProperties(
13882                                         SearchSpec.SCHEMA_TYPE_WILDCARD,
13883                                         ImmutableList.of("carrier"))
13884                                 .build());
13885         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
13886 
13887         // We specified the parent document in the filter schemas, but only indexed child documents.
13888         // As we also specified a wildcard schema type projection, it should apply to the child docs
13889         // The content property must not appear. Also emailNoContent should not appear as we are
13890         // filter on the content property
13891         GenericDocument expectedText =
13892                 new GenericDocument.Builder<>("namespace", "id1", "Text")
13893                         .setCreationTimestampMillis(1000)
13894                         .setPropertyString("content", "Some note")
13895                         .setPropertyString("carrier", "Network Inc")
13896                         .build();
13897         assertThat(documents).containsExactly(expectedText);
13898     }
13899 
13900     @Test
13901     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCORABLE_PROPERTY)
testRankWithNonScorableProperty()13902     public void testRankWithNonScorableProperty() throws Exception {
13903         // TODO(b/379923400): Implement this test.
13904     }
13905 
13906     @Test
13907     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCORABLE_PROPERTY)
testRankWithInvalidPropertyName()13908     public void testRankWithInvalidPropertyName() throws Exception {
13909         // TODO(b/379923400): Implement this test.
13910     }
13911 }
13912