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