1 /* 2 * Copyright 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package androidx.appsearch.cts.app; 18 19 import static androidx.appsearch.app.AppSearchResult.RESULT_INVALID_ARGUMENT; 20 import static androidx.appsearch.app.AppSearchResult.RESULT_INVALID_SCHEMA; 21 import static androidx.appsearch.app.AppSearchResult.RESULT_NOT_FOUND; 22 import static androidx.appsearch.testutil.AppSearchTestUtils.checkIsBatchResultSuccess; 23 import static androidx.appsearch.testutil.AppSearchTestUtils.convertSearchResultsToDocuments; 24 import static androidx.appsearch.testutil.AppSearchTestUtils.doGet; 25 import static androidx.appsearch.testutil.AppSearchTestUtils.retrieveAllSearchResults; 26 27 import static com.google.common.truth.Truth.assertThat; 28 29 import static org.junit.Assert.assertThrows; 30 import static org.junit.Assume.assumeFalse; 31 import static org.junit.Assume.assumeTrue; 32 33 import android.content.Context; 34 35 import androidx.appsearch.app.AppSearchBatchResult; 36 import androidx.appsearch.app.AppSearchResult; 37 import androidx.appsearch.app.AppSearchSchema; 38 import androidx.appsearch.app.AppSearchSchema.BooleanPropertyConfig; 39 import androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig; 40 import androidx.appsearch.app.AppSearchSchema.DoublePropertyConfig; 41 import androidx.appsearch.app.AppSearchSchema.LongPropertyConfig; 42 import androidx.appsearch.app.AppSearchSchema.PropertyConfig; 43 import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig; 44 import androidx.appsearch.app.AppSearchSession; 45 import androidx.appsearch.app.EmbeddingVector; 46 import androidx.appsearch.app.Features; 47 import androidx.appsearch.app.GenericDocument; 48 import androidx.appsearch.app.GetByDocumentIdRequest; 49 import androidx.appsearch.app.GetSchemaResponse; 50 import androidx.appsearch.app.JoinSpec; 51 import androidx.appsearch.app.PackageIdentifier; 52 import androidx.appsearch.app.PropertyPath; 53 import androidx.appsearch.app.PutDocumentsRequest; 54 import androidx.appsearch.app.RemoveByDocumentIdRequest; 55 import androidx.appsearch.app.ReportUsageRequest; 56 import androidx.appsearch.app.SchemaVisibilityConfig; 57 import androidx.appsearch.app.SearchResult; 58 import androidx.appsearch.app.SearchResults; 59 import androidx.appsearch.app.SearchSpec; 60 import androidx.appsearch.app.SearchSuggestionResult; 61 import androidx.appsearch.app.SearchSuggestionSpec; 62 import androidx.appsearch.app.SetSchemaRequest; 63 import androidx.appsearch.app.StorageInfo; 64 import androidx.appsearch.cts.app.customer.EmailDocument; 65 import androidx.appsearch.exceptions.AppSearchException; 66 import androidx.appsearch.flags.Flags; 67 import androidx.appsearch.testutil.AppSearchEmail; 68 import androidx.appsearch.testutil.AppSearchTestUtils; 69 import androidx.appsearch.testutil.flags.RequiresFlagsEnabled; 70 import androidx.appsearch.usagereporting.ClickAction; 71 import androidx.appsearch.usagereporting.DismissAction; 72 import androidx.appsearch.usagereporting.ImpressionAction; 73 import androidx.appsearch.usagereporting.SearchAction; 74 import androidx.appsearch.util.DocumentIdUtil; 75 import androidx.collection.ArrayMap; 76 import androidx.test.core.app.ApplicationProvider; 77 78 import com.google.common.collect.ImmutableList; 79 import com.google.common.collect.ImmutableMap; 80 import com.google.common.collect.ImmutableSet; 81 import com.google.common.util.concurrent.ListenableFuture; 82 import com.google.common.util.concurrent.MoreExecutors; 83 84 import org.jspecify.annotations.NonNull; 85 import org.junit.After; 86 import org.junit.Before; 87 import org.junit.Rule; 88 import org.junit.Test; 89 import org.junit.rules.RuleChain; 90 91 import java.util.ArrayList; 92 import java.util.Arrays; 93 import java.util.Collections; 94 import java.util.HashSet; 95 import java.util.List; 96 import java.util.Map; 97 import java.util.Set; 98 import java.util.concurrent.ExecutionException; 99 import java.util.concurrent.ExecutorService; 100 101 public abstract class AppSearchSessionCtsTestBase { 102 static final String DB_NAME_1 = ""; 103 static final String DB_NAME_2 = "testDb2"; 104 105 // Since we cannot call non-public API in the cts test, make a copy of these 3 action types, so 106 // we can create taken actions in GenericDocument form. 107 private static final int ACTION_TYPE_SEARCH = 1; 108 private static final int ACTION_TYPE_CLICK = 2; 109 private static final int ACTION_TYPE_IMPRESSION = 3; 110 private static final int ACTION_TYPE_DISMISS = 4; 111 112 @Rule 113 public final RuleChain mRuleChain = AppSearchTestUtils.createCommonTestRules(); 114 115 private final Context mContext = ApplicationProvider.getApplicationContext(); 116 117 private AppSearchSession mDb1; 118 private AppSearchSession mDb2; 119 createSearchSessionAsync( @onNull String dbName)120 protected abstract ListenableFuture<AppSearchSession> createSearchSessionAsync( 121 @NonNull String dbName) throws Exception; 122 createSearchSessionAsync( @onNull String dbName, @NonNull ExecutorService executor)123 protected abstract ListenableFuture<AppSearchSession> createSearchSessionAsync( 124 @NonNull String dbName, @NonNull ExecutorService executor) throws Exception; 125 126 @Before setUp()127 public void setUp() throws Exception { 128 mDb1 = createSearchSessionAsync(DB_NAME_1).get(); 129 mDb2 = createSearchSessionAsync(DB_NAME_2).get(); 130 131 // Cleanup whatever documents may still exist in these databases. This is needed in 132 // addition to tearDown in case a test exited without completing properly. 133 cleanup(); 134 } 135 136 @After tearDown()137 public void tearDown() throws Exception { 138 // Cleanup whatever documents may still exist in these databases. 139 cleanup(); 140 } 141 cleanup()142 private void cleanup() throws Exception { 143 mDb1.setSchemaAsync( 144 new SetSchemaRequest.Builder().setForceOverride(true).build()).get(); 145 mDb2.setSchemaAsync( 146 new SetSchemaRequest.Builder().setForceOverride(true).build()).get(); 147 } 148 149 @Test testSetSchema()150 public void testSetSchema() throws Exception { 151 AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email") 152 .addProperty(new StringPropertyConfig.Builder("subject") 153 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 154 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 155 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 156 .build() 157 ).addProperty(new StringPropertyConfig.Builder("body") 158 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 159 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 160 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 161 .build() 162 ).build(); 163 mDb1.setSchemaAsync( 164 new SetSchemaRequest.Builder().addSchemas(emailSchema).build()).get(); 165 } 166 167 @Test testSetSchema_Failure()168 public void testSetSchema_Failure() throws Exception { 169 mDb1.setSchemaAsync( 170 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get(); 171 AppSearchSchema emailSchema1 = new AppSearchSchema.Builder(AppSearchEmail.SCHEMA_TYPE) 172 .build(); 173 174 SetSchemaRequest setSchemaRequest1 = 175 new SetSchemaRequest.Builder().addSchemas(emailSchema1).build(); 176 ExecutionException executionException = 177 assertThrows(ExecutionException.class, 178 () -> mDb1.setSchemaAsync(setSchemaRequest1).get()); 179 assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class); 180 AppSearchException exception = (AppSearchException) executionException.getCause(); 181 assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_SCHEMA); 182 assertThat(exception).hasMessageThat().contains("Schema is incompatible."); 183 assertThat(exception).hasMessageThat().contains("Incompatible types: {builtin:Email}"); 184 185 SetSchemaRequest setSchemaRequest2 = new SetSchemaRequest.Builder().build(); 186 executionException = assertThrows(ExecutionException.class, 187 () -> mDb1.setSchemaAsync(setSchemaRequest2).get()); 188 assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class); 189 exception = (AppSearchException) executionException.getCause(); 190 assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_SCHEMA); 191 assertThat(exception).hasMessageThat().contains("Schema is incompatible."); 192 assertThat(exception).hasMessageThat().contains("Deleted types: {builtin:Email}"); 193 } 194 195 @Test 196 @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_DESCRIPTION) // setDescription testSetSchema_schemaDescription_notSupported()197 public void testSetSchema_schemaDescription_notSupported() throws Exception { 198 assumeFalse(mDb1.getFeatures().isFeatureSupported( 199 Features.SCHEMA_SET_DESCRIPTION)); 200 AppSearchSchema schema = new AppSearchSchema.Builder("Email1") 201 .setDescription("Unsupported description") 202 .addProperty(new StringPropertyConfig.Builder("body") 203 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 204 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 205 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 206 .build() 207 ).build(); 208 209 SetSchemaRequest request = new SetSchemaRequest.Builder() 210 .addSchemas(schema) 211 .build(); 212 213 UnsupportedOperationException exception = assertThrows( 214 UnsupportedOperationException.class, 215 () -> mDb1.setSchemaAsync(request).get()); 216 assertThat(exception).hasMessageThat().contains(Features.SCHEMA_SET_DESCRIPTION 217 + " is not available on this AppSearch implementation."); 218 } 219 220 @Test 221 @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_DESCRIPTION) // setDescription testSetSchema_propertyDescription_notSupported()222 public void testSetSchema_propertyDescription_notSupported() throws Exception { 223 assumeFalse(mDb1.getFeatures().isFeatureSupported( 224 Features.SCHEMA_SET_DESCRIPTION)); 225 AppSearchSchema schema = new AppSearchSchema.Builder("Email1") 226 .addProperty(new StringPropertyConfig.Builder("body") 227 .setDescription("Unsupported description") 228 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 229 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 230 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 231 .build() 232 ).build(); 233 234 SetSchemaRequest request = new SetSchemaRequest.Builder() 235 .addSchemas(schema) 236 .build(); 237 238 UnsupportedOperationException exception = assertThrows( 239 UnsupportedOperationException.class, 240 () -> mDb1.setSchemaAsync(request).get()); 241 assertThat(exception).hasMessageThat().contains(Features.SCHEMA_SET_DESCRIPTION 242 + " is not available on this AppSearch implementation."); 243 } 244 245 @Test 246 @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_DESCRIPTION) // setDescription testSetSchema_updateSchemaDescription()247 public void testSetSchema_updateSchemaDescription() throws Exception { 248 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_SET_DESCRIPTION)); 249 250 AppSearchSchema schema1 = 251 new AppSearchSchema.Builder("Email") 252 .setDescription("A type of electronic message.") 253 .addProperty( 254 new StringPropertyConfig.Builder("subject") 255 .setDescription("A summary of the email.") 256 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 257 .setIndexingType( 258 StringPropertyConfig.INDEXING_TYPE_PREFIXES) 259 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 260 .build()) 261 .addProperty( 262 new StringPropertyConfig.Builder("body") 263 .setDescription("All of the content of the email.") 264 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 265 .setIndexingType( 266 StringPropertyConfig.INDEXING_TYPE_PREFIXES) 267 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 268 .build()) 269 .build(); 270 271 mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema1).build()) 272 .get(); 273 274 Set<AppSearchSchema> actualSchemaTypes = mDb1.getSchemaAsync().get().getSchemas(); 275 assertThat(actualSchemaTypes).containsExactly(schema1); 276 277 // Change the type description. 278 AppSearchSchema schema2 = 279 new AppSearchSchema.Builder("Email") 280 .setDescription("Like mail but with an 'a'.") 281 .addProperty( 282 new StringPropertyConfig.Builder("subject") 283 .setDescription("A summary of the email.") 284 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 285 .setIndexingType( 286 StringPropertyConfig.INDEXING_TYPE_PREFIXES) 287 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 288 .build()) 289 .addProperty( 290 new StringPropertyConfig.Builder("body") 291 .setDescription("All of the content of the email.") 292 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 293 .setIndexingType( 294 StringPropertyConfig.INDEXING_TYPE_PREFIXES) 295 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 296 .build()) 297 .build(); 298 299 mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema2).build()) 300 .get(); 301 302 GetSchemaResponse getSchemaResponse = mDb1.getSchemaAsync().get(); 303 assertThat(getSchemaResponse.getSchemas()).containsExactly(schema2); 304 } 305 306 @Test 307 @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_DESCRIPTION) // setDescription testSetSchema_updatePropertyDescription()308 public void testSetSchema_updatePropertyDescription() throws Exception { 309 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_SET_DESCRIPTION)); 310 311 AppSearchSchema schema1 = 312 new AppSearchSchema.Builder("Email") 313 .setDescription("A type of electronic message.") 314 .addProperty( 315 new StringPropertyConfig.Builder("subject") 316 .setDescription("A summary of the email.") 317 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 318 .setIndexingType( 319 StringPropertyConfig.INDEXING_TYPE_PREFIXES) 320 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 321 .build()) 322 .addProperty( 323 new StringPropertyConfig.Builder("body") 324 .setDescription("All of the content of the email.") 325 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 326 .setIndexingType( 327 StringPropertyConfig.INDEXING_TYPE_PREFIXES) 328 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 329 .build()) 330 .build(); 331 332 mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema1).build()) 333 .get(); 334 335 Set<AppSearchSchema> actualSchemaTypes = mDb1.getSchemaAsync().get().getSchemas(); 336 assertThat(actualSchemaTypes).containsExactly(schema1); 337 338 // Change the type description. 339 AppSearchSchema schema2 = 340 new AppSearchSchema.Builder("Email") 341 .setDescription("A type of electronic message.") 342 .addProperty( 343 new StringPropertyConfig.Builder("subject") 344 .setDescription("The most important part of the email.") 345 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 346 .setIndexingType( 347 StringPropertyConfig.INDEXING_TYPE_PREFIXES) 348 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 349 .build()) 350 .addProperty( 351 new StringPropertyConfig.Builder("body") 352 .setDescription("All the other stuff.") 353 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 354 .setIndexingType( 355 StringPropertyConfig.INDEXING_TYPE_PREFIXES) 356 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 357 .build()) 358 .build(); 359 360 mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema2).build()) 361 .get(); 362 363 GetSchemaResponse getSchemaResponse = mDb1.getSchemaAsync().get(); 364 assertThat(getSchemaResponse.getSchemas()).containsExactly(schema2); 365 } 366 367 @Test testSetSchema_updateVersion()368 public void testSetSchema_updateVersion() throws Exception { 369 AppSearchSchema schema = new AppSearchSchema.Builder("Email") 370 .addProperty(new StringPropertyConfig.Builder("subject") 371 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 372 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 373 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 374 .build() 375 ).addProperty(new StringPropertyConfig.Builder("body") 376 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 377 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 378 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 379 .build() 380 ).build(); 381 382 mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema) 383 .setVersion(1).build()).get(); 384 385 Set<AppSearchSchema> actualSchemaTypes = mDb1.getSchemaAsync().get().getSchemas(); 386 assertThat(actualSchemaTypes).containsExactly(schema); 387 388 // increase version number 389 mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema) 390 .setVersion(2).build()).get(); 391 392 GetSchemaResponse getSchemaResponse = mDb1.getSchemaAsync().get(); 393 assertThat(getSchemaResponse.getSchemas()).containsExactly(schema); 394 assertThat(getSchemaResponse.getVersion()).isEqualTo(2); 395 } 396 397 @Test testSetSchema_checkVersion()398 public void testSetSchema_checkVersion() throws Exception { 399 AppSearchSchema schema = new AppSearchSchema.Builder("Email") 400 .addProperty(new StringPropertyConfig.Builder("subject") 401 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 402 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 403 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 404 .build() 405 ).addProperty(new StringPropertyConfig.Builder("body") 406 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 407 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 408 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 409 .build() 410 ).build(); 411 412 // set different version number to different database. 413 mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema) 414 .setVersion(135).build()).get(); 415 mDb2.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema) 416 .setVersion(246).build()).get(); 417 418 419 // check the version has been set correctly. 420 GetSchemaResponse getSchemaResponse = mDb1.getSchemaAsync().get(); 421 assertThat(getSchemaResponse.getSchemas()).containsExactly(schema); 422 assertThat(getSchemaResponse.getVersion()).isEqualTo(135); 423 424 getSchemaResponse = mDb2.getSchemaAsync().get(); 425 assertThat(getSchemaResponse.getSchemas()).containsExactly(schema); 426 assertThat(getSchemaResponse.getVersion()).isEqualTo(246); 427 } 428 429 @Test testSetSchema_addIndexedNestedDocumentProperty()430 public void testSetSchema_addIndexedNestedDocumentProperty() throws Exception { 431 // Create schema with a nested document type 432 // SectionId assignment for 'Person': 433 // - "name": string type, indexed. Section id = 0. 434 // - "worksFor.name": string type, (nested) indexed. Section id = 1. 435 AppSearchSchema personSchema = new AppSearchSchema.Builder("Person") 436 .addProperty(new StringPropertyConfig.Builder("name") 437 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 438 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 439 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 440 .build()) 441 .addProperty(new DocumentPropertyConfig.Builder("worksFor", "Organization") 442 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 443 .setShouldIndexNestedProperties(true) 444 .build()) 445 .build(); 446 AppSearchSchema organizationSchema = new AppSearchSchema.Builder("Organization") 447 .addProperty(new StringPropertyConfig.Builder("name") 448 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 449 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 450 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 451 .build()) 452 .build(); 453 mDb1.setSchemaAsync( 454 new SetSchemaRequest.Builder().addSchemas(personSchema, organizationSchema) 455 .build()).get(); 456 457 // Index documents and verify using getDocuments 458 GenericDocument person = new GenericDocument.Builder<>("namespace", "person1", "Person") 459 .setPropertyString("name", "John") 460 .setPropertyDocument("worksFor", 461 new GenericDocument.Builder<>("namespace", "org1", "Organization") 462 .setPropertyString("name", "Google") 463 .build()) 464 .build(); 465 466 AppSearchBatchResult<String, Void> putResult = 467 checkIsBatchResultSuccess(mDb1.putAsync( 468 new PutDocumentsRequest.Builder().addGenericDocuments(person).build())); 469 assertThat(putResult.getSuccesses()).containsExactly("person1", null); 470 assertThat(putResult.getFailures()).isEmpty(); 471 472 GetByDocumentIdRequest getByDocumentIdRequest = 473 new GetByDocumentIdRequest.Builder("namespace") 474 .addIds("person1") 475 .build(); 476 List<GenericDocument> outDocuments = doGet(mDb1, getByDocumentIdRequest); 477 assertThat(outDocuments).hasSize(1); 478 assertThat(outDocuments).containsExactly(person); 479 480 // Verify search using property filter 481 SearchResults searchResults = mDb1.search("worksFor.name:Google", new SearchSpec.Builder() 482 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 483 .build()); 484 outDocuments = convertSearchResultsToDocuments(searchResults); 485 assertThat(outDocuments).hasSize(1); 486 assertThat(outDocuments).containsExactly(person); 487 488 // Change the schema to add another nested document property to 'Person' 489 // The added property has 'optional' cardinality, so this change is compatible and indexed 490 // documents should still be searchable. 491 // 492 // New section id assignment for 'Person': 493 // - "almaMater.name", string type, (nested) indexed. Section id = 0 494 // - "name": string type, indexed. Section id = 1 495 // - "worksFor.name": string type, (nested) indexed. Section id = 2 496 AppSearchSchema newPersonSchema = new AppSearchSchema.Builder("Person") 497 .addProperty(new StringPropertyConfig.Builder("name") 498 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 499 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 500 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 501 .build()) 502 .addProperty(new DocumentPropertyConfig.Builder("worksFor", "Organization") 503 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 504 .setShouldIndexNestedProperties(true) 505 .build()) 506 .addProperty(new DocumentPropertyConfig.Builder("almaMater", "Organization") 507 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 508 .setShouldIndexNestedProperties(true) 509 .build()) 510 .build(); 511 mDb1.setSchemaAsync( 512 new SetSchemaRequest.Builder().addSchemas(newPersonSchema, organizationSchema) 513 .build()).get(); 514 Set<AppSearchSchema> outSchemaTypes = mDb1.getSchemaAsync().get().getSchemas(); 515 assertThat(outSchemaTypes).containsExactly(newPersonSchema, organizationSchema); 516 517 getByDocumentIdRequest = new GetByDocumentIdRequest.Builder("namespace") 518 .addIds("person1") 519 .build(); 520 outDocuments = doGet(mDb1, getByDocumentIdRequest); 521 assertThat(outDocuments).hasSize(1); 522 assertThat(outDocuments).containsExactly(person); 523 524 // Verify that index rebuild was triggered correctly. The same query "worksFor.name:Google" 525 // should still match the same result. 526 searchResults = mDb1.search("worksFor.name:Google", new SearchSpec.Builder() 527 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 528 .build()); 529 outDocuments = convertSearchResultsToDocuments(searchResults); 530 assertThat(outDocuments).hasSize(1); 531 assertThat(outDocuments).containsExactly(person); 532 533 // In new_schema the 'name' property is now indexed at section id 1. If searching for 534 // "name:Google" matched the document, this means that index rebuild was not triggered 535 // correctly and Icing is still searching the old index, where 'worksFor.name' was 536 // indexed at section id 1. 537 searchResults = mDb1.search("name:Google", new SearchSpec.Builder() 538 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 539 .build()); 540 outDocuments = convertSearchResultsToDocuments(searchResults); 541 assertThat(outDocuments).isEmpty(); 542 } 543 544 @Test testSetSchemaWithValidCycle_allowCircularReferences()545 public void testSetSchemaWithValidCycle_allowCircularReferences() throws Exception { 546 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SET_SCHEMA_CIRCULAR_REFERENCES)); 547 548 // Create schema with valid cycle: Person -> Organization -> Person... 549 AppSearchSchema personSchema = new AppSearchSchema.Builder("Person") 550 .addProperty(new StringPropertyConfig.Builder("name") 551 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 552 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 553 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 554 .build()) 555 .addProperty(new DocumentPropertyConfig.Builder("worksFor", "Organization") 556 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 557 .setShouldIndexNestedProperties(true) 558 .build()) 559 .build(); 560 AppSearchSchema organizationSchema = new AppSearchSchema.Builder("Organization") 561 .addProperty(new StringPropertyConfig.Builder("name") 562 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 563 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 564 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 565 .build()) 566 .addProperty(new DocumentPropertyConfig.Builder("funder", "Person") 567 .setCardinality(PropertyConfig.CARDINALITY_REPEATED) 568 .setShouldIndexNestedProperties(false) 569 .build()) 570 .build(); 571 mDb1.setSchemaAsync( 572 new SetSchemaRequest.Builder().addSchemas(personSchema, organizationSchema) 573 .build()).get(); 574 575 // Test that documents following the circular schema are indexable, and that its sections 576 // are searchable 577 GenericDocument person = new GenericDocument.Builder<>("namespace", "person1", "Person") 578 .setPropertyString("name", "John") 579 .setPropertyDocument("worksFor") 580 .build(); 581 GenericDocument org = new GenericDocument.Builder<>("namespace", "org1", "Organization") 582 .setPropertyString("name", "Org") 583 .setPropertyDocument("funder", person) 584 .build(); 585 GenericDocument person2 = new GenericDocument.Builder<>("namespace", "person2", "Person") 586 .setPropertyString("name", "Jane") 587 .setPropertyDocument("worksFor", org) 588 .build(); 589 590 AppSearchBatchResult<String, Void> putResult = 591 checkIsBatchResultSuccess(mDb1.putAsync( 592 new PutDocumentsRequest.Builder().addGenericDocuments(person, org, 593 person2).build())); 594 assertThat(putResult.getSuccesses()).containsExactly("person1", null, "org1", null, 595 "person2", null); 596 assertThat(putResult.getFailures()).isEmpty(); 597 598 GetByDocumentIdRequest getByDocumentIdRequest = 599 new GetByDocumentIdRequest.Builder("namespace") 600 .addIds("person1", "person2", "org1") 601 .build(); 602 List<GenericDocument> outDocuments = doGet(mDb1, getByDocumentIdRequest); 603 assertThat(outDocuments).hasSize(3); 604 assertThat(outDocuments).containsExactly(person, person2, org); 605 606 // Both org1 and person2 should be returned for query "Org" 607 // For org1 this matches the 'name' property and for person2 this matches the 608 // 'worksFor.name' property. 609 SearchResults searchResults = mDb1.search("Org", new SearchSpec.Builder() 610 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 611 .build()); 612 outDocuments = convertSearchResultsToDocuments(searchResults); 613 assertThat(outDocuments).hasSize(2); 614 assertThat(outDocuments).containsExactly(person2, org); 615 } 616 617 @Test testSetSchemaWithInvalidCycle_circularReferencesSupported()618 public void testSetSchemaWithInvalidCycle_circularReferencesSupported() throws Exception { 619 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SET_SCHEMA_CIRCULAR_REFERENCES)); 620 621 // Create schema with invalid cycle: Person -> Organization -> Person... where all 622 // DocumentPropertyConfigs have setShouldIndexNestedProperties(true). 623 AppSearchSchema personSchema = new AppSearchSchema.Builder("Person") 624 .addProperty(new StringPropertyConfig.Builder("name") 625 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 626 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 627 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 628 .build()) 629 .addProperty(new DocumentPropertyConfig.Builder("worksFor", "Organization") 630 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 631 .setShouldIndexNestedProperties(true) 632 .build()) 633 .build(); 634 AppSearchSchema organizationSchema = new AppSearchSchema.Builder("Organization") 635 .addProperty(new StringPropertyConfig.Builder("name") 636 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 637 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 638 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 639 .build()) 640 .addProperty(new DocumentPropertyConfig.Builder("funder", "Person") 641 .setCardinality(PropertyConfig.CARDINALITY_REPEATED) 642 .setShouldIndexNestedProperties(true) 643 .build()) 644 .build(); 645 646 SetSchemaRequest setSchemaRequest = 647 new SetSchemaRequest.Builder().addSchemas(personSchema, organizationSchema).build(); 648 ExecutionException executionException = 649 assertThrows(ExecutionException.class, 650 () -> mDb1.setSchemaAsync(setSchemaRequest).get()); 651 assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class); 652 AppSearchException exception = (AppSearchException) executionException.getCause(); 653 assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT); 654 assertThat(exception).hasMessageThat().containsMatch("Invalid cycle|Infinite loop"); 655 } 656 657 @Test testSetSchemaWithValidCycle_circularReferencesNotSupported()658 public void testSetSchemaWithValidCycle_circularReferencesNotSupported() { 659 assumeFalse(mDb1.getFeatures().isFeatureSupported(Features.SET_SCHEMA_CIRCULAR_REFERENCES)); 660 661 // Create schema with valid cycle: Person -> Organization -> Person... 662 AppSearchSchema personSchema = new AppSearchSchema.Builder("Person") 663 .addProperty(new StringPropertyConfig.Builder("name") 664 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 665 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 666 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 667 .build()) 668 .addProperty(new DocumentPropertyConfig.Builder("worksFor", "Organization") 669 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 670 .setShouldIndexNestedProperties(true) 671 .build()) 672 .build(); 673 AppSearchSchema organizationSchema = new AppSearchSchema.Builder("Organization") 674 .addProperty(new StringPropertyConfig.Builder("name") 675 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 676 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 677 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 678 .build()) 679 .addProperty(new DocumentPropertyConfig.Builder("funder", "Person") 680 .setCardinality(PropertyConfig.CARDINALITY_REPEATED) 681 .setShouldIndexNestedProperties(false) 682 .build()) 683 .build(); 684 685 SetSchemaRequest setSchemaRequest = 686 new SetSchemaRequest.Builder().addSchemas(personSchema, organizationSchema).build(); 687 ExecutionException executionException = 688 assertThrows(ExecutionException.class, 689 () -> mDb1.setSchemaAsync(setSchemaRequest).get()); 690 assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class); 691 AppSearchException exception = (AppSearchException) executionException.getCause(); 692 assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT); 693 assertThat(exception).hasMessageThat().containsMatch("Invalid cycle|Infinite loop"); 694 } 695 696 // @exportToFramework:startStrip() 697 698 @Test testSetSchema_addDocumentClasses()699 public void testSetSchema_addDocumentClasses() throws Exception { 700 mDb1.setSchemaAsync(new SetSchemaRequest.Builder() 701 .addDocumentClasses(EmailDocument.class).build()).get(); 702 } 703 // @exportToFramework:endStrip() 704 705 /** Test indexing maximum properties into a schema. */ 706 @Test testSetSchema_maxProperties()707 public void testSetSchema_maxProperties() throws Exception { 708 int maxProperties = mDb1.getFeatures().getMaxIndexedProperties(); 709 AppSearchSchema.Builder schemaBuilder = new AppSearchSchema.Builder("testSchema"); 710 for (int i = 0; i < maxProperties; i++) { 711 schemaBuilder.addProperty(new StringPropertyConfig.Builder("string" + i) 712 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 713 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 714 .build()); 715 } 716 AppSearchSchema maxSchema = schemaBuilder.build(); 717 mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(maxSchema).build()).get(); 718 Set<AppSearchSchema> actual1 = mDb1.getSchemaAsync().get().getSchemas(); 719 assertThat(actual1).containsExactly(maxSchema); 720 721 schemaBuilder.addProperty(new StringPropertyConfig.Builder("toomuch") 722 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 723 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 724 .build()); 725 ExecutionException exception = assertThrows(ExecutionException.class, () -> 726 mDb1.setSchemaAsync(new SetSchemaRequest.Builder() 727 .addSchemas(schemaBuilder.build()).setForceOverride(true).build()).get()); 728 Throwable cause = exception.getCause(); 729 assertThat(cause).isInstanceOf(AppSearchException.class); 730 assertThat(cause.getMessage()).isEqualTo("Too many properties to be indexed, max " 731 + "number of properties allowed: " + maxProperties); 732 } 733 734 @Test testSetSchema_maxProperties_nestedSchemas()735 public void testSetSchema_maxProperties_nestedSchemas() throws Exception { 736 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SET_SCHEMA_CIRCULAR_REFERENCES)); 737 738 int maxProperties = mDb1.getFeatures().getMaxIndexedProperties(); 739 AppSearchSchema.Builder personSchemaBuilder = new AppSearchSchema.Builder("Person"); 740 for (int i = 0; i < maxProperties / 3 + 1; i++) { 741 personSchemaBuilder.addProperty(new StringPropertyConfig.Builder("string" + i) 742 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 743 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 744 .build()); 745 } 746 personSchemaBuilder.addProperty(new DocumentPropertyConfig.Builder("worksFor", 747 "Organization") 748 .setShouldIndexNestedProperties(false) 749 .build()); 750 personSchemaBuilder.addProperty(new DocumentPropertyConfig.Builder("address", "Address") 751 .setShouldIndexNestedProperties(true) 752 .build()); 753 754 AppSearchSchema.Builder orgSchemaBuilder = new AppSearchSchema.Builder("Organization"); 755 for (int i = 0; i < maxProperties / 3; i++) { 756 orgSchemaBuilder.addProperty(new StringPropertyConfig.Builder("string" + i) 757 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 758 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 759 .build()); 760 } 761 orgSchemaBuilder.addProperty(new DocumentPropertyConfig.Builder("funder", "Person") 762 .setShouldIndexNestedProperties(true) 763 .build()); 764 765 AppSearchSchema.Builder addressSchemaBuilder = new AppSearchSchema.Builder("Address"); 766 for (int i = 0; i < maxProperties / 3; i++) { 767 addressSchemaBuilder.addProperty(new StringPropertyConfig.Builder("string" + i) 768 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 769 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 770 .build()); 771 } 772 773 AppSearchSchema personSchema = personSchemaBuilder.build(); 774 AppSearchSchema orgSchema = orgSchemaBuilder.build(); 775 AppSearchSchema addressSchema = addressSchemaBuilder.build(); 776 mDb1.setSchemaAsync( 777 new SetSchemaRequest.Builder() 778 .addSchemas(personSchema, orgSchema, addressSchema) 779 .build()).get(); 780 Set<AppSearchSchema> schemas = mDb1.getSchemaAsync().get().getSchemas(); 781 assertThat(schemas).containsExactly(personSchema, orgSchema, addressSchema); 782 783 // Add one more property to bring the number of sections over the max limit 784 personSchemaBuilder.addProperty(new StringPropertyConfig.Builder("toomuch") 785 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 786 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 787 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 788 .build()); 789 ExecutionException exception = assertThrows(ExecutionException.class, 790 () -> mDb1.setSchemaAsync( 791 new SetSchemaRequest.Builder() 792 .addSchemas(personSchemaBuilder.build(), orgSchema, addressSchema) 793 .setForceOverride(true) 794 .build() 795 ).get()); 796 Throwable cause = exception.getCause(); 797 assertThat(cause).isInstanceOf(AppSearchException.class); 798 assertThat(cause.getMessage()).contains("Too many properties to be indexed"); 799 } 800 801 // @exportToFramework:startStrip() 802 803 @Test testGetSchema()804 public void testGetSchema() throws Exception { 805 AppSearchSchema emailSchema1 = new AppSearchSchema.Builder("Email1") 806 .addProperty(new StringPropertyConfig.Builder("subject") 807 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 808 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 809 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 810 .build() 811 ).addProperty(new StringPropertyConfig.Builder("body") 812 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 813 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 814 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 815 .build() 816 ).build(); 817 AppSearchSchema emailSchema2 = new AppSearchSchema.Builder("Email2") 818 .addProperty(new StringPropertyConfig.Builder("subject") 819 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 820 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) // Diff 821 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 822 .build() 823 ).addProperty(new StringPropertyConfig.Builder("body") 824 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 825 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) // Diff 826 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 827 .build() 828 ).build(); 829 830 SetSchemaRequest request1 = new SetSchemaRequest.Builder() 831 .addSchemas(emailSchema1).addDocumentClasses(EmailDocument.class).build(); 832 SetSchemaRequest request2 = new SetSchemaRequest.Builder() 833 .addSchemas(emailSchema2).addDocumentClasses(EmailDocument.class).build(); 834 835 mDb1.setSchemaAsync(request1).get(); 836 mDb2.setSchemaAsync(request2).get(); 837 838 Set<AppSearchSchema> actual1 = mDb1.getSchemaAsync().get().getSchemas(); 839 assertThat(actual1).hasSize(2); 840 assertThat(actual1).isEqualTo(request1.getSchemas()); 841 Set<AppSearchSchema> actual2 = mDb2.getSchemaAsync().get().getSchemas(); 842 assertThat(actual2).hasSize(2); 843 assertThat(actual2).isEqualTo(request2.getSchemas()); 844 } 845 // @exportToFramework:endStrip() 846 847 @Test testGetSchema_allPropertyTypes()848 public void testGetSchema_allPropertyTypes() throws Exception { 849 AppSearchSchema inSchema = new AppSearchSchema.Builder("Test") 850 .addProperty(new StringPropertyConfig.Builder("string") 851 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 852 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 853 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 854 .build()) 855 .addProperty(new AppSearchSchema.LongPropertyConfig.Builder("long") 856 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 857 .build()) 858 .addProperty(new AppSearchSchema.DoublePropertyConfig.Builder("double") 859 .setCardinality(PropertyConfig.CARDINALITY_REPEATED) 860 .build()) 861 .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder("boolean") 862 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 863 .build()) 864 .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder("bytes") 865 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 866 .build()) 867 .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder( 868 "document", AppSearchEmail.SCHEMA_TYPE) 869 .setCardinality(PropertyConfig.CARDINALITY_REPEATED) 870 .setShouldIndexNestedProperties(true) 871 .build()) 872 .build(); 873 874 // Add it to AppSearch and then obtain it again 875 mDb1.setSchemaAsync(new SetSchemaRequest.Builder() 876 .addSchemas(inSchema, AppSearchEmail.SCHEMA).build()).get(); 877 GetSchemaResponse response = mDb1.getSchemaAsync().get(); 878 List<AppSearchSchema> schemas = new ArrayList<>(response.getSchemas()); 879 assertThat(schemas).containsExactly(inSchema, AppSearchEmail.SCHEMA); 880 AppSearchSchema outSchema; 881 if (schemas.get(0).getSchemaType().equals("Test")) { 882 outSchema = schemas.get(0); 883 } else { 884 outSchema = schemas.get(1); 885 } 886 assertThat(outSchema.getSchemaType()).isEqualTo("Test"); 887 assertThat(outSchema).isNotSameInstanceAs(inSchema); 888 889 List<PropertyConfig> properties = outSchema.getProperties(); 890 assertThat(properties).hasSize(6); 891 892 assertThat(properties.get(0).getName()).isEqualTo("string"); 893 assertThat(properties.get(0).getCardinality()) 894 .isEqualTo(PropertyConfig.CARDINALITY_REQUIRED); 895 assertThat(((StringPropertyConfig) properties.get(0)).getIndexingType()) 896 .isEqualTo(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS); 897 assertThat(((StringPropertyConfig) properties.get(0)).getTokenizerType()) 898 .isEqualTo(StringPropertyConfig.TOKENIZER_TYPE_PLAIN); 899 900 assertThat(properties.get(1).getName()).isEqualTo("long"); 901 assertThat(properties.get(1).getCardinality()) 902 .isEqualTo(PropertyConfig.CARDINALITY_OPTIONAL); 903 assertThat(properties.get(1)).isInstanceOf(AppSearchSchema.LongPropertyConfig.class); 904 905 assertThat(properties.get(2).getName()).isEqualTo("double"); 906 assertThat(properties.get(2).getCardinality()) 907 .isEqualTo(PropertyConfig.CARDINALITY_REPEATED); 908 assertThat(properties.get(2)).isInstanceOf(AppSearchSchema.DoublePropertyConfig.class); 909 910 assertThat(properties.get(3).getName()).isEqualTo("boolean"); 911 assertThat(properties.get(3).getCardinality()) 912 .isEqualTo(PropertyConfig.CARDINALITY_REQUIRED); 913 assertThat(properties.get(3)).isInstanceOf(AppSearchSchema.BooleanPropertyConfig.class); 914 915 assertThat(properties.get(4).getName()).isEqualTo("bytes"); 916 assertThat(properties.get(4).getCardinality()) 917 .isEqualTo(PropertyConfig.CARDINALITY_OPTIONAL); 918 assertThat(properties.get(4)).isInstanceOf(AppSearchSchema.BytesPropertyConfig.class); 919 920 assertThat(properties.get(5).getName()).isEqualTo("document"); 921 assertThat(properties.get(5).getCardinality()) 922 .isEqualTo(PropertyConfig.CARDINALITY_REPEATED); 923 assertThat(((AppSearchSchema.DocumentPropertyConfig) properties.get(5)).getSchemaType()) 924 .isEqualTo(AppSearchEmail.SCHEMA_TYPE); 925 assertThat(((AppSearchSchema.DocumentPropertyConfig) properties.get(5)) 926 .shouldIndexNestedProperties()).isEqualTo(true); 927 } 928 929 @Test testGetSchema_visibilitySetting()930 public void testGetSchema_visibilitySetting() throws Exception { 931 assumeTrue(mDb1.getFeatures().isFeatureSupported( 932 Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)); 933 AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email1") 934 .addProperty(new StringPropertyConfig.Builder("subject") 935 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 936 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 937 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 938 .build() 939 ).addProperty(new StringPropertyConfig.Builder("body") 940 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 941 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 942 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 943 .build() 944 ).build(); 945 946 byte[] shar256Cert1 = new byte[32]; 947 Arrays.fill(shar256Cert1, (byte) 1); 948 byte[] shar256Cert2 = new byte[32]; 949 Arrays.fill(shar256Cert2, (byte) 2); 950 PackageIdentifier packageIdentifier1 = 951 new PackageIdentifier("pkgFoo", shar256Cert1); 952 PackageIdentifier packageIdentifier2 = 953 new PackageIdentifier("pkgBar", shar256Cert2); 954 SetSchemaRequest request = new SetSchemaRequest.Builder() 955 .addSchemas(emailSchema) 956 .setSchemaTypeDisplayedBySystem("Email1", /*displayed=*/false) 957 .setSchemaTypeVisibilityForPackage("Email1", /*visible=*/true, 958 packageIdentifier1) 959 .setSchemaTypeVisibilityForPackage("Email1", /*visible=*/true, 960 packageIdentifier2) 961 .addRequiredPermissionsForSchemaTypeVisibility("Email1", 962 ImmutableSet.of(SetSchemaRequest.READ_SMS, SetSchemaRequest.READ_CALENDAR)) 963 .addRequiredPermissionsForSchemaTypeVisibility("Email1", 964 ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA)) 965 .build(); 966 967 mDb1.setSchemaAsync(request).get(); 968 969 GetSchemaResponse getSchemaResponse = mDb1.getSchemaAsync().get(); 970 Set<AppSearchSchema> actual = getSchemaResponse.getSchemas(); 971 assertThat(actual).hasSize(1); 972 assertThat(actual).isEqualTo(request.getSchemas()); 973 assertThat(getSchemaResponse.getSchemaTypesNotDisplayedBySystem()) 974 .containsExactly("Email1"); 975 assertThat(getSchemaResponse.getSchemaTypesVisibleToPackages()) 976 .containsExactly("Email1", ImmutableSet.of( 977 packageIdentifier1, packageIdentifier2)); 978 assertThat(getSchemaResponse.getRequiredPermissionsForSchemaTypeVisibility()) 979 .containsExactly("Email1", ImmutableSet.of( 980 ImmutableSet.of(SetSchemaRequest.READ_SMS, 981 SetSchemaRequest.READ_CALENDAR), 982 ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA))); 983 } 984 985 @Test testGetSchema_visibilitySetting_oneSharedSchema()986 public void testGetSchema_visibilitySetting_oneSharedSchema() throws Exception { 987 assumeTrue(mDb1.getFeatures().isFeatureSupported( 988 Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)); 989 990 AppSearchSchema noteSchema = new AppSearchSchema.Builder("Note") 991 .addProperty(new StringPropertyConfig.Builder("subject").build()).build(); 992 SetSchemaRequest.Builder requestBuilder = new SetSchemaRequest.Builder() 993 .addSchemas(AppSearchEmail.SCHEMA, noteSchema) 994 .setSchemaTypeDisplayedBySystem(noteSchema.getSchemaType(), false) 995 .setSchemaTypeVisibilityForPackage( 996 noteSchema.getSchemaType(), 997 true, 998 new PackageIdentifier("com.some.package1", new byte[32])) 999 .addRequiredPermissionsForSchemaTypeVisibility( 1000 noteSchema.getSchemaType(), 1001 Collections.singleton(SetSchemaRequest.READ_SMS)); 1002 if (mDb1.getFeatures().isFeatureSupported( 1003 Features.SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE)) { 1004 requestBuilder.setPubliclyVisibleSchema( 1005 noteSchema.getSchemaType(), 1006 new PackageIdentifier("com.some.package2", new byte[32])); 1007 } 1008 SetSchemaRequest request = requestBuilder.build(); 1009 mDb1.setSchemaAsync(request).get(); 1010 1011 GetSchemaResponse getSchemaResponse = mDb1.getSchemaAsync().get(); 1012 Set<AppSearchSchema> actual = getSchemaResponse.getSchemas(); 1013 assertThat(actual).hasSize(2); 1014 assertThat(actual).isEqualTo(request.getSchemas()); 1015 1016 // Check visibility settings. Schemas without settings shouldn't appear in the result at 1017 // all, even with empty maps as values. 1018 assertThat(getSchemaResponse.getSchemaTypesNotDisplayedBySystem()) 1019 .containsExactly(noteSchema.getSchemaType()); 1020 assertThat(getSchemaResponse.getSchemaTypesVisibleToPackages()) 1021 .containsExactly( 1022 noteSchema.getSchemaType(), 1023 ImmutableSet.of(new PackageIdentifier("com.some.package1", new byte[32]))); 1024 assertThat(getSchemaResponse.getRequiredPermissionsForSchemaTypeVisibility()) 1025 .containsExactly( 1026 noteSchema.getSchemaType(), 1027 ImmutableSet.of(ImmutableSet.of(SetSchemaRequest.READ_SMS))); 1028 if (mDb1.getFeatures().isFeatureSupported( 1029 Features.SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE)) { 1030 assertThat(getSchemaResponse.getPubliclyVisibleSchemas()) 1031 .containsExactly( 1032 noteSchema.getSchemaType(), 1033 new PackageIdentifier("com.some.package2", new byte[32])); 1034 } 1035 } 1036 1037 @Test testGetSchema_visibilitySetting_notSupported()1038 public void testGetSchema_visibilitySetting_notSupported() throws Exception { 1039 assumeFalse(mDb1.getFeatures().isFeatureSupported( 1040 Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)); 1041 AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email1") 1042 .addProperty(new StringPropertyConfig.Builder("subject") 1043 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 1044 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 1045 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 1046 .build() 1047 ).addProperty(new StringPropertyConfig.Builder("body") 1048 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 1049 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 1050 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 1051 .build() 1052 ).build(); 1053 1054 byte[] shar256Cert1 = new byte[32]; 1055 Arrays.fill(shar256Cert1, (byte) 1); 1056 byte[] shar256Cert2 = new byte[32]; 1057 Arrays.fill(shar256Cert2, (byte) 2); 1058 PackageIdentifier packageIdentifier1 = 1059 new PackageIdentifier("pkgFoo", shar256Cert1); 1060 PackageIdentifier packageIdentifier2 = 1061 new PackageIdentifier("pkgBar", shar256Cert2); 1062 SetSchemaRequest request = new SetSchemaRequest.Builder() 1063 .addSchemas(emailSchema) 1064 .setSchemaTypeDisplayedBySystem("Email1", /*displayed=*/false) 1065 .setSchemaTypeVisibilityForPackage("Email1", /*visible=*/true, 1066 packageIdentifier1) 1067 .setSchemaTypeVisibilityForPackage("Email1", /*visible=*/true, 1068 packageIdentifier2) 1069 .build(); 1070 1071 mDb1.setSchemaAsync(request).get(); 1072 1073 GetSchemaResponse getSchemaResponse = mDb1.getSchemaAsync().get(); 1074 Set<AppSearchSchema> actual = getSchemaResponse.getSchemas(); 1075 assertThat(actual).hasSize(1); 1076 assertThat(actual).isEqualTo(request.getSchemas()); 1077 assertThrows( 1078 UnsupportedOperationException.class, 1079 () -> getSchemaResponse.getSchemaTypesNotDisplayedBySystem()); 1080 assertThrows( 1081 UnsupportedOperationException.class, 1082 () -> getSchemaResponse.getSchemaTypesVisibleToPackages()); 1083 assertThrows( 1084 UnsupportedOperationException.class, 1085 () -> getSchemaResponse.getRequiredPermissionsForSchemaTypeVisibility()); 1086 } 1087 1088 @Test testSetSchema_visibilitySettingPermission_notSupported()1089 public void testSetSchema_visibilitySettingPermission_notSupported() { 1090 assumeFalse(mDb1.getFeatures().isFeatureSupported( 1091 Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)); 1092 AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email1").build(); 1093 1094 SetSchemaRequest request = new SetSchemaRequest.Builder() 1095 .addSchemas(emailSchema) 1096 .setSchemaTypeDisplayedBySystem("Email1", /*displayed=*/false) 1097 .addRequiredPermissionsForSchemaTypeVisibility("Email1", 1098 ImmutableSet.of(SetSchemaRequest.READ_SMS)) 1099 .build(); 1100 1101 assertThrows(UnsupportedOperationException.class, () -> 1102 mDb1.setSchemaAsync(request).get()); 1103 } 1104 1105 @Test testSetSchema_publiclyVisible()1106 public void testSetSchema_publiclyVisible() throws Exception { 1107 assumeTrue(mDb1.getFeatures() 1108 .isFeatureSupported(Features.SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE)); 1109 1110 PackageIdentifier pkg = new PackageIdentifier(mContext.getPackageName(), new byte[32]); 1111 SetSchemaRequest request = new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA) 1112 .setPubliclyVisibleSchema("builtin:Email", pkg).build(); 1113 1114 mDb1.setSchemaAsync(request).get(); 1115 GetSchemaResponse getSchemaResponse = mDb1.getSchemaAsync().get(); 1116 1117 assertThat(getSchemaResponse.getSchemas()).containsExactly(AppSearchEmail.SCHEMA); 1118 assertThat(getSchemaResponse.getPubliclyVisibleSchemas()) 1119 .isEqualTo(ImmutableMap.of("builtin:Email", pkg)); 1120 1121 AppSearchEmail email = new AppSearchEmail.Builder("namespace", "id1") 1122 .setSubject("testPut example").build(); 1123 1124 // mDb1 and mDb2 are in the same package, so we can't REALLY test out public acl. But we 1125 // can make sure they their own documents under the Public ACL. 1126 AppSearchBatchResult<String, Void> putResult = 1127 checkIsBatchResultSuccess(mDb1.putAsync( 1128 new PutDocumentsRequest.Builder().addGenericDocuments(email).build())); 1129 assertThat(putResult.getSuccesses()).containsExactly("id1", null); 1130 assertThat(putResult.getFailures()).isEmpty(); 1131 1132 GetByDocumentIdRequest getByDocumentIdRequest = 1133 new GetByDocumentIdRequest.Builder("namespace") 1134 .addIds("id1") 1135 .build(); 1136 List<GenericDocument> outDocuments = doGet(mDb1, getByDocumentIdRequest); 1137 assertThat(outDocuments).hasSize(1); 1138 assertThat(outDocuments).containsExactly(email); 1139 } 1140 1141 @Test testSetSchema_publiclyVisible_unsupported()1142 public void testSetSchema_publiclyVisible_unsupported() { 1143 assumeFalse(mDb1.getFeatures() 1144 .isFeatureSupported(Features.SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE)); 1145 1146 SetSchemaRequest request = new SetSchemaRequest.Builder() 1147 .addSchemas(new AppSearchSchema.Builder("Email").build()) 1148 .setPubliclyVisibleSchema("Email", 1149 new PackageIdentifier(mContext.getPackageName(), new byte[32])).build(); 1150 Exception e = assertThrows(UnsupportedOperationException.class, 1151 () -> mDb1.setSchemaAsync(request).get()); 1152 assertThat(e.getMessage()).isEqualTo("Publicly visible schema are not supported on this " 1153 + "AppSearch implementation."); 1154 } 1155 1156 @Test testSetSchema_visibleToConfig()1157 public void testSetSchema_visibleToConfig() throws Exception { 1158 assumeTrue(mDb1.getFeatures() 1159 .isFeatureSupported(Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG)); 1160 byte[] cert1 = new byte[32]; 1161 byte[] cert2 = new byte[32]; 1162 Arrays.fill(cert1, (byte) 1); 1163 Arrays.fill(cert2, (byte) 2); 1164 PackageIdentifier pkg1 = new PackageIdentifier("package1", cert1); 1165 PackageIdentifier pkg2 = new PackageIdentifier("package2", cert2); 1166 SchemaVisibilityConfig config1 = new SchemaVisibilityConfig.Builder() 1167 .setPubliclyVisibleTargetPackage(pkg1) 1168 .addRequiredPermissions(ImmutableSet.of(1, 2)).build(); 1169 SchemaVisibilityConfig config2 = new SchemaVisibilityConfig.Builder() 1170 .setPubliclyVisibleTargetPackage(pkg2) 1171 .addRequiredPermissions(ImmutableSet.of(3, 4)).build(); 1172 SetSchemaRequest request = new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA) 1173 .addSchemaTypeVisibleToConfig("builtin:Email", config1) 1174 .addSchemaTypeVisibleToConfig("builtin:Email", config2) 1175 .build(); 1176 mDb1.setSchemaAsync(request).get(); 1177 1178 GetSchemaResponse getSchemaResponse = mDb1.getSchemaAsync().get(); 1179 assertThat(getSchemaResponse.getSchemas()).containsExactly(AppSearchEmail.SCHEMA); 1180 assertThat(getSchemaResponse.getSchemaTypesVisibleToConfigs()) 1181 .isEqualTo(ImmutableMap.of("builtin:Email", ImmutableSet.of(config1, config2))); 1182 } 1183 1184 @Test testSetSchema_visibleToConfig_unsupported()1185 public void testSetSchema_visibleToConfig_unsupported() { 1186 assumeFalse(mDb1.getFeatures() 1187 .isFeatureSupported(Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG)); 1188 1189 SchemaVisibilityConfig config = new SchemaVisibilityConfig.Builder() 1190 .addRequiredPermissions(ImmutableSet.of(1, 2)).build(); 1191 SetSchemaRequest request = new SetSchemaRequest.Builder() 1192 .addSchemas(new AppSearchSchema.Builder("Email").build()) 1193 .addSchemaTypeVisibleToConfig("Email", config).build(); 1194 Exception e = assertThrows(UnsupportedOperationException.class, 1195 () -> mDb1.setSchemaAsync(request).get()); 1196 assertThat(e.getMessage()).isEqualTo("Schema visible to config are not supported on" 1197 + " this AppSearch implementation."); 1198 } 1199 1200 @Test testGetSchema_longPropertyIndexingType()1201 public void testGetSchema_longPropertyIndexingType() throws Exception { 1202 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.NUMERIC_SEARCH)); 1203 AppSearchSchema inSchema = new AppSearchSchema.Builder("Test") 1204 .addProperty(new LongPropertyConfig.Builder("indexableLong") 1205 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 1206 .setIndexingType(LongPropertyConfig.INDEXING_TYPE_RANGE) 1207 .build() 1208 ).addProperty(new LongPropertyConfig.Builder("long") 1209 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 1210 .setIndexingType(LongPropertyConfig.INDEXING_TYPE_NONE) 1211 .build() 1212 ).build(); 1213 1214 SetSchemaRequest request = new SetSchemaRequest.Builder() 1215 .addSchemas(inSchema).build(); 1216 1217 mDb1.setSchemaAsync(request).get(); 1218 1219 Set<AppSearchSchema> actual = mDb1.getSchemaAsync().get().getSchemas(); 1220 assertThat(actual).hasSize(1); 1221 assertThat(actual).containsExactlyElementsIn(request.getSchemas()); 1222 } 1223 1224 @Test testGetSchema_longPropertyIndexingTypeNone_succeeds()1225 public void testGetSchema_longPropertyIndexingTypeNone_succeeds() throws Exception { 1226 AppSearchSchema inSchema = new AppSearchSchema.Builder("Test") 1227 .addProperty(new LongPropertyConfig.Builder("long") 1228 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 1229 .setIndexingType(LongPropertyConfig.INDEXING_TYPE_NONE) 1230 .build() 1231 ).build(); 1232 1233 SetSchemaRequest request = new SetSchemaRequest.Builder() 1234 .addSchemas(inSchema).build(); 1235 1236 mDb1.setSchemaAsync(request).get(); 1237 1238 Set<AppSearchSchema> actual = mDb1.getSchemaAsync().get().getSchemas(); 1239 assertThat(actual).hasSize(1); 1240 assertThat(actual).containsExactlyElementsIn(request.getSchemas()); 1241 } 1242 1243 @Test testGetSchema_longPropertyIndexingTypeRange_notSupported()1244 public void testGetSchema_longPropertyIndexingTypeRange_notSupported() throws Exception { 1245 assumeFalse(mDb1.getFeatures().isFeatureSupported(Features.NUMERIC_SEARCH)); 1246 AppSearchSchema inSchema = new AppSearchSchema.Builder("Test") 1247 .addProperty(new LongPropertyConfig.Builder("indexableLong") 1248 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 1249 .setIndexingType(LongPropertyConfig.INDEXING_TYPE_RANGE) 1250 .build() 1251 ).build(); 1252 1253 SetSchemaRequest request = new SetSchemaRequest.Builder() 1254 .addSchemas(inSchema).build(); 1255 1256 UnsupportedOperationException e = assertThrows(UnsupportedOperationException.class, () -> 1257 mDb1.setSchemaAsync(request).get()); 1258 assertThat(e.getMessage()).isEqualTo("LongProperty.INDEXING_TYPE_RANGE is not " 1259 + "supported on this AppSearch implementation."); 1260 } 1261 1262 @Test testGetSchema_joinableValueType()1263 public void testGetSchema_joinableValueType() throws Exception { 1264 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID)); 1265 AppSearchSchema inSchema = new AppSearchSchema.Builder("Test") 1266 .addProperty(new StringPropertyConfig.Builder("normalStr") 1267 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 1268 .build() 1269 ).addProperty(new StringPropertyConfig.Builder("optionalQualifiedIdStr") 1270 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 1271 .setJoinableValueType(StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID) 1272 .build() 1273 ).addProperty(new StringPropertyConfig.Builder("requiredQualifiedIdStr") 1274 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 1275 .setJoinableValueType(StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID) 1276 .build() 1277 ).build(); 1278 1279 SetSchemaRequest request = new SetSchemaRequest.Builder() 1280 .addSchemas(inSchema).build(); 1281 1282 mDb1.setSchemaAsync(request).get(); 1283 1284 Set<AppSearchSchema> actual = mDb1.getSchemaAsync().get().getSchemas(); 1285 assertThat(actual).hasSize(1); 1286 assertThat(actual).containsExactlyElementsIn(request.getSchemas()); 1287 } 1288 1289 @Test testGetSchema_joinableValueTypeNone_succeeds()1290 public void testGetSchema_joinableValueTypeNone_succeeds() throws Exception { 1291 AppSearchSchema inSchema = new AppSearchSchema.Builder("Test") 1292 .addProperty(new StringPropertyConfig.Builder("optionalString") 1293 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 1294 .setJoinableValueType(StringPropertyConfig.JOINABLE_VALUE_TYPE_NONE) 1295 .build() 1296 ).addProperty(new StringPropertyConfig.Builder("requiredString") 1297 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 1298 .setJoinableValueType(StringPropertyConfig.JOINABLE_VALUE_TYPE_NONE) 1299 .build() 1300 ).addProperty(new StringPropertyConfig.Builder("repeatedString") 1301 .setCardinality(PropertyConfig.CARDINALITY_REPEATED) 1302 .setJoinableValueType(StringPropertyConfig.JOINABLE_VALUE_TYPE_NONE) 1303 .build() 1304 ).build(); 1305 1306 SetSchemaRequest request = new SetSchemaRequest.Builder() 1307 .addSchemas(inSchema).build(); 1308 1309 mDb1.setSchemaAsync(request).get(); 1310 1311 Set<AppSearchSchema> actual = mDb1.getSchemaAsync().get().getSchemas(); 1312 assertThat(actual).hasSize(1); 1313 assertThat(actual).containsExactlyElementsIn(request.getSchemas()); 1314 } 1315 1316 @Test testGetSchema_joinableValueTypeQualifiedId_notSupported()1317 public void testGetSchema_joinableValueTypeQualifiedId_notSupported() throws Exception { 1318 assumeFalse(mDb1.getFeatures().isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID)); 1319 AppSearchSchema inSchema = new AppSearchSchema.Builder("Test") 1320 .addProperty(new StringPropertyConfig.Builder("qualifiedId") 1321 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 1322 .setJoinableValueType(StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID) 1323 .build() 1324 ).build(); 1325 1326 SetSchemaRequest request = new SetSchemaRequest.Builder() 1327 .addSchemas(inSchema).build(); 1328 1329 UnsupportedOperationException e = assertThrows(UnsupportedOperationException.class, () -> 1330 mDb1.setSchemaAsync(request).get()); 1331 assertThat(e.getMessage()).isEqualTo( 1332 "StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID is not supported on this " 1333 + "AppSearch implementation."); 1334 } 1335 1336 @Test testGetNamespaces()1337 public void testGetNamespaces() throws Exception { 1338 // Schema registration 1339 mDb1.setSchemaAsync( 1340 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get(); 1341 assertThat(mDb1.getNamespacesAsync().get()).isEmpty(); 1342 1343 // Index a document 1344 checkIsBatchResultSuccess(mDb1.putAsync(new PutDocumentsRequest.Builder() 1345 .addGenericDocuments(new AppSearchEmail.Builder("namespace1", "id1").build()) 1346 .build())); 1347 assertThat(mDb1.getNamespacesAsync().get()).containsExactly("namespace1"); 1348 1349 // Index additional data 1350 checkIsBatchResultSuccess(mDb1.putAsync(new PutDocumentsRequest.Builder() 1351 .addGenericDocuments( 1352 new AppSearchEmail.Builder("namespace2", "id1").build(), 1353 new AppSearchEmail.Builder("namespace2", "id2").build(), 1354 new AppSearchEmail.Builder("namespace3", "id1").build()) 1355 .build())); 1356 assertThat(mDb1.getNamespacesAsync().get()).containsExactly( 1357 "namespace1", "namespace2", "namespace3"); 1358 1359 // Remove namespace2/id2 -- namespace2 should still exist because of namespace2/id1 1360 checkIsBatchResultSuccess( 1361 mDb1.removeAsync(new RemoveByDocumentIdRequest.Builder("namespace2").addIds( 1362 "id2").build())); 1363 assertThat(mDb1.getNamespacesAsync().get()).containsExactly( 1364 "namespace1", "namespace2", "namespace3"); 1365 1366 // Remove namespace2/id1 -- namespace2 should now be gone 1367 checkIsBatchResultSuccess( 1368 mDb1.removeAsync(new RemoveByDocumentIdRequest.Builder("namespace2").addIds( 1369 "id1").build())); 1370 assertThat(mDb1.getNamespacesAsync().get()).containsExactly("namespace1", "namespace3"); 1371 1372 // Make sure the list of namespaces is preserved after restart 1373 mDb1.close(); 1374 mDb1 = createSearchSessionAsync(DB_NAME_1).get(); 1375 assertThat(mDb1.getNamespacesAsync().get()).containsExactly("namespace1", "namespace3"); 1376 } 1377 1378 @Test testGetNamespaces_dbIsolation()1379 public void testGetNamespaces_dbIsolation() throws Exception { 1380 // Schema registration 1381 mDb1.setSchemaAsync( 1382 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get(); 1383 mDb2.setSchemaAsync( 1384 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get(); 1385 assertThat(mDb1.getNamespacesAsync().get()).isEmpty(); 1386 assertThat(mDb2.getNamespacesAsync().get()).isEmpty(); 1387 1388 // Index documents 1389 checkIsBatchResultSuccess(mDb1.putAsync(new PutDocumentsRequest.Builder() 1390 .addGenericDocuments(new AppSearchEmail.Builder("namespace1_db1", "id1").build()) 1391 .build())); 1392 checkIsBatchResultSuccess(mDb1.putAsync(new PutDocumentsRequest.Builder() 1393 .addGenericDocuments(new AppSearchEmail.Builder("namespace2_db1", "id1").build()) 1394 .build())); 1395 checkIsBatchResultSuccess(mDb2.putAsync(new PutDocumentsRequest.Builder() 1396 .addGenericDocuments(new AppSearchEmail.Builder("namespace_db2", "id1").build()) 1397 .build())); 1398 assertThat(mDb1.getNamespacesAsync().get()) 1399 .containsExactly("namespace1_db1", "namespace2_db1"); 1400 assertThat(mDb2.getNamespacesAsync().get()).containsExactly("namespace_db2"); 1401 1402 // Make sure the list of namespaces is preserved after restart 1403 mDb1.close(); 1404 mDb1 = createSearchSessionAsync(DB_NAME_1).get(); 1405 assertThat(mDb1.getNamespacesAsync().get()) 1406 .containsExactly("namespace1_db1", "namespace2_db1"); 1407 assertThat(mDb2.getNamespacesAsync().get()).containsExactly("namespace_db2"); 1408 } 1409 1410 @Test testGetSchema_emptyDB()1411 public void testGetSchema_emptyDB() throws Exception { 1412 GetSchemaResponse getSchemaResponse = mDb1.getSchemaAsync().get(); 1413 assertThat(getSchemaResponse.getVersion()).isEqualTo(0); 1414 } 1415 1416 @Test testPutDocuments()1417 public void testPutDocuments() throws Exception { 1418 // Schema registration 1419 mDb1.setSchemaAsync( 1420 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get(); 1421 1422 // Index a document 1423 AppSearchEmail email = new AppSearchEmail.Builder("namespace", "id1") 1424 .setFrom("from@example.com") 1425 .setTo("to1@example.com", "to2@example.com") 1426 .setSubject("testPut example") 1427 .setBody("This is the body of the testPut email") 1428 .build(); 1429 1430 AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb1.putAsync( 1431 new PutDocumentsRequest.Builder().addGenericDocuments(email).build())); 1432 assertThat(result.getSuccesses()).containsExactly("id1", null); 1433 assertThat(result.getFailures()).isEmpty(); 1434 } 1435 1436 @Test testPutDocuments_emptyProperties()1437 public void testPutDocuments_emptyProperties() throws Exception { 1438 // Schema registration. Due to b/204677124 is fixed in Android T. We have different 1439 // behaviour when set empty array to bytes and documents between local and platform storage. 1440 // This test only test String, long, boolean and double, for byte array and Document will be 1441 // test in backend's specific test. 1442 AppSearchSchema schema = new AppSearchSchema.Builder("testSchema") 1443 .addProperty(new StringPropertyConfig.Builder("string") 1444 .setCardinality(PropertyConfig.CARDINALITY_REPEATED) 1445 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 1446 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 1447 .build()) 1448 .addProperty(new AppSearchSchema.LongPropertyConfig.Builder("long") 1449 .setCardinality(PropertyConfig.CARDINALITY_REPEATED) 1450 .build()) 1451 .addProperty(new AppSearchSchema.DoublePropertyConfig.Builder("double") 1452 .setCardinality(PropertyConfig.CARDINALITY_REPEATED) 1453 .build()) 1454 .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder("boolean") 1455 .setCardinality(PropertyConfig.CARDINALITY_REPEATED) 1456 .build()) 1457 .build(); 1458 mDb1.setSchemaAsync(new SetSchemaRequest.Builder() 1459 .addSchemas(schema, AppSearchEmail.SCHEMA).build()).get(); 1460 1461 // Index a document 1462 GenericDocument document = new GenericDocument.Builder<>("namespace", "id1", "testSchema") 1463 .setPropertyBoolean("boolean") 1464 .setPropertyString("string") 1465 .setPropertyDouble("double") 1466 .setPropertyLong("long") 1467 .build(); 1468 1469 AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb1.putAsync( 1470 new PutDocumentsRequest.Builder().addGenericDocuments(document).build())); 1471 assertThat(result.getSuccesses()).containsExactly("id1", null); 1472 assertThat(result.getFailures()).isEmpty(); 1473 1474 GetByDocumentIdRequest request = new GetByDocumentIdRequest.Builder("namespace") 1475 .addIds("id1") 1476 .build(); 1477 List<GenericDocument> outDocuments = doGet(mDb1, request); 1478 assertThat(outDocuments).hasSize(1); 1479 GenericDocument outDocument = outDocuments.get(0); 1480 assertThat(outDocument.getPropertyBooleanArray("boolean")).isEmpty(); 1481 assertThat(outDocument.getPropertyStringArray("string")).isEmpty(); 1482 assertThat(outDocument.getPropertyDoubleArray("double")).isEmpty(); 1483 assertThat(outDocument.getPropertyLongArray("long")).isEmpty(); 1484 } 1485 1486 @Test testPutLargeDocumentBatch()1487 public void testPutLargeDocumentBatch() throws Exception { 1488 // Schema registration 1489 AppSearchSchema schema = new AppSearchSchema.Builder("Type").addProperty( 1490 new StringPropertyConfig.Builder("body") 1491 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 1492 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 1493 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 1494 .build()) 1495 .build(); 1496 mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get(); 1497 1498 // Creates a large batch of Documents, since we have max document size in Framework which is 1499 // 512KiB, we will create 1KiB * 4000 docs = 4MiB total size > 1MiB binder transaction limit 1500 char[] chars = new char[1024]; // 1KiB 1501 Arrays.fill(chars, ' '); 1502 String body = String.valueOf(chars) + "the end."; 1503 List<GenericDocument> inDocuments = new ArrayList<>(); 1504 GetByDocumentIdRequest.Builder getByDocumentIdRequestBuilder = 1505 new GetByDocumentIdRequest.Builder("namespace"); 1506 for (int i = 0; i < 4000; i++) { 1507 GenericDocument inDocument = new GenericDocument.Builder<>( 1508 "namespace", "id" + i, "Type") 1509 .setPropertyString("body", body) 1510 .build(); 1511 inDocuments.add(inDocument); 1512 getByDocumentIdRequestBuilder.addIds("id" + i); 1513 } 1514 1515 // Index documents. 1516 AppSearchBatchResult<String, Void> result = 1517 mDb1.putAsync(new PutDocumentsRequest.Builder().addGenericDocuments(inDocuments) 1518 .build()).get(); 1519 assertThat(result.isSuccess()).isTrue(); 1520 1521 // Query those documents and verify they are same with the input. This also verify 1522 // AppSearchResult could handle large batch. 1523 SearchResults searchResults = mDb1.search("end", new SearchSpec.Builder() 1524 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 1525 .setResultCountPerPage(4000) 1526 .build()); 1527 List<GenericDocument> outDocuments = convertSearchResultsToDocuments(searchResults); 1528 1529 // Create a map to assert the output is same to the input in O(n). 1530 // containsExactlyElementsIn will create two iterators and the complexity is O(n^2). 1531 Map<String, GenericDocument> outMap = new ArrayMap<>(outDocuments.size()); 1532 for (int i = 0; i < outDocuments.size(); i++) { 1533 outMap.put(outDocuments.get(i).getId(), outDocuments.get(i)); 1534 } 1535 for (int i = 0; i < inDocuments.size(); i++) { 1536 GenericDocument inDocument = inDocuments.get(i); 1537 assertThat(inDocument).isEqualTo(outMap.get(inDocument.getId())); 1538 outMap.remove(inDocument.getId()); 1539 } 1540 assertThat(outMap).isEmpty(); 1541 1542 // Get by document ID and verify they are same with the input. This also verify 1543 // AppSearchBatchResult could handle large batch. 1544 AppSearchBatchResult<String, GenericDocument> batchResult = mDb1.getByDocumentIdAsync( 1545 getByDocumentIdRequestBuilder.build()).get(); 1546 assertThat(batchResult.isSuccess()).isTrue(); 1547 for (int i = 0; i < inDocuments.size(); i++) { 1548 GenericDocument inDocument = inDocuments.get(i); 1549 assertThat(batchResult.getSuccesses().get(inDocument.getId())).isEqualTo(inDocument); 1550 } 1551 } 1552 1553 // @exportToFramework:startStrip() 1554 1555 @Test testPut_addDocumentClasses()1556 public void testPut_addDocumentClasses() throws Exception { 1557 // Schema registration 1558 mDb1.setSchemaAsync(new SetSchemaRequest.Builder() 1559 .addDocumentClasses(EmailDocument.class).build()).get(); 1560 1561 // Index a document 1562 EmailDocument email = new EmailDocument(); 1563 email.namespace = "namespace"; 1564 email.id = "id1"; 1565 email.subject = "testPut example"; 1566 email.body = "This is the body of the testPut email"; 1567 1568 AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb1.putAsync( 1569 new PutDocumentsRequest.Builder().addDocuments(email).build())); 1570 assertThat(result.getSuccesses()).containsExactly("id1", null); 1571 assertThat(result.getFailures()).isEmpty(); 1572 } 1573 1574 @Test testPutDocuments_takenActions()1575 public void testPutDocuments_takenActions() throws Exception { 1576 assumeTrue(mDb1.getFeatures() 1577 .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID)); 1578 1579 // Schema registration 1580 mDb1.setSchemaAsync( 1581 new SetSchemaRequest.Builder() 1582 .addDocumentClasses(SearchAction.class, ClickAction.class, 1583 ImpressionAction.class, DismissAction.class) 1584 .build()) 1585 .get(); 1586 1587 // Put a SearchAction and ClickAction document 1588 SearchAction searchAction = 1589 new SearchAction.Builder("namespace", "search", /* actionTimestampMillis= */1000) 1590 .setDocumentTtlMillis(0) 1591 .setQuery("query") 1592 .setFetchedResultCount(10) 1593 .build(); 1594 ClickAction clickAction = 1595 new ClickAction.Builder("namespace", "click", /* actionTimestampMillis= */2000) 1596 .setDocumentTtlMillis(0) 1597 .setQuery("query") 1598 .setReferencedQualifiedId("pkg$db/ns#refId1") 1599 .setResultRankInBlock(1) 1600 .setResultRankGlobal(3) 1601 .setTimeStayOnResultMillis(1024) 1602 .build(); 1603 ImpressionAction impressionAction = 1604 new ImpressionAction.Builder( 1605 "namespace", "impression", /* actionTimestampMillis= */3000) 1606 .setDocumentTtlMillis(0) 1607 .setQuery("query") 1608 .setReferencedQualifiedId("pkg$db/ns#refId2") 1609 .setResultRankInBlock(2) 1610 .setResultRankGlobal(4) 1611 .build(); 1612 DismissAction dismissAction = 1613 new DismissAction.Builder( 1614 "namespace", "dismiss", /* actionTimestampMillis= */4000) 1615 .setDocumentTtlMillis(0) 1616 .setQuery("query") 1617 .setReferencedQualifiedId("pkg$db/ns#refId3") 1618 .setResultRankInBlock(3) 1619 .setResultRankGlobal(5) 1620 .build(); 1621 1622 AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb1.putAsync( 1623 new PutDocumentsRequest.Builder() 1624 .addTakenActions(searchAction, clickAction, impressionAction, dismissAction) 1625 .build())); 1626 assertThat(result.getSuccesses()).containsEntry("search", null); 1627 assertThat(result.getSuccesses()).containsEntry("click", null); 1628 assertThat(result.getSuccesses()).containsEntry("impression", null); 1629 assertThat(result.getSuccesses()).containsEntry("dismiss", null); 1630 assertThat(result.getFailures()).isEmpty(); 1631 } 1632 // @exportToFramework:endStrip() 1633 1634 @Test testPutDocuments_takenActionGenericDocuments()1635 public void testPutDocuments_takenActionGenericDocuments() throws Exception { 1636 assumeTrue(mDb1.getFeatures() 1637 .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID)); 1638 1639 // Schema registration 1640 AppSearchSchema searchActionSchema = new AppSearchSchema.Builder("builtin:SearchAction") 1641 .addProperty(new LongPropertyConfig.Builder("actionType") 1642 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 1643 .build()) 1644 .addProperty(new StringPropertyConfig.Builder("query") 1645 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 1646 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 1647 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 1648 .build() 1649 ).build(); 1650 AppSearchSchema clickActionSchema = new AppSearchSchema.Builder("builtin:ClickAction") 1651 .addProperty(new LongPropertyConfig.Builder("actionType") 1652 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 1653 .build()) 1654 .addProperty(new StringPropertyConfig.Builder("query") 1655 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 1656 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 1657 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 1658 .build() 1659 ).addProperty(new StringPropertyConfig.Builder("referencedQualifiedId") 1660 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 1661 .setJoinableValueType(StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID) 1662 .build() 1663 ).build(); 1664 AppSearchSchema impressionActionSchema = 1665 new AppSearchSchema.Builder("builtin:ImpressionAction") 1666 .addProperty(new LongPropertyConfig.Builder("actionType") 1667 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 1668 .build()) 1669 .addProperty(new StringPropertyConfig.Builder("query") 1670 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 1671 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 1672 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 1673 .build() 1674 ).addProperty(new StringPropertyConfig.Builder("referencedQualifiedId") 1675 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 1676 .setJoinableValueType( 1677 StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID) 1678 .build() 1679 ).build(); 1680 AppSearchSchema dismissActionSchema = 1681 new AppSearchSchema.Builder("builtin:DismissAction") 1682 .addProperty(new LongPropertyConfig.Builder("actionType") 1683 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 1684 .build()) 1685 .addProperty(new StringPropertyConfig.Builder("query") 1686 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 1687 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 1688 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 1689 .build() 1690 ).addProperty(new StringPropertyConfig.Builder("referencedQualifiedId") 1691 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 1692 .setJoinableValueType( 1693 StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID) 1694 .build() 1695 ).build(); 1696 1697 mDb1.setSchemaAsync( 1698 new SetSchemaRequest.Builder().addSchemas(searchActionSchema, clickActionSchema, 1699 impressionActionSchema, dismissActionSchema) 1700 .build()).get(); 1701 1702 // Put search action, click action and impression action generic documents. 1703 GenericDocument searchAction = 1704 new GenericDocument.Builder<>("namespace", "search", "builtin:SearchAction") 1705 .setCreationTimestampMillis(1000) 1706 .setPropertyLong("actionType", ACTION_TYPE_SEARCH) 1707 .setPropertyString("query", "body") 1708 .build(); 1709 GenericDocument clickAction = 1710 new GenericDocument.Builder<>("namespace", "click", "builtin:ClickAction") 1711 .setCreationTimestampMillis(2000) 1712 .setPropertyLong("actionType", ACTION_TYPE_CLICK) 1713 .setPropertyString("query", "body") 1714 .setPropertyString("referencedQualifiedId", "pkg$db/ns#refId1") 1715 .build(); 1716 GenericDocument impressionAction = 1717 new GenericDocument.Builder<>("namespace", "impression", "builtin:ImpressionAction") 1718 .setCreationTimestampMillis(3000) 1719 .setPropertyLong("actionType", ACTION_TYPE_IMPRESSION) 1720 .setPropertyString("query", "body") 1721 .setPropertyString("referencedQualifiedId", "pkg$db/ns#refId2") 1722 .build(); 1723 GenericDocument dismissAction = 1724 new GenericDocument.Builder<>("namespace", "dismiss", "builtin:DismissAction") 1725 .setCreationTimestampMillis(4000) 1726 .setPropertyLong("actionType", ACTION_TYPE_DISMISS) 1727 .setPropertyString("query", "body") 1728 .setPropertyString("referencedQualifiedId", "pkg$db/ns#refId3") 1729 .build(); 1730 1731 AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb1.putAsync( 1732 new PutDocumentsRequest.Builder() 1733 .addTakenActionGenericDocuments(searchAction, clickAction, impressionAction, 1734 dismissAction) 1735 .build())); 1736 assertThat(result.getSuccesses()).containsEntry("search", null); 1737 assertThat(result.getSuccesses()).containsEntry("click", null); 1738 assertThat(result.getSuccesses()).containsEntry("impression", null); 1739 assertThat(result.getSuccesses()).containsEntry("dismiss", null); 1740 assertThat(result.getFailures()).isEmpty(); 1741 } 1742 1743 @Test testUpdateSchema()1744 public void testUpdateSchema() throws Exception { 1745 // Schema registration 1746 AppSearchSchema oldEmailSchema = new AppSearchSchema.Builder(AppSearchEmail.SCHEMA_TYPE) 1747 .addProperty(new StringPropertyConfig.Builder("subject") 1748 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 1749 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 1750 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 1751 .build()) 1752 .build(); 1753 AppSearchSchema newEmailSchema = new AppSearchSchema.Builder(AppSearchEmail.SCHEMA_TYPE) 1754 .addProperty(new StringPropertyConfig.Builder("subject") 1755 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 1756 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 1757 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 1758 .build()) 1759 .addProperty(new StringPropertyConfig.Builder("body") 1760 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 1761 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 1762 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 1763 .build()) 1764 .build(); 1765 AppSearchSchema giftSchema = new AppSearchSchema.Builder("Gift") 1766 .addProperty(new AppSearchSchema.LongPropertyConfig.Builder("price") 1767 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 1768 .build()) 1769 .build(); 1770 mDb1.setSchemaAsync( 1771 new SetSchemaRequest.Builder().addSchemas(oldEmailSchema).build()).get(); 1772 1773 // Try to index a gift. This should fail as it's not in the schema. 1774 GenericDocument gift = 1775 new GenericDocument.Builder<>("namespace", "gift1", "Gift").setPropertyLong("price", 1776 5).build(); 1777 AppSearchBatchResult<String, Void> result = 1778 mDb1.putAsync( 1779 new PutDocumentsRequest.Builder().addGenericDocuments(gift).build()).get(); 1780 assertThat(result.isSuccess()).isFalse(); 1781 assertThat(result.getFailures().get("gift1").getResultCode()) 1782 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND); 1783 1784 // Update the schema to include the gift and update email with a new field 1785 mDb1.setSchemaAsync(new SetSchemaRequest.Builder() 1786 .addSchemas(newEmailSchema, giftSchema).build()).get(); 1787 1788 // Try to index the document again, which should now work 1789 checkIsBatchResultSuccess( 1790 mDb1.putAsync( 1791 new PutDocumentsRequest.Builder().addGenericDocuments(gift).build())); 1792 1793 // Indexing an email with a body should also work 1794 AppSearchEmail email = new AppSearchEmail.Builder("namespace", "email1") 1795 .setSubject("testPut example") 1796 .setBody("This is the body of the testPut email") 1797 .build(); 1798 checkIsBatchResultSuccess( 1799 mDb1.putAsync( 1800 new PutDocumentsRequest.Builder().addGenericDocuments(email).build())); 1801 } 1802 1803 @Test testRemoveSchema()1804 public void testRemoveSchema() throws Exception { 1805 // Schema registration 1806 AppSearchSchema emailSchema = new AppSearchSchema.Builder(AppSearchEmail.SCHEMA_TYPE) 1807 .addProperty(new StringPropertyConfig.Builder("subject") 1808 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 1809 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 1810 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 1811 .build()) 1812 .build(); 1813 mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(emailSchema).build()).get(); 1814 1815 // Index an email and check it present. 1816 AppSearchEmail email = new AppSearchEmail.Builder("namespace", "email1") 1817 .setSubject("testPut example") 1818 .build(); 1819 checkIsBatchResultSuccess( 1820 mDb1.putAsync( 1821 new PutDocumentsRequest.Builder().addGenericDocuments(email).build())); 1822 List<GenericDocument> outDocuments = 1823 doGet(mDb1, "namespace", "email1"); 1824 assertThat(outDocuments).hasSize(1); 1825 AppSearchEmail outEmail = new AppSearchEmail(outDocuments.get(0)); 1826 assertThat(outEmail).isEqualTo(email); 1827 1828 // Try to remove the email schema. This should fail as it's an incompatible change. 1829 SetSchemaRequest setSchemaRequest = new SetSchemaRequest.Builder().build(); 1830 ExecutionException executionException = assertThrows(ExecutionException.class, 1831 () -> mDb1.setSchemaAsync(setSchemaRequest).get()); 1832 assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class); 1833 AppSearchException failResult1 = (AppSearchException) executionException.getCause(); 1834 assertThat(failResult1).hasMessageThat().contains("Schema is incompatible"); 1835 assertThat(failResult1).hasMessageThat().contains( 1836 "Deleted types: {builtin:Email}"); 1837 1838 // Try to remove the email schema again, which should now work as we set forceOverride to 1839 // be true. 1840 mDb1.setSchemaAsync(new SetSchemaRequest.Builder().setForceOverride(true).build()).get(); 1841 1842 // Make sure the indexed email is gone. 1843 AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentIdAsync( 1844 new GetByDocumentIdRequest.Builder("namespace") 1845 .addIds("email1") 1846 .build()).get(); 1847 assertThat(getResult.isSuccess()).isFalse(); 1848 assertThat(getResult.getFailures().get("email1").getResultCode()) 1849 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND); 1850 1851 // Try to index an email again. This should fail as the schema has been removed. 1852 AppSearchEmail email2 = new AppSearchEmail.Builder("namespace", "email2") 1853 .setSubject("testPut example") 1854 .build(); 1855 AppSearchBatchResult<String, Void> failResult2 = mDb1.putAsync( 1856 new PutDocumentsRequest.Builder().addGenericDocuments(email2).build()).get(); 1857 assertThat(failResult2.isSuccess()).isFalse(); 1858 assertThat(failResult2.getFailures().get("email2").getErrorMessage()) 1859 .isEqualTo("Schema type config '" + mContext.getPackageName() + "$" + DB_NAME_1 1860 + "/builtin:Email' not found"); 1861 } 1862 1863 @Test testRemoveSchema_twoDatabases()1864 public void testRemoveSchema_twoDatabases() throws Exception { 1865 // Schema registration in mDb1 and mDb2 1866 AppSearchSchema emailSchema = new AppSearchSchema.Builder(AppSearchEmail.SCHEMA_TYPE) 1867 .addProperty(new StringPropertyConfig.Builder("subject") 1868 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 1869 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 1870 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 1871 .build()) 1872 .build(); 1873 mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(emailSchema).build()).get(); 1874 mDb2.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(emailSchema).build()).get(); 1875 1876 // Index an email and check it present in database1. 1877 AppSearchEmail email1 = new AppSearchEmail.Builder("namespace", "email1") 1878 .setSubject("testPut example") 1879 .build(); 1880 checkIsBatchResultSuccess( 1881 mDb1.putAsync( 1882 new PutDocumentsRequest.Builder().addGenericDocuments(email1).build())); 1883 List<GenericDocument> outDocuments = 1884 doGet(mDb1, "namespace", "email1"); 1885 assertThat(outDocuments).hasSize(1); 1886 AppSearchEmail outEmail = new AppSearchEmail(outDocuments.get(0)); 1887 assertThat(outEmail).isEqualTo(email1); 1888 1889 // Index an email and check it present in database2. 1890 AppSearchEmail email2 = new AppSearchEmail.Builder("namespace", "email2") 1891 .setSubject("testPut example") 1892 .build(); 1893 checkIsBatchResultSuccess( 1894 mDb2.putAsync( 1895 new PutDocumentsRequest.Builder().addGenericDocuments(email2).build())); 1896 outDocuments = doGet(mDb2, "namespace", "email2"); 1897 assertThat(outDocuments).hasSize(1); 1898 outEmail = new AppSearchEmail(outDocuments.get(0)); 1899 assertThat(outEmail).isEqualTo(email2); 1900 1901 // Try to remove the email schema in database1. This should fail as it's an incompatible 1902 // change. 1903 SetSchemaRequest setSchemaRequest = new SetSchemaRequest.Builder().build(); 1904 ExecutionException executionException = assertThrows(ExecutionException.class, 1905 () -> mDb1.setSchemaAsync(setSchemaRequest).get()); 1906 assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class); 1907 AppSearchException failResult1 = (AppSearchException) executionException.getCause(); 1908 assertThat(failResult1).hasMessageThat().contains("Schema is incompatible"); 1909 assertThat(failResult1).hasMessageThat().contains( 1910 "Deleted types: {builtin:Email}"); 1911 1912 // Try to remove the email schema again, which should now work as we set forceOverride to 1913 // be true. 1914 mDb1.setSchemaAsync(new SetSchemaRequest.Builder().setForceOverride(true).build()).get(); 1915 1916 // Make sure the indexed email is gone in database 1. 1917 AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentIdAsync( 1918 new GetByDocumentIdRequest.Builder("namespace") 1919 .addIds("email1").build()).get(); 1920 assertThat(getResult.isSuccess()).isFalse(); 1921 assertThat(getResult.getFailures().get("email1").getResultCode()) 1922 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND); 1923 1924 // Try to index an email again. This should fail as the schema has been removed. 1925 AppSearchEmail email3 = new AppSearchEmail.Builder("namespace", "email3") 1926 .setSubject("testPut example") 1927 .build(); 1928 AppSearchBatchResult<String, Void> failResult2 = mDb1.putAsync( 1929 new PutDocumentsRequest.Builder().addGenericDocuments(email3).build()).get(); 1930 assertThat(failResult2.isSuccess()).isFalse(); 1931 assertThat(failResult2.getFailures().get("email3").getErrorMessage()) 1932 .isEqualTo("Schema type config '" + mContext.getPackageName() + "$" + DB_NAME_1 1933 + "/builtin:Email' not found"); 1934 1935 // Make sure email in database 2 still present. 1936 outDocuments = doGet(mDb2, "namespace", "email2"); 1937 assertThat(outDocuments).hasSize(1); 1938 outEmail = new AppSearchEmail(outDocuments.get(0)); 1939 assertThat(outEmail).isEqualTo(email2); 1940 1941 // Make sure email could still be indexed in database 2. 1942 checkIsBatchResultSuccess( 1943 mDb2.putAsync( 1944 new PutDocumentsRequest.Builder().addGenericDocuments(email2).build())); 1945 } 1946 1947 @Test testGetDocuments()1948 public void testGetDocuments() throws Exception { 1949 // Schema registration 1950 mDb1.setSchemaAsync( 1951 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get(); 1952 1953 // Index a document 1954 AppSearchEmail inEmail = 1955 new AppSearchEmail.Builder("namespace", "id1") 1956 .setFrom("from@example.com") 1957 .setTo("to1@example.com", "to2@example.com") 1958 .setSubject("testPut example") 1959 .setBody("This is the body of the testPut email") 1960 .build(); 1961 checkIsBatchResultSuccess(mDb1.putAsync( 1962 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build())); 1963 1964 // Get the document 1965 List<GenericDocument> outDocuments = doGet(mDb1, "namespace", "id1"); 1966 assertThat(outDocuments).hasSize(1); 1967 AppSearchEmail outEmail = new AppSearchEmail(outDocuments.get(0)); 1968 assertThat(outEmail).isEqualTo(inEmail); 1969 1970 // Can't get the document in the other instance. 1971 AppSearchBatchResult<String, GenericDocument> failResult = mDb2.getByDocumentIdAsync( 1972 new GetByDocumentIdRequest.Builder("namespace").addIds( 1973 "id1").build()).get(); 1974 assertThat(failResult.isSuccess()).isFalse(); 1975 assertThat(failResult.getFailures().get("id1").getResultCode()) 1976 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND); 1977 } 1978 1979 // @exportToFramework:startStrip() 1980 1981 @Test testGet_addDocumentClasses()1982 public void testGet_addDocumentClasses() throws Exception { 1983 // Schema registration 1984 mDb1.setSchemaAsync(new SetSchemaRequest.Builder() 1985 .addDocumentClasses(EmailDocument.class).build()).get(); 1986 1987 // Index a document 1988 EmailDocument inEmail = new EmailDocument(); 1989 inEmail.namespace = "namespace"; 1990 inEmail.id = "id1"; 1991 inEmail.subject = "testPut example"; 1992 inEmail.body = "This is the body of the testPut inEmail"; 1993 checkIsBatchResultSuccess(mDb1.putAsync( 1994 new PutDocumentsRequest.Builder().addDocuments(inEmail).build())); 1995 1996 // Get the document 1997 List<GenericDocument> outDocuments = doGet(mDb1, "namespace", "id1"); 1998 assertThat(outDocuments).hasSize(1); 1999 EmailDocument outEmail = outDocuments.get(0).toDocumentClass(EmailDocument.class); 2000 assertThat(inEmail.id).isEqualTo(outEmail.id); 2001 assertThat(inEmail.subject).isEqualTo(outEmail.subject); 2002 assertThat(inEmail.body).isEqualTo(outEmail.body); 2003 } 2004 // @exportToFramework:endStrip() 2005 2006 2007 @Test testGetDocuments_projection()2008 public void testGetDocuments_projection() throws Exception { 2009 // Schema registration 2010 mDb1.setSchemaAsync( 2011 new SetSchemaRequest.Builder() 2012 .addSchemas(AppSearchEmail.SCHEMA) 2013 .build()).get(); 2014 2015 // Index two documents 2016 AppSearchEmail email1 = 2017 new AppSearchEmail.Builder("namespace", "id1") 2018 .setCreationTimestampMillis(1000) 2019 .setFrom("from@example.com") 2020 .setTo("to1@example.com", "to2@example.com") 2021 .setSubject("testPut example") 2022 .setBody("This is the body of the testPut email") 2023 .build(); 2024 AppSearchEmail email2 = 2025 new AppSearchEmail.Builder("namespace", "id2") 2026 .setCreationTimestampMillis(1000) 2027 .setFrom("from@example.com") 2028 .setTo("to1@example.com", "to2@example.com") 2029 .setSubject("testPut example") 2030 .setBody("This is the body of the testPut email") 2031 .build(); 2032 checkIsBatchResultSuccess(mDb1.putAsync( 2033 new PutDocumentsRequest.Builder() 2034 .addGenericDocuments(email1, email2).build())); 2035 2036 // Get with type property paths {"Email", ["subject", "to"]} 2037 GetByDocumentIdRequest request = new GetByDocumentIdRequest.Builder("namespace") 2038 .addIds("id1", "id2") 2039 .addProjection( 2040 AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("subject", "to")) 2041 .build(); 2042 List<GenericDocument> outDocuments = doGet(mDb1, request); 2043 2044 // The two email documents should have been returned with only the "subject" and "to" 2045 // properties. 2046 AppSearchEmail expected1 = 2047 new AppSearchEmail.Builder("namespace", "id2") 2048 .setCreationTimestampMillis(1000) 2049 .setTo("to1@example.com", "to2@example.com") 2050 .setSubject("testPut example") 2051 .build(); 2052 AppSearchEmail expected2 = 2053 new AppSearchEmail.Builder("namespace", "id1") 2054 .setCreationTimestampMillis(1000) 2055 .setTo("to1@example.com", "to2@example.com") 2056 .setSubject("testPut example") 2057 .build(); 2058 assertThat(outDocuments).containsExactly(expected1, expected2); 2059 } 2060 2061 @Test testGetDocuments_projectionEmpty()2062 public void testGetDocuments_projectionEmpty() throws Exception { 2063 // Schema registration 2064 mDb1.setSchemaAsync( 2065 new SetSchemaRequest.Builder() 2066 .addSchemas(AppSearchEmail.SCHEMA) 2067 .build()).get(); 2068 2069 // Index two documents 2070 AppSearchEmail email1 = 2071 new AppSearchEmail.Builder("namespace", "id1") 2072 .setCreationTimestampMillis(1000) 2073 .setFrom("from@example.com") 2074 .setTo("to1@example.com", "to2@example.com") 2075 .setSubject("testPut example") 2076 .setBody("This is the body of the testPut email") 2077 .build(); 2078 AppSearchEmail email2 = 2079 new AppSearchEmail.Builder("namespace", "id2") 2080 .setCreationTimestampMillis(1000) 2081 .setFrom("from@example.com") 2082 .setTo("to1@example.com", "to2@example.com") 2083 .setSubject("testPut example") 2084 .setBody("This is the body of the testPut email") 2085 .build(); 2086 checkIsBatchResultSuccess(mDb1.putAsync( 2087 new PutDocumentsRequest.Builder() 2088 .addGenericDocuments(email1, email2).build())); 2089 2090 // Get with type property paths {"Email", ["subject", "to"]} 2091 GetByDocumentIdRequest request = new GetByDocumentIdRequest.Builder("namespace").addIds( 2092 "id1", 2093 "id2").addProjection(AppSearchEmail.SCHEMA_TYPE, Collections.emptyList()).build(); 2094 List<GenericDocument> outDocuments = doGet(mDb1, request); 2095 2096 // The two email documents should have been returned without any properties. 2097 AppSearchEmail expected1 = 2098 new AppSearchEmail.Builder("namespace", "id2") 2099 .setCreationTimestampMillis(1000) 2100 .build(); 2101 AppSearchEmail expected2 = 2102 new AppSearchEmail.Builder("namespace", "id1") 2103 .setCreationTimestampMillis(1000) 2104 .build(); 2105 assertThat(outDocuments).containsExactly(expected1, expected2); 2106 } 2107 2108 @Test testGetDocuments_projectionNonExistentType()2109 public void testGetDocuments_projectionNonExistentType() throws Exception { 2110 // Schema registration 2111 mDb1.setSchemaAsync( 2112 new SetSchemaRequest.Builder() 2113 .addSchemas(AppSearchEmail.SCHEMA) 2114 .build()).get(); 2115 2116 // Index two documents 2117 AppSearchEmail email1 = 2118 new AppSearchEmail.Builder("namespace", "id1") 2119 .setCreationTimestampMillis(1000) 2120 .setFrom("from@example.com") 2121 .setTo("to1@example.com", "to2@example.com") 2122 .setSubject("testPut example") 2123 .setBody("This is the body of the testPut email") 2124 .build(); 2125 AppSearchEmail email2 = 2126 new AppSearchEmail.Builder("namespace", "id2") 2127 .setCreationTimestampMillis(1000) 2128 .setFrom("from@example.com") 2129 .setTo("to1@example.com", "to2@example.com") 2130 .setSubject("testPut example") 2131 .setBody("This is the body of the testPut email") 2132 .build(); 2133 checkIsBatchResultSuccess(mDb1.putAsync( 2134 new PutDocumentsRequest.Builder() 2135 .addGenericDocuments(email1, email2).build())); 2136 2137 // Get with type property paths {"Email", ["subject", "to"]} 2138 GetByDocumentIdRequest request = new GetByDocumentIdRequest.Builder("namespace") 2139 .addIds("id1", "id2") 2140 .addProjection("NonExistentType", Collections.emptyList()) 2141 .addProjection(AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("subject", "to")) 2142 .build(); 2143 List<GenericDocument> outDocuments = doGet(mDb1, request); 2144 2145 // The two email documents should have been returned with only the "subject" and "to" 2146 // properties. 2147 AppSearchEmail expected1 = 2148 new AppSearchEmail.Builder("namespace", "id2") 2149 .setCreationTimestampMillis(1000) 2150 .setTo("to1@example.com", "to2@example.com") 2151 .setSubject("testPut example") 2152 .build(); 2153 AppSearchEmail expected2 = 2154 new AppSearchEmail.Builder("namespace", "id1") 2155 .setCreationTimestampMillis(1000) 2156 .setTo("to1@example.com", "to2@example.com") 2157 .setSubject("testPut example") 2158 .build(); 2159 assertThat(outDocuments).containsExactly(expected1, expected2); 2160 } 2161 2162 @Test testGetDocuments_wildcardProjection()2163 public void testGetDocuments_wildcardProjection() throws Exception { 2164 // Schema registration 2165 mDb1.setSchemaAsync( 2166 new SetSchemaRequest.Builder() 2167 .addSchemas(AppSearchEmail.SCHEMA) 2168 .build()).get(); 2169 2170 // Index two documents 2171 AppSearchEmail email1 = 2172 new AppSearchEmail.Builder("namespace", "id1") 2173 .setCreationTimestampMillis(1000) 2174 .setFrom("from@example.com") 2175 .setTo("to1@example.com", "to2@example.com") 2176 .setSubject("testPut example") 2177 .setBody("This is the body of the testPut email") 2178 .build(); 2179 AppSearchEmail email2 = 2180 new AppSearchEmail.Builder("namespace", "id2") 2181 .setCreationTimestampMillis(1000) 2182 .setFrom("from@example.com") 2183 .setTo("to1@example.com", "to2@example.com") 2184 .setSubject("testPut example") 2185 .setBody("This is the body of the testPut email") 2186 .build(); 2187 checkIsBatchResultSuccess(mDb1.putAsync( 2188 new PutDocumentsRequest.Builder() 2189 .addGenericDocuments(email1, email2).build())); 2190 2191 // Get with type property paths {"Email", ["subject", "to"]} 2192 GetByDocumentIdRequest request = new GetByDocumentIdRequest.Builder("namespace") 2193 .addIds("id1", "id2") 2194 .addProjection( 2195 GetByDocumentIdRequest.PROJECTION_SCHEMA_TYPE_WILDCARD, 2196 ImmutableList.of("subject", "to")) 2197 .build(); 2198 List<GenericDocument> outDocuments = doGet(mDb1, request); 2199 2200 // The two email documents should have been returned with only the "subject" and "to" 2201 // properties. 2202 AppSearchEmail expected1 = 2203 new AppSearchEmail.Builder("namespace", "id2") 2204 .setCreationTimestampMillis(1000) 2205 .setTo("to1@example.com", "to2@example.com") 2206 .setSubject("testPut example") 2207 .build(); 2208 AppSearchEmail expected2 = 2209 new AppSearchEmail.Builder("namespace", "id1") 2210 .setCreationTimestampMillis(1000) 2211 .setTo("to1@example.com", "to2@example.com") 2212 .setSubject("testPut example") 2213 .build(); 2214 assertThat(outDocuments).containsExactly(expected1, expected2); 2215 } 2216 2217 @Test testGetDocuments_wildcardProjectionEmpty()2218 public void testGetDocuments_wildcardProjectionEmpty() throws Exception { 2219 // Schema registration 2220 mDb1.setSchemaAsync( 2221 new SetSchemaRequest.Builder() 2222 .addSchemas(AppSearchEmail.SCHEMA) 2223 .build()).get(); 2224 2225 // Index two documents 2226 AppSearchEmail email1 = 2227 new AppSearchEmail.Builder("namespace", "id1") 2228 .setCreationTimestampMillis(1000) 2229 .setFrom("from@example.com") 2230 .setTo("to1@example.com", "to2@example.com") 2231 .setSubject("testPut example") 2232 .setBody("This is the body of the testPut email") 2233 .build(); 2234 AppSearchEmail email2 = 2235 new AppSearchEmail.Builder("namespace", "id2") 2236 .setCreationTimestampMillis(1000) 2237 .setFrom("from@example.com") 2238 .setTo("to1@example.com", "to2@example.com") 2239 .setSubject("testPut example") 2240 .setBody("This is the body of the testPut email") 2241 .build(); 2242 checkIsBatchResultSuccess(mDb1.putAsync( 2243 new PutDocumentsRequest.Builder() 2244 .addGenericDocuments(email1, email2).build())); 2245 2246 // Get with type property paths {"Email", ["subject", "to"]} 2247 GetByDocumentIdRequest request = new GetByDocumentIdRequest.Builder("namespace").addIds( 2248 "id1", 2249 "id2").addProjection(GetByDocumentIdRequest.PROJECTION_SCHEMA_TYPE_WILDCARD, 2250 Collections.emptyList()).build(); 2251 List<GenericDocument> outDocuments = doGet(mDb1, request); 2252 2253 // The two email documents should have been returned without any properties. 2254 AppSearchEmail expected1 = 2255 new AppSearchEmail.Builder("namespace", "id2") 2256 .setCreationTimestampMillis(1000) 2257 .build(); 2258 AppSearchEmail expected2 = 2259 new AppSearchEmail.Builder("namespace", "id1") 2260 .setCreationTimestampMillis(1000) 2261 .build(); 2262 assertThat(outDocuments).containsExactly(expected1, expected2); 2263 } 2264 2265 @Test testGetDocuments_wildcardProjectionNonExistentType()2266 public void testGetDocuments_wildcardProjectionNonExistentType() throws Exception { 2267 // Schema registration 2268 mDb1.setSchemaAsync( 2269 new SetSchemaRequest.Builder() 2270 .addSchemas(AppSearchEmail.SCHEMA) 2271 .build()).get(); 2272 2273 // Index two documents 2274 AppSearchEmail email1 = 2275 new AppSearchEmail.Builder("namespace", "id1") 2276 .setCreationTimestampMillis(1000) 2277 .setFrom("from@example.com") 2278 .setTo("to1@example.com", "to2@example.com") 2279 .setSubject("testPut example") 2280 .setBody("This is the body of the testPut email") 2281 .build(); 2282 AppSearchEmail email2 = 2283 new AppSearchEmail.Builder("namespace", "id2") 2284 .setCreationTimestampMillis(1000) 2285 .setFrom("from@example.com") 2286 .setTo("to1@example.com", "to2@example.com") 2287 .setSubject("testPut example") 2288 .setBody("This is the body of the testPut email") 2289 .build(); 2290 checkIsBatchResultSuccess(mDb1.putAsync( 2291 new PutDocumentsRequest.Builder() 2292 .addGenericDocuments(email1, email2).build())); 2293 2294 // Get with type property paths {"Email", ["subject", "to"]} 2295 GetByDocumentIdRequest request = new GetByDocumentIdRequest.Builder("namespace") 2296 .addIds("id1", "id2") 2297 .addProjection("NonExistentType", Collections.emptyList()) 2298 .addProjection( 2299 GetByDocumentIdRequest.PROJECTION_SCHEMA_TYPE_WILDCARD, 2300 ImmutableList.of("subject", "to")) 2301 .build(); 2302 List<GenericDocument> outDocuments = doGet(mDb1, request); 2303 2304 // The two email documents should have been returned with only the "subject" and "to" 2305 // properties. 2306 AppSearchEmail expected1 = 2307 new AppSearchEmail.Builder("namespace", "id2") 2308 .setCreationTimestampMillis(1000) 2309 .setTo("to1@example.com", "to2@example.com") 2310 .setSubject("testPut example") 2311 .build(); 2312 AppSearchEmail expected2 = 2313 new AppSearchEmail.Builder("namespace", "id1") 2314 .setCreationTimestampMillis(1000) 2315 .setTo("to1@example.com", "to2@example.com") 2316 .setSubject("testPut example") 2317 .build(); 2318 assertThat(outDocuments).containsExactly(expected1, expected2); 2319 } 2320 2321 @Test testQuery()2322 public void testQuery() throws Exception { 2323 // Schema registration 2324 mDb1.setSchemaAsync( 2325 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get(); 2326 2327 // Index a document 2328 AppSearchEmail inEmail = 2329 new AppSearchEmail.Builder("namespace", "id1") 2330 .setFrom("from@example.com") 2331 .setTo("to1@example.com", "to2@example.com") 2332 .setSubject("testPut example") 2333 .setBody("This is the body of the testPut email") 2334 .build(); 2335 checkIsBatchResultSuccess(mDb1.putAsync( 2336 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build())); 2337 2338 // Query for the document 2339 SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder() 2340 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 2341 .build()); 2342 List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults); 2343 assertThat(documents).hasSize(1); 2344 assertThat(documents.get(0)).isEqualTo(inEmail); 2345 2346 // Multi-term query 2347 searchResults = mDb1.search("body email", new SearchSpec.Builder() 2348 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 2349 .build()); 2350 documents = convertSearchResultsToDocuments(searchResults); 2351 assertThat(documents).hasSize(1); 2352 assertThat(documents.get(0)).isEqualTo(inEmail); 2353 } 2354 2355 @Test testQuery_getNextPage()2356 public void testQuery_getNextPage() throws Exception { 2357 // Schema registration 2358 mDb1.setSchemaAsync( 2359 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get(); 2360 Set<AppSearchEmail> emailSet = new HashSet<>(); 2361 PutDocumentsRequest.Builder putDocumentsRequestBuilder = new PutDocumentsRequest.Builder(); 2362 // Index 31 documents 2363 for (int i = 0; i < 31; i++) { 2364 AppSearchEmail inEmail = 2365 new AppSearchEmail.Builder("namespace", "id" + i) 2366 .setFrom("from@example.com") 2367 .setTo("to1@example.com", "to2@example.com") 2368 .setSubject("testPut example") 2369 .setBody("This is the body of the testPut email") 2370 .build(); 2371 emailSet.add(inEmail); 2372 putDocumentsRequestBuilder.addGenericDocuments(inEmail); 2373 } 2374 checkIsBatchResultSuccess(mDb1.putAsync(putDocumentsRequestBuilder.build())); 2375 2376 // Set number of results per page is 7. 2377 SearchResults searchResults = mDb1.search("body", 2378 new SearchSpec.Builder() 2379 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 2380 .setResultCountPerPage(7) 2381 .build()); 2382 List<GenericDocument> documents = new ArrayList<>(); 2383 2384 int pageNumber = 0; 2385 List<SearchResult> results; 2386 2387 // keep loading next page until it's empty. 2388 do { 2389 results = searchResults.getNextPageAsync().get(); 2390 ++pageNumber; 2391 for (SearchResult result : results) { 2392 documents.add(result.getGenericDocument()); 2393 } 2394 } while (results.size() > 0); 2395 2396 // check all document presents 2397 assertThat(documents).containsExactlyElementsIn(emailSet); 2398 assertThat(pageNumber).isEqualTo(6); // 5 (upper(31/7)) + 1 (final empty page) 2399 } 2400 2401 @Test testQueryIndexableLongProperty_numericSearchEnabledSucceeds()2402 public void testQueryIndexableLongProperty_numericSearchEnabledSucceeds() throws Exception { 2403 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.NUMERIC_SEARCH)); 2404 2405 // Schema registration 2406 AppSearchSchema transactionSchema = new AppSearchSchema.Builder("transaction") 2407 .addProperty(new LongPropertyConfig.Builder("price") 2408 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 2409 .setIndexingType(LongPropertyConfig.INDEXING_TYPE_RANGE) 2410 .build() 2411 ).addProperty(new LongPropertyConfig.Builder("cost") 2412 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 2413 .setIndexingType(LongPropertyConfig.INDEXING_TYPE_RANGE) 2414 .build() 2415 ).build(); 2416 mDb1.setSchemaAsync( 2417 new SetSchemaRequest.Builder().addSchemas(transactionSchema).build()).get(); 2418 2419 // Index some documents 2420 GenericDocument doc1 = 2421 new GenericDocument.Builder<>("namespace", "id1", "transaction") 2422 .setPropertyLong("price", 10) 2423 .setCreationTimestampMillis(1000) 2424 .build(); 2425 GenericDocument doc2 = 2426 new GenericDocument.Builder<>("namespace", "id2", "transaction") 2427 .setPropertyLong("price", 25) 2428 .setCreationTimestampMillis(1000) 2429 .build(); 2430 GenericDocument doc3 = 2431 new GenericDocument.Builder<>("namespace", "id3", "transaction") 2432 .setPropertyLong("cost", 2) 2433 .setCreationTimestampMillis(1000) 2434 .build(); 2435 checkIsBatchResultSuccess(mDb1.putAsync( 2436 new PutDocumentsRequest.Builder().addGenericDocuments(doc1, doc2, doc3).build())); 2437 2438 // Query for the document 2439 SearchResults searchResults = mDb1.search("price < 20", 2440 new SearchSpec.Builder() 2441 .setNumericSearchEnabled(true) 2442 .build()); 2443 List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults); 2444 assertThat(documents).hasSize(1); 2445 assertThat(documents.get(0)).isEqualTo(doc1); 2446 2447 searchResults = mDb1.search("price == 25", 2448 new SearchSpec.Builder() 2449 .setNumericSearchEnabled(true) 2450 .build()); 2451 documents = convertSearchResultsToDocuments(searchResults); 2452 assertThat(documents).hasSize(1); 2453 assertThat(documents.get(0)).isEqualTo(doc2); 2454 2455 searchResults = mDb1.search("cost > 2", 2456 new SearchSpec.Builder() 2457 .setNumericSearchEnabled(true) 2458 .build()); 2459 documents = convertSearchResultsToDocuments(searchResults); 2460 assertThat(documents).isEmpty(); 2461 2462 searchResults = mDb1.search("cost >= 2", 2463 new SearchSpec.Builder() 2464 .setNumericSearchEnabled(true) 2465 .build()); 2466 documents = convertSearchResultsToDocuments(searchResults); 2467 assertThat(documents).hasSize(1); 2468 assertThat(documents.get(0)).isEqualTo(doc3); 2469 2470 searchResults = mDb1.search("price <= 25", 2471 new SearchSpec.Builder() 2472 .setNumericSearchEnabled(true) 2473 .build()); 2474 documents = convertSearchResultsToDocuments(searchResults); 2475 assertThat(documents).hasSize(2); 2476 assertThat(documents.get(0)).isEqualTo(doc2); 2477 assertThat(documents.get(1)).isEqualTo(doc1); 2478 } 2479 2480 @Test testQueryIndexableLongProperty_numericSearchNotEnabled()2481 public void testQueryIndexableLongProperty_numericSearchNotEnabled() throws Exception { 2482 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.NUMERIC_SEARCH)); 2483 2484 // Schema registration 2485 AppSearchSchema transactionSchema = new AppSearchSchema.Builder("transaction") 2486 .addProperty(new LongPropertyConfig.Builder("price") 2487 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 2488 .setIndexingType(LongPropertyConfig.INDEXING_TYPE_RANGE) 2489 .build() 2490 ).build(); 2491 mDb1.setSchemaAsync( 2492 new SetSchemaRequest.Builder().addSchemas(transactionSchema).build()).get(); 2493 2494 // Index some documents 2495 GenericDocument doc = 2496 new GenericDocument.Builder<>("namespace", "id1", "transaction") 2497 .setPropertyLong("price", 10) 2498 .setCreationTimestampMillis(1000) 2499 .build(); 2500 checkIsBatchResultSuccess(mDb1.putAsync( 2501 new PutDocumentsRequest.Builder().addGenericDocuments(doc).build())); 2502 2503 // Query for the document 2504 // Use advanced query but disable NUMERIC_SEARCH in the SearchSpec. 2505 SearchResults searchResults = mDb1.search("price < 20", 2506 new SearchSpec.Builder() 2507 .setNumericSearchEnabled(false) 2508 .build()); 2509 2510 ExecutionException executionException = assertThrows(ExecutionException.class, 2511 () -> searchResults.getNextPageAsync().get()); 2512 assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class); 2513 AppSearchException exception = (AppSearchException) executionException.getCause(); 2514 assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT); 2515 assertThat(exception).hasMessageThat().contains("Attempted use of unenabled feature"); 2516 assertThat(exception).hasMessageThat().contains(Features.NUMERIC_SEARCH); 2517 } 2518 2519 @Test testQuery_relevanceScoring()2520 public void testQuery_relevanceScoring() throws Exception { 2521 // Schema registration 2522 mDb1.setSchemaAsync( 2523 new SetSchemaRequest.Builder() 2524 .addSchemas(AppSearchEmail.SCHEMA) 2525 .build()).get(); 2526 2527 // Index two documents 2528 AppSearchEmail email1 = 2529 new AppSearchEmail.Builder("namespace", "id1") 2530 .setCreationTimestampMillis(1000) 2531 .setFrom("from@example.com") 2532 .setTo("to1@example.com", "to2@example.com") 2533 .setSubject("Mary had a little lamb") 2534 .setBody("A little lamb, little lamb") 2535 .build(); 2536 AppSearchEmail email2 = 2537 new AppSearchEmail.Builder("namespace", "id2") 2538 .setCreationTimestampMillis(1000) 2539 .setFrom("from@example.com") 2540 .setTo("to1@example.com", "to2@example.com") 2541 .setSubject("I'm a little teapot") 2542 .setBody("short and stout. Here is my handle, here is my spout.") 2543 .build(); 2544 checkIsBatchResultSuccess(mDb1.putAsync( 2545 new PutDocumentsRequest.Builder() 2546 .addGenericDocuments(email1, email2).build())); 2547 2548 // Query for "little". It should match both emails. 2549 SearchResults searchResults = mDb1.search("little", new SearchSpec.Builder() 2550 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 2551 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE) 2552 .build()); 2553 List<SearchResult> results = retrieveAllSearchResults(searchResults); 2554 2555 // The email1 should be ranked higher because 'little' appears three times in email1 and 2556 // only once in email2. 2557 assertThat(results).hasSize(2); 2558 assertThat(results.get(0).getGenericDocument()).isEqualTo(email1); 2559 assertThat(results.get(0).getRankingSignal()).isGreaterThan( 2560 results.get(1).getRankingSignal()); 2561 assertThat(results.get(1).getGenericDocument()).isEqualTo(email2); 2562 assertThat(results.get(1).getRankingSignal()).isGreaterThan(0); 2563 2564 // Query for "little OR stout". It should match both emails. 2565 searchResults = mDb1.search("little OR stout", new SearchSpec.Builder() 2566 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 2567 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE) 2568 .build()); 2569 results = retrieveAllSearchResults(searchResults); 2570 2571 // The email2 should be ranked higher because 'little' appears once and "stout", which is a 2572 // rarer term, appears once. email1 only has the three 'little' appearances. 2573 assertThat(results).hasSize(2); 2574 assertThat(results.get(0).getGenericDocument()).isEqualTo(email2); 2575 assertThat(results.get(0).getRankingSignal()).isGreaterThan( 2576 results.get(1).getRankingSignal()); 2577 assertThat(results.get(1).getGenericDocument()).isEqualTo(email1); 2578 assertThat(results.get(1).getRankingSignal()).isGreaterThan(0); 2579 } 2580 2581 @Test testQuery_advancedRanking()2582 public void testQuery_advancedRanking() throws Exception { 2583 assumeTrue(mDb1.getFeatures().isFeatureSupported( 2584 Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION)); 2585 2586 // Schema registration 2587 mDb1.setSchemaAsync( 2588 new SetSchemaRequest.Builder() 2589 .addSchemas(AppSearchEmail.SCHEMA) 2590 .build()).get(); 2591 2592 // Index a document 2593 AppSearchEmail inEmail = 2594 new AppSearchEmail.Builder("namespace", "id1") 2595 .setFrom("from@example.com") 2596 .setTo("to1@example.com", "to2@example.com") 2597 .setSubject("testPut example") 2598 .setBody("This is the body of the testPut email") 2599 .setScore(3) 2600 .build(); 2601 checkIsBatchResultSuccess(mDb1.putAsync( 2602 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build())); 2603 2604 // Query for the document, and set an advanced ranking expression that evaluates to 6. 2605 SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder() 2606 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 2607 // "abs(pow(2, 2) - 6)" should be evaluated to 2. 2608 // "this.documentScore()" should be evaluated to 3. 2609 .setRankingStrategy("abs(pow(2, 2) - 6) * this.documentScore()") 2610 .build()); 2611 List<SearchResult> results = retrieveAllSearchResults(searchResults); 2612 assertThat(results).hasSize(1); 2613 assertThat(results.get(0).getGenericDocument()).isEqualTo(inEmail); 2614 assertThat(results.get(0).getRankingSignal()).isEqualTo(6); 2615 } 2616 2617 @Test testQuery_advancedRankingWithPropertyWeights()2618 public void testQuery_advancedRankingWithPropertyWeights() throws Exception { 2619 assumeTrue(mDb1.getFeatures().isFeatureSupported( 2620 Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION)); 2621 assumeTrue(mDb1.getFeatures().isFeatureSupported( 2622 Features.SEARCH_SPEC_PROPERTY_WEIGHTS)); 2623 2624 // Schema registration 2625 mDb1.setSchemaAsync( 2626 new SetSchemaRequest.Builder() 2627 .addSchemas(AppSearchEmail.SCHEMA) 2628 .build()).get(); 2629 2630 // Index a document 2631 AppSearchEmail inEmail = 2632 new AppSearchEmail.Builder("namespace", "id1") 2633 .setFrom("test from") 2634 .setTo("test to") 2635 .setSubject("subject") 2636 .setBody("test body") 2637 .build(); 2638 checkIsBatchResultSuccess(mDb1.putAsync( 2639 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build())); 2640 2641 // Query for the document, and set an advanced ranking expression that evaluates to 0.7. 2642 SearchResults searchResults = mDb1.search("test", new SearchSpec.Builder() 2643 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 2644 .setPropertyWeights(AppSearchEmail.SCHEMA_TYPE, 2645 ImmutableMap.of("from", 0.1, "to", 0.2, 2646 "subject", 2.0, "body", 0.4)) 2647 // this.propertyWeights() returns normalized property weights, in which each 2648 // weight is divided by the maximum weight. 2649 // As a result, this expression will evaluates to the list {0.1 / 2.0, 0.2 / 2.0, 2650 // 0.4 / 2.0}, since the matched properties are "from", "to" and "body", and the 2651 // maximum weight provided is 2.0. 2652 // Thus, sum(this.propertyWeights()) will be evaluated to 0.05 + 0.1 + 0.2 = 0.35. 2653 .setRankingStrategy("sum(this.propertyWeights())") 2654 .build()); 2655 List<SearchResult> results = retrieveAllSearchResults(searchResults); 2656 assertThat(results).hasSize(1); 2657 assertThat(results.get(0).getGenericDocument()).isEqualTo(inEmail); 2658 assertThat(results.get(0).getRankingSignal()).isEqualTo(0.35); 2659 } 2660 2661 @Test testQuery_advancedRankingWithJoin()2662 public void testQuery_advancedRankingWithJoin() throws Exception { 2663 assumeTrue(mDb1.getFeatures().isFeatureSupported( 2664 Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION)); 2665 assumeTrue(mDb1.getFeatures() 2666 .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID)); 2667 2668 // A full example of how join might be used 2669 AppSearchSchema actionSchema = new AppSearchSchema.Builder("ViewAction") 2670 .addProperty(new StringPropertyConfig.Builder("entityId") 2671 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 2672 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 2673 .setJoinableValueType(StringPropertyConfig 2674 .JOINABLE_VALUE_TYPE_QUALIFIED_ID) 2675 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 2676 .build() 2677 ).addProperty(new StringPropertyConfig.Builder("note") 2678 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 2679 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 2680 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 2681 .build() 2682 ).build(); 2683 2684 // Schema registration 2685 mDb1.setSchemaAsync( 2686 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA, actionSchema) 2687 .build()).get(); 2688 2689 // Index a document 2690 AppSearchEmail inEmail = 2691 new AppSearchEmail.Builder("namespace", "id1") 2692 .setFrom("from@example.com") 2693 .setTo("to1@example.com", "to2@example.com") 2694 .setSubject("testPut example") 2695 .setBody("This is the body of the testPut email") 2696 .setScore(1) 2697 .build(); 2698 2699 String qualifiedId = DocumentIdUtil.createQualifiedId(mContext.getPackageName(), DB_NAME_1, 2700 "namespace", "id1"); 2701 GenericDocument viewAction1 = new GenericDocument.Builder<>("NS", "id2", "ViewAction") 2702 .setScore(1) 2703 .setPropertyString("entityId", qualifiedId) 2704 .setPropertyString("note", "Viewed email on Monday").build(); 2705 GenericDocument viewAction2 = new GenericDocument.Builder<>("NS", "id3", "ViewAction") 2706 .setScore(2) 2707 .setPropertyString("entityId", qualifiedId) 2708 .setPropertyString("note", "Viewed email on Tuesday").build(); 2709 checkIsBatchResultSuccess(mDb1.putAsync( 2710 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail, viewAction1, 2711 viewAction2).build())); 2712 2713 SearchSpec nestedSearchSpec = 2714 new SearchSpec.Builder() 2715 .setRankingStrategy("2 * this.documentScore()") 2716 .setOrder(SearchSpec.ORDER_ASCENDING) 2717 .build(); 2718 2719 JoinSpec js = new JoinSpec.Builder("entityId") 2720 .setNestedSearch("", nestedSearchSpec) 2721 .build(); 2722 2723 SearchResults searchResults = mDb1.search("body email", new SearchSpec.Builder() 2724 // this.childrenRankingSignals() evaluates to the list {1 * 2, 2 * 2}. 2725 // Thus, sum(this.childrenRankingSignals()) evaluates to 6. 2726 .setRankingStrategy("sum(this.childrenRankingSignals())") 2727 .setJoinSpec(js) 2728 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 2729 .build()); 2730 2731 List<SearchResult> sr = searchResults.getNextPageAsync().get(); 2732 2733 assertThat(sr).hasSize(1); 2734 assertThat(sr.get(0).getGenericDocument().getId()).isEqualTo("id1"); 2735 assertThat(sr.get(0).getJoinedResults()).hasSize(2); 2736 assertThat(sr.get(0).getJoinedResults().get(0).getGenericDocument()).isEqualTo(viewAction1); 2737 assertThat(sr.get(0).getJoinedResults().get(1).getGenericDocument()).isEqualTo(viewAction2); 2738 assertThat(sr.get(0).getRankingSignal()).isEqualTo(6.0); 2739 } 2740 2741 // @exportToFramework:startStrip() 2742 @Test testQueryRankByClickActions_useTakenAction()2743 public void testQueryRankByClickActions_useTakenAction() throws Exception { 2744 assumeTrue(mDb1.getFeatures() 2745 .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID)); 2746 2747 // Schema registration 2748 mDb1.setSchemaAsync( 2749 new SetSchemaRequest.Builder() 2750 .addSchemas(AppSearchEmail.SCHEMA) 2751 .addDocumentClasses(SearchAction.class, ClickAction.class, 2752 ImpressionAction.class, DismissAction.class) 2753 .build()) 2754 .get(); 2755 2756 // Index several email documents 2757 AppSearchEmail inEmail1 = 2758 new AppSearchEmail.Builder("namespace", "email1") 2759 .setFrom("from@example.com") 2760 .setTo("to1@example.com", "to2@example.com") 2761 .setSubject("testPut example") 2762 .setBody("This is the body of the testPut email") 2763 .setScore(1) 2764 .build(); 2765 AppSearchEmail inEmail2 = 2766 new AppSearchEmail.Builder("namespace", "email2") 2767 .setFrom("from@example.com") 2768 .setTo("to1@example.com", "to2@example.com") 2769 .setSubject("testPut example") 2770 .setBody("This is the body of the testPut email") 2771 .setScore(1) 2772 .build(); 2773 2774 String qualifiedId1 = DocumentIdUtil.createQualifiedId( 2775 mContext.getPackageName(), DB_NAME_1, inEmail1); 2776 String qualifiedId2 = DocumentIdUtil.createQualifiedId( 2777 mContext.getPackageName(), DB_NAME_1, inEmail2); 2778 2779 SearchAction searchAction = 2780 new SearchAction.Builder("namespace", "search1", /* actionTimestampMillis= */1000) 2781 .setDocumentTtlMillis(0) 2782 .setQuery("body") 2783 .setFetchedResultCount(20) 2784 .build(); 2785 ClickAction clickAction1 = 2786 new ClickAction.Builder("namespace", "click1", /* actionTimestampMillis= */2000) 2787 .setDocumentTtlMillis(0) 2788 .setQuery("body") 2789 .setReferencedQualifiedId(qualifiedId1) 2790 .setResultRankInBlock(1) 2791 .setResultRankGlobal(1) 2792 .setTimeStayOnResultMillis(512) 2793 .build(); 2794 ClickAction clickAction2 = 2795 new ClickAction.Builder("namespace", "click2", /* actionTimestampMillis= */3000) 2796 .setDocumentTtlMillis(0) 2797 .setQuery("body") 2798 .setReferencedQualifiedId(qualifiedId2) 2799 .setResultRankInBlock(2) 2800 .setResultRankGlobal(2) 2801 .setTimeStayOnResultMillis(128) 2802 .build(); 2803 ClickAction clickAction3 = 2804 new ClickAction.Builder("namespace", "click3", /* actionTimestampMillis= */4000) 2805 .setDocumentTtlMillis(0) 2806 .setQuery("body") 2807 .setReferencedQualifiedId(qualifiedId1) 2808 .setResultRankInBlock(2) 2809 .setResultRankGlobal(2) 2810 .setTimeStayOnResultMillis(256) 2811 .build(); 2812 ImpressionAction impressionAction1 = 2813 new ImpressionAction.Builder( 2814 "namespace", "impression1", /* actionTimestampMillis= */5000) 2815 .setDocumentTtlMillis(0) 2816 .setQuery("body") 2817 .setReferencedQualifiedId(qualifiedId2) 2818 .setResultRankInBlock(2) 2819 .setResultRankGlobal(2) 2820 .build(); 2821 DismissAction dismissAction1 = 2822 new DismissAction.Builder( 2823 "namespace", "dismiss1", /* actionTimestampMillis= */6000) 2824 .setDocumentTtlMillis(0) 2825 .setQuery("body") 2826 .setReferencedQualifiedId(qualifiedId2) 2827 .setResultRankInBlock(2) 2828 .setResultRankGlobal(2) 2829 .build(); 2830 2831 checkIsBatchResultSuccess(mDb1.putAsync( 2832 new PutDocumentsRequest.Builder() 2833 .addGenericDocuments(inEmail1, inEmail2) 2834 .addTakenActions(searchAction, clickAction1, clickAction2, clickAction3, 2835 impressionAction1, dismissAction1) 2836 .build())); 2837 2838 SearchSpec nestedSearchSpec = 2839 new SearchSpec.Builder() 2840 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_DOCUMENT_SCORE) 2841 .setOrder(SearchSpec.ORDER_DESCENDING) 2842 .addFilterDocumentClasses(ClickAction.class) 2843 .build(); 2844 2845 // Note: SearchSpec.Builder#setMaxJoinedResultCount only limits the number of child 2846 // documents returned. It does not affect the number of child documents that are scored. 2847 JoinSpec js = new JoinSpec.Builder("referencedQualifiedId") 2848 .setNestedSearch("query:body", nestedSearchSpec) 2849 .setAggregationScoringStrategy(JoinSpec.AGGREGATION_SCORING_RESULT_COUNT) 2850 .setMaxJoinedResultCount(0) 2851 .build(); 2852 2853 // Search "body" for AppSearchEmail documents, ranking by ClickAction signals with 2854 // query = "body". 2855 SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder() 2856 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_JOIN_AGGREGATE_SCORE) 2857 .setOrder(SearchSpec.ORDER_DESCENDING) 2858 .setJoinSpec(js) 2859 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 2860 .addFilterSchemas(AppSearchEmail.SCHEMA_TYPE) 2861 .build()); 2862 2863 List<SearchResult> sr = searchResults.getNextPageAsync().get(); 2864 2865 assertThat(sr).hasSize(2); 2866 assertThat(sr.get(0).getGenericDocument().getId()).isEqualTo("email1"); 2867 assertThat(sr.get(0).getRankingSignal()).isEqualTo(2.0); 2868 assertThat(sr.get(1).getGenericDocument().getId()).isEqualTo("email2"); 2869 assertThat(sr.get(1).getRankingSignal()).isEqualTo(1.0); 2870 } 2871 2872 @Test testQueryRankByImpressionActions_useTakenAction()2873 public void testQueryRankByImpressionActions_useTakenAction() throws Exception { 2874 assumeTrue(mDb1.getFeatures() 2875 .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID)); 2876 2877 // Schema registration 2878 mDb1.setSchemaAsync( 2879 new SetSchemaRequest.Builder() 2880 .addSchemas(AppSearchEmail.SCHEMA) 2881 .addDocumentClasses(SearchAction.class, ClickAction.class, 2882 ImpressionAction.class, DismissAction.class) 2883 .build()) 2884 .get(); 2885 2886 // Index several email documents 2887 AppSearchEmail inEmail1 = 2888 new AppSearchEmail.Builder("namespace", "email1") 2889 .setFrom("from@example.com") 2890 .setTo("to1@example.com", "to2@example.com") 2891 .setSubject("testPut example") 2892 .setBody("This is the body of the testPut email") 2893 .setScore(1) 2894 .build(); 2895 AppSearchEmail inEmail2 = 2896 new AppSearchEmail.Builder("namespace", "email2") 2897 .setFrom("from@example.com") 2898 .setTo("to1@example.com", "to2@example.com") 2899 .setSubject("testPut example") 2900 .setBody("This is the body of the testPut email") 2901 .setScore(1) 2902 .build(); 2903 2904 String qualifiedId1 = DocumentIdUtil.createQualifiedId( 2905 mContext.getPackageName(), DB_NAME_1, inEmail1); 2906 String qualifiedId2 = DocumentIdUtil.createQualifiedId( 2907 mContext.getPackageName(), DB_NAME_1, inEmail2); 2908 2909 SearchAction searchAction = 2910 new SearchAction.Builder("namespace", "search1", /* actionTimestampMillis= */1000) 2911 .setDocumentTtlMillis(0) 2912 .setQuery("body") 2913 .setFetchedResultCount(20) 2914 .build(); 2915 ClickAction clickAction1 = 2916 new ClickAction.Builder("namespace", "click1", /* actionTimestampMillis= */2000) 2917 .setDocumentTtlMillis(0) 2918 .setQuery("body") 2919 .setReferencedQualifiedId(qualifiedId1) 2920 .setResultRankInBlock(1) 2921 .setResultRankGlobal(1) 2922 .setTimeStayOnResultMillis(512) 2923 .build(); 2924 ClickAction clickAction2 = 2925 new ClickAction.Builder("namespace", "click2", /* actionTimestampMillis= */3000) 2926 .setDocumentTtlMillis(0) 2927 .setQuery("body") 2928 .setReferencedQualifiedId(qualifiedId2) 2929 .setResultRankInBlock(2) 2930 .setResultRankGlobal(2) 2931 .setTimeStayOnResultMillis(128) 2932 .build(); 2933 ClickAction clickAction3 = 2934 new ClickAction.Builder("namespace", "click3", /* actionTimestampMillis= */4000) 2935 .setDocumentTtlMillis(0) 2936 .setQuery("body") 2937 .setReferencedQualifiedId(qualifiedId1) 2938 .setResultRankInBlock(2) 2939 .setResultRankGlobal(2) 2940 .setTimeStayOnResultMillis(256) 2941 .build(); 2942 ImpressionAction impressionAction1 = 2943 new ImpressionAction.Builder( 2944 "namespace", "impression1", /* actionTimestampMillis= */5000) 2945 .setDocumentTtlMillis(0) 2946 .setQuery("body") 2947 .setReferencedQualifiedId(qualifiedId2) 2948 .setResultRankInBlock(2) 2949 .setResultRankGlobal(2) 2950 .build(); 2951 DismissAction dismissAction1 = 2952 new DismissAction.Builder( 2953 "namespace", "dismiss1", /* actionTimestampMillis= */6000) 2954 .setDocumentTtlMillis(0) 2955 .setQuery("body") 2956 .setReferencedQualifiedId(qualifiedId2) 2957 .setResultRankInBlock(2) 2958 .setResultRankGlobal(2) 2959 .build(); 2960 2961 checkIsBatchResultSuccess(mDb1.putAsync( 2962 new PutDocumentsRequest.Builder() 2963 .addGenericDocuments(inEmail1, inEmail2) 2964 .addTakenActions(searchAction, clickAction1, clickAction2, clickAction3, 2965 impressionAction1, dismissAction1) 2966 .build())); 2967 2968 SearchSpec nestedSearchSpec = 2969 new SearchSpec.Builder() 2970 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_DOCUMENT_SCORE) 2971 .setOrder(SearchSpec.ORDER_DESCENDING) 2972 .addFilterDocumentClasses(ImpressionAction.class) 2973 .build(); 2974 2975 // Note: SearchSpec.Builder#setMaxJoinedResultCount only limits the number of child 2976 // documents returned. It does not affect the number of child documents that are scored. 2977 JoinSpec js = new JoinSpec.Builder("referencedQualifiedId") 2978 .setNestedSearch("query:body", nestedSearchSpec) 2979 .setAggregationScoringStrategy(JoinSpec.AGGREGATION_SCORING_RESULT_COUNT) 2980 .setMaxJoinedResultCount(0) 2981 .build(); 2982 2983 // Search "body" for AppSearchEmail documents, ranking by ImpressionAction signals with 2984 // query = "body". 2985 SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder() 2986 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_JOIN_AGGREGATE_SCORE) 2987 .setOrder(SearchSpec.ORDER_DESCENDING) 2988 .setJoinSpec(js) 2989 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 2990 .addFilterSchemas(AppSearchEmail.SCHEMA_TYPE) 2991 .build()); 2992 2993 List<SearchResult> sr = searchResults.getNextPageAsync().get(); 2994 2995 assertThat(sr).hasSize(2); 2996 assertThat(sr.get(0).getGenericDocument().getId()).isEqualTo("email2"); 2997 assertThat(sr.get(0).getRankingSignal()).isEqualTo(1.0); 2998 assertThat(sr.get(1).getGenericDocument().getId()).isEqualTo("email1"); 2999 assertThat(sr.get(1).getRankingSignal()).isEqualTo(0.0); 3000 } 3001 3002 @Test testQueryRankByDismissActions_useTakenAction()3003 public void testQueryRankByDismissActions_useTakenAction() throws Exception { 3004 assumeTrue(mDb1.getFeatures() 3005 .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID)); 3006 3007 // Schema registration 3008 mDb1.setSchemaAsync( 3009 new SetSchemaRequest.Builder() 3010 .addSchemas(AppSearchEmail.SCHEMA) 3011 .addDocumentClasses(SearchAction.class, ClickAction.class, 3012 ImpressionAction.class, DismissAction.class) 3013 .build()) 3014 .get(); 3015 3016 // Index several email documents 3017 AppSearchEmail inEmail1 = 3018 new AppSearchEmail.Builder("namespace", "email1") 3019 .setFrom("from@example.com") 3020 .setTo("to1@example.com", "to2@example.com") 3021 .setSubject("testPut example") 3022 .setBody("This is the body of the testPut email") 3023 .setScore(1) 3024 .build(); 3025 AppSearchEmail inEmail2 = 3026 new AppSearchEmail.Builder("namespace", "email2") 3027 .setFrom("from@example.com") 3028 .setTo("to1@example.com", "to2@example.com") 3029 .setSubject("testPut example") 3030 .setBody("This is the body of the testPut email") 3031 .setScore(1) 3032 .build(); 3033 3034 String qualifiedId1 = DocumentIdUtil.createQualifiedId( 3035 mContext.getPackageName(), DB_NAME_1, inEmail1); 3036 String qualifiedId2 = DocumentIdUtil.createQualifiedId( 3037 mContext.getPackageName(), DB_NAME_1, inEmail2); 3038 3039 SearchAction searchAction = 3040 new SearchAction.Builder("namespace", "search1", /* actionTimestampMillis= */1000) 3041 .setDocumentTtlMillis(0) 3042 .setQuery("body") 3043 .setFetchedResultCount(20) 3044 .build(); 3045 ClickAction clickAction1 = 3046 new ClickAction.Builder("namespace", "click1", /* actionTimestampMillis= */2000) 3047 .setDocumentTtlMillis(0) 3048 .setQuery("body") 3049 .setReferencedQualifiedId(qualifiedId1) 3050 .setResultRankInBlock(1) 3051 .setResultRankGlobal(1) 3052 .setTimeStayOnResultMillis(512) 3053 .build(); 3054 ClickAction clickAction2 = 3055 new ClickAction.Builder("namespace", "click2", /* actionTimestampMillis= */3000) 3056 .setDocumentTtlMillis(0) 3057 .setQuery("body") 3058 .setReferencedQualifiedId(qualifiedId2) 3059 .setResultRankInBlock(2) 3060 .setResultRankGlobal(2) 3061 .setTimeStayOnResultMillis(128) 3062 .build(); 3063 ClickAction clickAction3 = 3064 new ClickAction.Builder("namespace", "click3", /* actionTimestampMillis= */4000) 3065 .setDocumentTtlMillis(0) 3066 .setQuery("body") 3067 .setReferencedQualifiedId(qualifiedId1) 3068 .setResultRankInBlock(2) 3069 .setResultRankGlobal(2) 3070 .setTimeStayOnResultMillis(256) 3071 .build(); 3072 ImpressionAction impressionAction1 = 3073 new ImpressionAction.Builder( 3074 "namespace", "impression1", /* actionTimestampMillis= */5000) 3075 .setDocumentTtlMillis(0) 3076 .setQuery("body") 3077 .setReferencedQualifiedId(qualifiedId2) 3078 .setResultRankInBlock(2) 3079 .setResultRankGlobal(2) 3080 .build(); 3081 DismissAction dismissAction1 = 3082 new DismissAction.Builder( 3083 "namespace", "dismiss1", /* actionTimestampMillis= */6000) 3084 .setDocumentTtlMillis(0) 3085 .setQuery("body") 3086 .setReferencedQualifiedId(qualifiedId2) 3087 .setResultRankInBlock(2) 3088 .setResultRankGlobal(2) 3089 .build(); 3090 DismissAction dismissAction2 = 3091 new DismissAction.Builder( 3092 "namespace", "dismiss2", /* actionTimestampMillis= */7000) 3093 .setDocumentTtlMillis(0) 3094 .setQuery("body") 3095 .setReferencedQualifiedId(qualifiedId2) 3096 .setResultRankInBlock(2) 3097 .setResultRankGlobal(2) 3098 .build(); 3099 DismissAction dismissAction3 = 3100 new DismissAction.Builder( 3101 "namespace", "dismiss3", /* actionTimestampMillis= */8000) 3102 .setDocumentTtlMillis(0) 3103 .setQuery("body") 3104 .setReferencedQualifiedId(qualifiedId2) 3105 .setResultRankInBlock(2) 3106 .setResultRankGlobal(2) 3107 .build(); 3108 3109 checkIsBatchResultSuccess(mDb1.putAsync( 3110 new PutDocumentsRequest.Builder() 3111 .addGenericDocuments(inEmail1, inEmail2) 3112 .addTakenActions(searchAction, clickAction1, clickAction2, clickAction3, 3113 impressionAction1, dismissAction1, dismissAction2, dismissAction3) 3114 .build())); 3115 3116 SearchSpec nestedSearchSpec = 3117 new SearchSpec.Builder() 3118 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_DOCUMENT_SCORE) 3119 .setOrder(SearchSpec.ORDER_DESCENDING) 3120 .addFilterDocumentClasses(DismissAction.class) 3121 .build(); 3122 3123 // Note: SearchSpec.Builder#setMaxJoinedResultCount only limits the number of child 3124 // documents returned. It does not affect the number of child documents that are scored. 3125 JoinSpec js = new JoinSpec.Builder("referencedQualifiedId") 3126 .setNestedSearch("query:body", nestedSearchSpec) 3127 .setMaxJoinedResultCount(0) 3128 .build(); 3129 3130 // Search "body" for AppSearchEmail documents, ranking by DismissAction signals with 3131 // query = "body" via advanced scoring language syntax to assign negative weights. 3132 SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder() 3133 .setRankingStrategy("-len(this.childrenRankingSignals())") 3134 .setOrder(SearchSpec.ORDER_DESCENDING) 3135 .setJoinSpec(js) 3136 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 3137 .addFilterSchemas(AppSearchEmail.SCHEMA_TYPE) 3138 .build()); 3139 3140 List<SearchResult> sr = searchResults.getNextPageAsync().get(); 3141 3142 assertThat(sr).hasSize(2); 3143 assertThat(sr.get(0).getGenericDocument().getId()).isEqualTo("email1"); 3144 assertThat(sr.get(0).getRankingSignal()).isEqualTo(-0.0); 3145 assertThat(sr.get(1).getGenericDocument().getId()).isEqualTo("email2"); 3146 assertThat(sr.get(1).getRankingSignal()).isEqualTo(-3.0); 3147 3148 } 3149 // @exportToFramework:endStrip() 3150 3151 @Test testQueryRankByClickActions_useTakenActionGenericDocument()3152 public void testQueryRankByClickActions_useTakenActionGenericDocument() throws Exception { 3153 assumeTrue(mDb1.getFeatures() 3154 .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID)); 3155 3156 AppSearchSchema searchActionSchema = new AppSearchSchema.Builder("builtin:SearchAction") 3157 .addProperty(new LongPropertyConfig.Builder("actionType") 3158 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 3159 .build()) 3160 .addProperty(new StringPropertyConfig.Builder("query") 3161 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 3162 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 3163 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 3164 .build() 3165 ).build(); 3166 AppSearchSchema clickActionSchema = new AppSearchSchema.Builder("builtin:ClickAction") 3167 .addProperty(new LongPropertyConfig.Builder("actionType") 3168 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 3169 .build()) 3170 .addProperty(new StringPropertyConfig.Builder("query") 3171 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 3172 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 3173 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 3174 .build() 3175 ).addProperty(new StringPropertyConfig.Builder("referencedQualifiedId") 3176 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 3177 .setJoinableValueType(StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID) 3178 .build() 3179 ).build(); 3180 AppSearchSchema impressionActionSchema = 3181 new AppSearchSchema.Builder("builtin:ImpressionAction") 3182 .addProperty(new LongPropertyConfig.Builder("actionType") 3183 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 3184 .build()) 3185 .addProperty(new StringPropertyConfig.Builder("query") 3186 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 3187 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 3188 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 3189 .build() 3190 ).addProperty(new StringPropertyConfig.Builder("referencedQualifiedId") 3191 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 3192 .setJoinableValueType( 3193 StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID) 3194 .build() 3195 ).build(); 3196 AppSearchSchema dismissActionSchema = 3197 new AppSearchSchema.Builder("builtin:DismissAction") 3198 .addProperty(new LongPropertyConfig.Builder("actionType") 3199 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 3200 .build()) 3201 .addProperty(new StringPropertyConfig.Builder("query") 3202 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 3203 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 3204 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 3205 .build() 3206 ).addProperty(new StringPropertyConfig.Builder("referencedQualifiedId") 3207 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 3208 .setJoinableValueType( 3209 StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID) 3210 .build() 3211 ).build(); 3212 3213 // Schema registration 3214 mDb1.setSchemaAsync( 3215 new SetSchemaRequest.Builder() 3216 .addSchemas(AppSearchEmail.SCHEMA, searchActionSchema, clickActionSchema, 3217 impressionActionSchema, dismissActionSchema) 3218 .build()) 3219 .get(); 3220 3221 // Index several email documents 3222 AppSearchEmail inEmail1 = 3223 new AppSearchEmail.Builder("namespace", "email1") 3224 .setFrom("from@example.com") 3225 .setTo("to1@example.com", "to2@example.com") 3226 .setSubject("testPut example") 3227 .setBody("This is the body of the testPut email") 3228 .setScore(1) 3229 .build(); 3230 AppSearchEmail inEmail2 = 3231 new AppSearchEmail.Builder("namespace", "email2") 3232 .setFrom("from@example.com") 3233 .setTo("to1@example.com", "to2@example.com") 3234 .setSubject("testPut example") 3235 .setBody("This is the body of the testPut email") 3236 .setScore(1) 3237 .build(); 3238 3239 String qualifiedId1 = DocumentIdUtil.createQualifiedId( 3240 mContext.getPackageName(), DB_NAME_1, inEmail1); 3241 String qualifiedId2 = DocumentIdUtil.createQualifiedId( 3242 mContext.getPackageName(), DB_NAME_1, inEmail2); 3243 3244 GenericDocument searchAction = 3245 new GenericDocument.Builder<>("namespace", "search1", "builtin:SearchAction") 3246 .setCreationTimestampMillis(1000) 3247 .setPropertyLong("actionType", ACTION_TYPE_SEARCH) 3248 .setPropertyString("query", "body") 3249 .build(); 3250 GenericDocument clickAction1 = 3251 new GenericDocument.Builder<>("namespace", "click1", "builtin:ClickAction") 3252 .setCreationTimestampMillis(2000) 3253 .setPropertyLong("actionType", ACTION_TYPE_CLICK) 3254 .setPropertyString("query", "body") 3255 .setPropertyString("referencedQualifiedId", qualifiedId1) 3256 .build(); 3257 GenericDocument clickAction2 = 3258 new GenericDocument.Builder<>("namespace", "click2", "builtin:ClickAction") 3259 .setCreationTimestampMillis(3000) 3260 .setPropertyLong("actionType", ACTION_TYPE_CLICK) 3261 .setPropertyString("query", "body") 3262 .setPropertyString("referencedQualifiedId", qualifiedId2) 3263 .build(); 3264 GenericDocument clickAction3 = 3265 new GenericDocument.Builder<>("namespace", "click3", "builtin:ClickAction") 3266 .setCreationTimestampMillis(4000) 3267 .setPropertyLong("actionType", ACTION_TYPE_CLICK) 3268 .setPropertyString("query", "body") 3269 .setPropertyString("referencedQualifiedId", qualifiedId1) 3270 .build(); 3271 GenericDocument impressionAction1 = 3272 new GenericDocument.Builder<>( 3273 "namespace", "impression1", "builtin:ImpressionAction") 3274 .setCreationTimestampMillis(5000) 3275 .setPropertyLong("actionType", ACTION_TYPE_IMPRESSION) 3276 .setPropertyString("query", "body") 3277 .setPropertyString("referencedQualifiedId", qualifiedId2) 3278 .build(); 3279 GenericDocument dismissAction1 = 3280 new GenericDocument.Builder<>("namespace", "dismiss1", "builtin:DismissAction") 3281 .setCreationTimestampMillis(6000) 3282 .setPropertyLong("actionType", ACTION_TYPE_DISMISS) 3283 .setPropertyString("query", "body") 3284 .setPropertyString("referencedQualifiedId", qualifiedId2) 3285 .build(); 3286 3287 checkIsBatchResultSuccess(mDb1.putAsync( 3288 new PutDocumentsRequest.Builder() 3289 .addGenericDocuments(inEmail1, inEmail2) 3290 .addTakenActionGenericDocuments( 3291 searchAction, clickAction1, clickAction2, clickAction3, 3292 impressionAction1, dismissAction1) 3293 .build())); 3294 3295 SearchSpec nestedSearchSpec = 3296 new SearchSpec.Builder() 3297 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_DOCUMENT_SCORE) 3298 .setOrder(SearchSpec.ORDER_DESCENDING) 3299 .addFilterSchemas("builtin:ClickAction") 3300 .build(); 3301 3302 // Note: SearchSpec.Builder#setMaxJoinedResultCount only limits the number of child 3303 // documents returned. It does not affect the number of child documents that are scored. 3304 JoinSpec js = new JoinSpec.Builder("referencedQualifiedId") 3305 .setNestedSearch("query:body", nestedSearchSpec) 3306 .setAggregationScoringStrategy(JoinSpec.AGGREGATION_SCORING_RESULT_COUNT) 3307 .setMaxJoinedResultCount(0) 3308 .build(); 3309 3310 // Search "body" for AppSearchEmail documents, ranking by ClickAction signals with 3311 // query = "body". 3312 SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder() 3313 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_JOIN_AGGREGATE_SCORE) 3314 .setOrder(SearchSpec.ORDER_DESCENDING) 3315 .setJoinSpec(js) 3316 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 3317 .addFilterSchemas(AppSearchEmail.SCHEMA_TYPE) 3318 .build()); 3319 3320 List<SearchResult> sr = searchResults.getNextPageAsync().get(); 3321 3322 assertThat(sr).hasSize(2); 3323 assertThat(sr.get(0).getGenericDocument().getId()).isEqualTo("email1"); 3324 assertThat(sr.get(0).getRankingSignal()).isEqualTo(2.0); 3325 assertThat(sr.get(1).getGenericDocument().getId()).isEqualTo("email2"); 3326 assertThat(sr.get(1).getRankingSignal()).isEqualTo(1.0); 3327 } 3328 3329 @Test testQueryRankByImpressionActions_useTakenActionGenericDocument()3330 public void testQueryRankByImpressionActions_useTakenActionGenericDocument() throws Exception { 3331 assumeTrue(mDb1.getFeatures() 3332 .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID)); 3333 3334 AppSearchSchema searchActionSchema = new AppSearchSchema.Builder("builtin:SearchAction") 3335 .addProperty(new LongPropertyConfig.Builder("actionType") 3336 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 3337 .build()) 3338 .addProperty(new StringPropertyConfig.Builder("query") 3339 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 3340 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 3341 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 3342 .build() 3343 ).build(); 3344 AppSearchSchema clickActionSchema = new AppSearchSchema.Builder("builtin:ClickAction") 3345 .addProperty(new LongPropertyConfig.Builder("actionType") 3346 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 3347 .build()) 3348 .addProperty(new StringPropertyConfig.Builder("query") 3349 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 3350 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 3351 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 3352 .build() 3353 ).addProperty(new StringPropertyConfig.Builder("referencedQualifiedId") 3354 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 3355 .setJoinableValueType(StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID) 3356 .build() 3357 ).build(); 3358 AppSearchSchema impressionActionSchema = 3359 new AppSearchSchema.Builder("builtin:ImpressionAction") 3360 .addProperty(new LongPropertyConfig.Builder("actionType") 3361 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 3362 .build()) 3363 .addProperty(new StringPropertyConfig.Builder("query") 3364 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 3365 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 3366 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 3367 .build() 3368 ).addProperty(new StringPropertyConfig.Builder("referencedQualifiedId") 3369 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 3370 .setJoinableValueType( 3371 StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID) 3372 .build() 3373 ).build(); 3374 AppSearchSchema dismissActionSchema = 3375 new AppSearchSchema.Builder("builtin:DismissAction") 3376 .addProperty(new LongPropertyConfig.Builder("actionType") 3377 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 3378 .build()) 3379 .addProperty(new StringPropertyConfig.Builder("query") 3380 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 3381 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 3382 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 3383 .build() 3384 ).addProperty(new StringPropertyConfig.Builder("referencedQualifiedId") 3385 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 3386 .setJoinableValueType( 3387 StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID) 3388 .build() 3389 ).build(); 3390 3391 // Schema registration 3392 mDb1.setSchemaAsync( 3393 new SetSchemaRequest.Builder() 3394 .addSchemas(AppSearchEmail.SCHEMA, searchActionSchema, clickActionSchema, 3395 impressionActionSchema, dismissActionSchema) 3396 .build()) 3397 .get(); 3398 3399 // Index several email documents 3400 AppSearchEmail inEmail1 = 3401 new AppSearchEmail.Builder("namespace", "email1") 3402 .setFrom("from@example.com") 3403 .setTo("to1@example.com", "to2@example.com") 3404 .setSubject("testPut example") 3405 .setBody("This is the body of the testPut email") 3406 .setScore(1) 3407 .build(); 3408 AppSearchEmail inEmail2 = 3409 new AppSearchEmail.Builder("namespace", "email2") 3410 .setFrom("from@example.com") 3411 .setTo("to1@example.com", "to2@example.com") 3412 .setSubject("testPut example") 3413 .setBody("This is the body of the testPut email") 3414 .setScore(1) 3415 .build(); 3416 3417 String qualifiedId1 = DocumentIdUtil.createQualifiedId( 3418 mContext.getPackageName(), DB_NAME_1, inEmail1); 3419 String qualifiedId2 = DocumentIdUtil.createQualifiedId( 3420 mContext.getPackageName(), DB_NAME_1, inEmail2); 3421 3422 GenericDocument searchAction = 3423 new GenericDocument.Builder<>("namespace", "search1", "builtin:SearchAction") 3424 .setCreationTimestampMillis(1000) 3425 .setPropertyLong("actionType", ACTION_TYPE_SEARCH) 3426 .setPropertyString("query", "body") 3427 .build(); 3428 GenericDocument clickAction1 = 3429 new GenericDocument.Builder<>("namespace", "click1", "builtin:ClickAction") 3430 .setCreationTimestampMillis(2000) 3431 .setPropertyLong("actionType", ACTION_TYPE_CLICK) 3432 .setPropertyString("query", "body") 3433 .setPropertyString("referencedQualifiedId", qualifiedId1) 3434 .build(); 3435 GenericDocument clickAction2 = 3436 new GenericDocument.Builder<>("namespace", "click2", "builtin:ClickAction") 3437 .setCreationTimestampMillis(3000) 3438 .setPropertyLong("actionType", ACTION_TYPE_CLICK) 3439 .setPropertyString("query", "body") 3440 .setPropertyString("referencedQualifiedId", qualifiedId2) 3441 .build(); 3442 GenericDocument clickAction3 = 3443 new GenericDocument.Builder<>("namespace", "click3", "builtin:ClickAction") 3444 .setCreationTimestampMillis(4000) 3445 .setPropertyLong("actionType", ACTION_TYPE_CLICK) 3446 .setPropertyString("query", "body") 3447 .setPropertyString("referencedQualifiedId", qualifiedId1) 3448 .build(); 3449 GenericDocument impressionAction1 = 3450 new GenericDocument.Builder<>( 3451 "namespace", "impression1", "builtin:ImpressionAction") 3452 .setCreationTimestampMillis(5000) 3453 .setPropertyLong("actionType", ACTION_TYPE_IMPRESSION) 3454 .setPropertyString("query", "body") 3455 .setPropertyString("referencedQualifiedId", qualifiedId2) 3456 .build(); 3457 GenericDocument dismissAction1 = 3458 new GenericDocument.Builder<>("namespace", "dismiss1", "builtin:DismissAction") 3459 .setCreationTimestampMillis(6000) 3460 .setPropertyLong("actionType", ACTION_TYPE_DISMISS) 3461 .setPropertyString("query", "body") 3462 .setPropertyString("referencedQualifiedId", qualifiedId2) 3463 .build(); 3464 3465 checkIsBatchResultSuccess(mDb1.putAsync( 3466 new PutDocumentsRequest.Builder() 3467 .addGenericDocuments(inEmail1, inEmail2) 3468 .addTakenActionGenericDocuments( 3469 searchAction, clickAction1, clickAction2, clickAction3, 3470 impressionAction1, dismissAction1) 3471 .build())); 3472 3473 SearchSpec nestedSearchSpec = 3474 new SearchSpec.Builder() 3475 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_DOCUMENT_SCORE) 3476 .setOrder(SearchSpec.ORDER_DESCENDING) 3477 .addFilterSchemas("builtin:ImpressionAction") 3478 .build(); 3479 3480 // Note: SearchSpec.Builder#setMaxJoinedResultCount only limits the number of child 3481 // documents returned. It does not affect the number of child documents that are scored. 3482 JoinSpec js = new JoinSpec.Builder("referencedQualifiedId") 3483 .setNestedSearch("query:body", nestedSearchSpec) 3484 .setAggregationScoringStrategy(JoinSpec.AGGREGATION_SCORING_RESULT_COUNT) 3485 .setMaxJoinedResultCount(0) 3486 .build(); 3487 3488 // Search "body" for AppSearchEmail documents, ranking by ImpressionAction signals with 3489 // query = "body". 3490 SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder() 3491 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_JOIN_AGGREGATE_SCORE) 3492 .setOrder(SearchSpec.ORDER_DESCENDING) 3493 .setJoinSpec(js) 3494 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 3495 .addFilterSchemas(AppSearchEmail.SCHEMA_TYPE) 3496 .build()); 3497 3498 List<SearchResult> sr = searchResults.getNextPageAsync().get(); 3499 3500 assertThat(sr).hasSize(2); 3501 assertThat(sr.get(0).getGenericDocument().getId()).isEqualTo("email2"); 3502 assertThat(sr.get(0).getRankingSignal()).isEqualTo(1.0); 3503 assertThat(sr.get(1).getGenericDocument().getId()).isEqualTo("email1"); 3504 assertThat(sr.get(1).getRankingSignal()).isEqualTo(0.0); 3505 } 3506 3507 @Test testQueryRankByDismissActions_useTakenActionGenericDocument()3508 public void testQueryRankByDismissActions_useTakenActionGenericDocument() throws Exception { 3509 assumeTrue(mDb1.getFeatures() 3510 .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID)); 3511 3512 AppSearchSchema searchActionSchema = new AppSearchSchema.Builder("builtin:SearchAction") 3513 .addProperty(new LongPropertyConfig.Builder("actionType") 3514 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 3515 .build()) 3516 .addProperty(new StringPropertyConfig.Builder("query") 3517 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 3518 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 3519 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 3520 .build() 3521 ).build(); 3522 AppSearchSchema clickActionSchema = new AppSearchSchema.Builder("builtin:ClickAction") 3523 .addProperty(new LongPropertyConfig.Builder("actionType") 3524 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 3525 .build()) 3526 .addProperty(new StringPropertyConfig.Builder("query") 3527 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 3528 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 3529 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 3530 .build() 3531 ).addProperty(new StringPropertyConfig.Builder("referencedQualifiedId") 3532 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 3533 .setJoinableValueType(StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID) 3534 .build() 3535 ).build(); 3536 AppSearchSchema impressionActionSchema = 3537 new AppSearchSchema.Builder("builtin:ImpressionAction") 3538 .addProperty(new LongPropertyConfig.Builder("actionType") 3539 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 3540 .build()) 3541 .addProperty(new StringPropertyConfig.Builder("query") 3542 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 3543 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 3544 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 3545 .build() 3546 ).addProperty(new StringPropertyConfig.Builder("referencedQualifiedId") 3547 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 3548 .setJoinableValueType( 3549 StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID) 3550 .build() 3551 ).build(); 3552 AppSearchSchema dismissActionSchema = 3553 new AppSearchSchema.Builder("builtin:DismissAction") 3554 .addProperty(new LongPropertyConfig.Builder("actionType") 3555 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 3556 .build()) 3557 .addProperty(new StringPropertyConfig.Builder("query") 3558 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 3559 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 3560 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 3561 .build() 3562 ).addProperty(new StringPropertyConfig.Builder("referencedQualifiedId") 3563 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 3564 .setJoinableValueType( 3565 StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID) 3566 .build() 3567 ).build(); 3568 3569 // Schema registration 3570 mDb1.setSchemaAsync( 3571 new SetSchemaRequest.Builder() 3572 .addSchemas(AppSearchEmail.SCHEMA, searchActionSchema, clickActionSchema, 3573 impressionActionSchema, dismissActionSchema) 3574 .build()) 3575 .get(); 3576 3577 // Index several email documents 3578 AppSearchEmail inEmail1 = 3579 new AppSearchEmail.Builder("namespace", "email1") 3580 .setFrom("from@example.com") 3581 .setTo("to1@example.com", "to2@example.com") 3582 .setSubject("testPut example") 3583 .setBody("This is the body of the testPut email") 3584 .setScore(1) 3585 .build(); 3586 AppSearchEmail inEmail2 = 3587 new AppSearchEmail.Builder("namespace", "email2") 3588 .setFrom("from@example.com") 3589 .setTo("to1@example.com", "to2@example.com") 3590 .setSubject("testPut example") 3591 .setBody("This is the body of the testPut email") 3592 .setScore(1) 3593 .build(); 3594 3595 String qualifiedId1 = DocumentIdUtil.createQualifiedId( 3596 mContext.getPackageName(), DB_NAME_1, inEmail1); 3597 String qualifiedId2 = DocumentIdUtil.createQualifiedId( 3598 mContext.getPackageName(), DB_NAME_1, inEmail2); 3599 3600 GenericDocument searchAction = 3601 new GenericDocument.Builder<>("namespace", "search1", "builtin:SearchAction") 3602 .setCreationTimestampMillis(1000) 3603 .setPropertyLong("actionType", ACTION_TYPE_SEARCH) 3604 .setPropertyString("query", "body") 3605 .build(); 3606 GenericDocument clickAction1 = 3607 new GenericDocument.Builder<>("namespace", "click1", "builtin:ClickAction") 3608 .setCreationTimestampMillis(2000) 3609 .setPropertyLong("actionType", ACTION_TYPE_CLICK) 3610 .setPropertyString("query", "body") 3611 .setPropertyString("referencedQualifiedId", qualifiedId1) 3612 .build(); 3613 GenericDocument clickAction2 = 3614 new GenericDocument.Builder<>("namespace", "click2", "builtin:ClickAction") 3615 .setCreationTimestampMillis(3000) 3616 .setPropertyLong("actionType", ACTION_TYPE_CLICK) 3617 .setPropertyString("query", "body") 3618 .setPropertyString("referencedQualifiedId", qualifiedId2) 3619 .build(); 3620 GenericDocument clickAction3 = 3621 new GenericDocument.Builder<>("namespace", "click3", "builtin:ClickAction") 3622 .setCreationTimestampMillis(4000) 3623 .setPropertyLong("actionType", ACTION_TYPE_CLICK) 3624 .setPropertyString("query", "body") 3625 .setPropertyString("referencedQualifiedId", qualifiedId1) 3626 .build(); 3627 GenericDocument impressionAction1 = 3628 new GenericDocument.Builder<>( 3629 "namespace", "impression1", "builtin:ImpressionAction") 3630 .setCreationTimestampMillis(5000) 3631 .setPropertyLong("actionType", ACTION_TYPE_IMPRESSION) 3632 .setPropertyString("query", "body") 3633 .setPropertyString("referencedQualifiedId", qualifiedId2) 3634 .build(); 3635 GenericDocument dismissAction1 = 3636 new GenericDocument.Builder<>("namespace", "dismiss1", "builtin:DismissAction") 3637 .setCreationTimestampMillis(6000) 3638 .setPropertyLong("actionType", ACTION_TYPE_DISMISS) 3639 .setPropertyString("query", "body") 3640 .setPropertyString("referencedQualifiedId", qualifiedId2) 3641 .build(); 3642 GenericDocument dismissAction2 = 3643 new GenericDocument.Builder<>("namespace", "dismiss2", "builtin:DismissAction") 3644 .setCreationTimestampMillis(7000) 3645 .setPropertyLong("actionType", ACTION_TYPE_DISMISS) 3646 .setPropertyString("query", "body") 3647 .setPropertyString("referencedQualifiedId", qualifiedId2) 3648 .build(); 3649 GenericDocument dismissAction3 = 3650 new GenericDocument.Builder<>("namespace", "dismiss3", "builtin:DismissAction") 3651 .setCreationTimestampMillis(8000) 3652 .setPropertyLong("actionType", ACTION_TYPE_DISMISS) 3653 .setPropertyString("query", "body") 3654 .setPropertyString("referencedQualifiedId", qualifiedId2) 3655 .build(); 3656 3657 checkIsBatchResultSuccess(mDb1.putAsync( 3658 new PutDocumentsRequest.Builder() 3659 .addGenericDocuments(inEmail1, inEmail2) 3660 .addTakenActionGenericDocuments( 3661 searchAction, clickAction1, clickAction2, clickAction3, 3662 impressionAction1, dismissAction1, dismissAction2, dismissAction3) 3663 .build())); 3664 3665 SearchSpec nestedSearchSpec = 3666 new SearchSpec.Builder() 3667 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_DOCUMENT_SCORE) 3668 .setOrder(SearchSpec.ORDER_DESCENDING) 3669 .addFilterSchemas("builtin:DismissAction") 3670 .build(); 3671 3672 // Note: SearchSpec.Builder#setMaxJoinedResultCount only limits the number of child 3673 // documents returned. It does not affect the number of child documents that are scored. 3674 JoinSpec js = new JoinSpec.Builder("referencedQualifiedId") 3675 .setNestedSearch("query:body", nestedSearchSpec) 3676 .setMaxJoinedResultCount(0) 3677 .build(); 3678 3679 // Search "body" for AppSearchEmail documents, ranking by DismissAction signals with 3680 // query = "body" via advanced scoring language syntax to assign negative weights. 3681 SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder() 3682 .setRankingStrategy("-len(this.childrenRankingSignals())") 3683 .setOrder(SearchSpec.ORDER_DESCENDING) 3684 .setJoinSpec(js) 3685 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 3686 .addFilterSchemas(AppSearchEmail.SCHEMA_TYPE) 3687 .build()); 3688 3689 List<SearchResult> sr = searchResults.getNextPageAsync().get(); 3690 3691 assertThat(sr).hasSize(2); 3692 assertThat(sr.get(0).getGenericDocument().getId()).isEqualTo("email1"); 3693 assertThat(sr.get(0).getRankingSignal()).isEqualTo(-0.0); 3694 assertThat(sr.get(1).getGenericDocument().getId()).isEqualTo("email2"); 3695 assertThat(sr.get(1).getRankingSignal()).isEqualTo(-3.0); 3696 } 3697 3698 @Test testQuery_invalidAdvancedRanking()3699 public void testQuery_invalidAdvancedRanking() throws Exception { 3700 assumeTrue(mDb1.getFeatures().isFeatureSupported( 3701 Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION)); 3702 3703 // Schema registration 3704 mDb1.setSchemaAsync( 3705 new SetSchemaRequest.Builder() 3706 .addSchemas(AppSearchEmail.SCHEMA) 3707 .build()).get(); 3708 3709 // Index a document 3710 AppSearchEmail inEmail = 3711 new AppSearchEmail.Builder("namespace", "id1") 3712 .setFrom("from@example.com") 3713 .setTo("to1@example.com", "to2@example.com") 3714 .setSubject("testPut example") 3715 .setBody("This is the body of the testPut email") 3716 .build(); 3717 checkIsBatchResultSuccess(mDb1.putAsync( 3718 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build())); 3719 3720 // Query for the document, but set an invalid advanced ranking expression. 3721 SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder() 3722 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 3723 .setRankingStrategy("sqrt()") 3724 .build()); 3725 ExecutionException executionException = assertThrows(ExecutionException.class, 3726 () -> searchResults.getNextPageAsync().get()); 3727 assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class); 3728 AppSearchException exception = (AppSearchException) executionException.getCause(); 3729 assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT); 3730 assertThat(exception).hasMessageThat().contains( 3731 "Math functions must have at least one argument."); 3732 } 3733 3734 @Test testQuery_invalidAdvancedRankingWithChildrenRankingSignals()3735 public void testQuery_invalidAdvancedRankingWithChildrenRankingSignals() throws Exception { 3736 assumeTrue(mDb1.getFeatures().isFeatureSupported( 3737 Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION)); 3738 assumeTrue(mDb1.getFeatures() 3739 .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID)); 3740 3741 // Schema registration 3742 mDb1.setSchemaAsync( 3743 new SetSchemaRequest.Builder() 3744 .addSchemas(AppSearchEmail.SCHEMA) 3745 .build()).get(); 3746 3747 // Index a document 3748 AppSearchEmail inEmail = 3749 new AppSearchEmail.Builder("namespace", "id1") 3750 .setFrom("from@example.com") 3751 .setTo("to1@example.com", "to2@example.com") 3752 .setSubject("testPut example") 3753 .setBody("This is the body of the testPut email") 3754 .build(); 3755 checkIsBatchResultSuccess(mDb1.putAsync( 3756 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build())); 3757 3758 SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder() 3759 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 3760 // Using this.childrenRankingSignals() without the context of a join is invalid. 3761 .setRankingStrategy("sum(this.childrenRankingSignals())") 3762 .build()); 3763 ExecutionException executionException = assertThrows(ExecutionException.class, 3764 () -> searchResults.getNextPageAsync().get()); 3765 assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class); 3766 AppSearchException exception = (AppSearchException) executionException.getCause(); 3767 assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT); 3768 assertThat(exception).hasMessageThat().contains( 3769 "childrenRankingSignals must only be used with join"); 3770 } 3771 3772 @Test testQuery_unsupportedAdvancedRanking()3773 public void testQuery_unsupportedAdvancedRanking() throws Exception { 3774 // Assume that advanced ranking has not been supported. 3775 assumeFalse(mDb1.getFeatures().isFeatureSupported( 3776 Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION)); 3777 3778 // Schema registration 3779 mDb1.setSchemaAsync( 3780 new SetSchemaRequest.Builder() 3781 .addSchemas(AppSearchEmail.SCHEMA) 3782 .build()).get(); 3783 3784 // Index a document 3785 AppSearchEmail inEmail = 3786 new AppSearchEmail.Builder("namespace", "id1") 3787 .setFrom("from@example.com") 3788 .setTo("to1@example.com", "to2@example.com") 3789 .setSubject("testPut example") 3790 .setBody("This is the body of the testPut email") 3791 .build(); 3792 checkIsBatchResultSuccess(mDb1.putAsync( 3793 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build())); 3794 3795 // Query for the document, and set a valid advanced ranking expression. 3796 SearchSpec searchSpec = new SearchSpec.Builder() 3797 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 3798 .setRankingStrategy("sqrt(4)") 3799 .build(); 3800 UnsupportedOperationException e = assertThrows(UnsupportedOperationException.class, 3801 () -> mDb1.search("body", searchSpec)); 3802 assertThat(e).hasMessageThat().contains( 3803 Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION + " is not available on this " 3804 + "AppSearch implementation."); 3805 } 3806 3807 @Test testQuery_typeFilter()3808 public void testQuery_typeFilter() throws Exception { 3809 // Schema registration 3810 AppSearchSchema genericSchema = new AppSearchSchema.Builder("Generic") 3811 .addProperty(new StringPropertyConfig.Builder("foo") 3812 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 3813 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 3814 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 3815 .build() 3816 ).build(); 3817 mDb1.setSchemaAsync( 3818 new SetSchemaRequest.Builder() 3819 .addSchemas(AppSearchEmail.SCHEMA) 3820 .addSchemas(genericSchema) 3821 .build()).get(); 3822 3823 // Index a document 3824 AppSearchEmail inEmail = 3825 new AppSearchEmail.Builder("namespace", "id1") 3826 .setFrom("from@example.com") 3827 .setTo("to1@example.com", "to2@example.com") 3828 .setSubject("testPut example") 3829 .setBody("This is the body of the testPut email") 3830 .build(); 3831 GenericDocument inDoc = new GenericDocument.Builder<>("namespace", "id2", "Generic") 3832 .setPropertyString("foo", "body").build(); 3833 checkIsBatchResultSuccess(mDb1.putAsync( 3834 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail, inDoc).build())); 3835 3836 // Query for the documents 3837 SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder() 3838 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 3839 .build()); 3840 List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults); 3841 assertThat(documents).hasSize(2); 3842 assertThat(documents).containsExactly(inEmail, inDoc); 3843 3844 // Query only for Document 3845 searchResults = mDb1.search("body", new SearchSpec.Builder() 3846 .addFilterSchemas("Generic", "Generic") // duplicate type in filter won't matter. 3847 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 3848 .build()); 3849 documents = convertSearchResultsToDocuments(searchResults); 3850 assertThat(documents).hasSize(1); 3851 assertThat(documents).containsExactly(inDoc); 3852 3853 // Query only for non-existent type 3854 searchResults = mDb1.search("body", new SearchSpec.Builder() 3855 .addFilterSchemas("nonExistType") 3856 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 3857 .build()); 3858 documents = convertSearchResultsToDocuments(searchResults); 3859 assertThat(documents).isEmpty(); 3860 } 3861 3862 @Test testQuery_packageFilter()3863 public void testQuery_packageFilter() throws Exception { 3864 // Schema registration 3865 mDb1.setSchemaAsync( 3866 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get(); 3867 3868 // Index documents 3869 AppSearchEmail email = 3870 new AppSearchEmail.Builder("namespace", "id1") 3871 .setFrom("from@example.com") 3872 .setTo("to1@example.com", "to2@example.com") 3873 .setSubject("foo") 3874 .setBody("This is the body of the testPut email") 3875 .build(); 3876 checkIsBatchResultSuccess(mDb1.putAsync( 3877 new PutDocumentsRequest.Builder().addGenericDocuments(email).build())); 3878 3879 // Query for the document within our package 3880 SearchResults searchResults = mDb1.search("foo", new SearchSpec.Builder() 3881 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 3882 .addFilterPackageNames(ApplicationProvider.getApplicationContext().getPackageName()) 3883 .build()); 3884 List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults); 3885 assertThat(documents).containsExactly(email); 3886 3887 // Query for the document in some other package, which won't exist 3888 searchResults = mDb1.search("foo", new SearchSpec.Builder() 3889 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 3890 .addFilterPackageNames("some.other.package") 3891 .build()); 3892 List<SearchResult> results = searchResults.getNextPageAsync().get(); 3893 assertThat(results).isEmpty(); 3894 } 3895 3896 @Test testQuery_namespaceFilter()3897 public void testQuery_namespaceFilter() throws Exception { 3898 // Schema registration 3899 mDb1.setSchemaAsync( 3900 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get(); 3901 3902 // Index two documents 3903 AppSearchEmail expectedEmail = 3904 new AppSearchEmail.Builder("expectedNamespace", "id1") 3905 .setFrom("from@example.com") 3906 .setTo("to1@example.com", "to2@example.com") 3907 .setSubject("testPut example") 3908 .setBody("This is the body of the testPut email") 3909 .build(); 3910 AppSearchEmail unexpectedEmail = 3911 new AppSearchEmail.Builder("unexpectedNamespace", "id1") 3912 .setFrom("from@example.com") 3913 .setTo("to1@example.com", "to2@example.com") 3914 .setSubject("testPut example") 3915 .setBody("This is the body of the testPut email") 3916 .build(); 3917 checkIsBatchResultSuccess(mDb1.putAsync( 3918 new PutDocumentsRequest.Builder() 3919 .addGenericDocuments(expectedEmail, unexpectedEmail).build())); 3920 3921 // Query for all namespaces 3922 SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder() 3923 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 3924 .build()); 3925 List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults); 3926 assertThat(documents).hasSize(2); 3927 assertThat(documents).containsExactly(expectedEmail, unexpectedEmail); 3928 3929 // Query only for expectedNamespace 3930 searchResults = mDb1.search("body", 3931 new SearchSpec.Builder() 3932 .addFilterNamespaces("expectedNamespace") 3933 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 3934 .build()); 3935 documents = convertSearchResultsToDocuments(searchResults); 3936 assertThat(documents).hasSize(1); 3937 assertThat(documents).containsExactly(expectedEmail); 3938 3939 // Query only for non-existent namespace 3940 searchResults = mDb1.search("body", 3941 new SearchSpec.Builder() 3942 .addFilterNamespaces("nonExistNamespace") 3943 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 3944 .build()); 3945 documents = convertSearchResultsToDocuments(searchResults); 3946 assertThat(documents).isEmpty(); 3947 } 3948 3949 @Test testQuery_getPackageName()3950 public void testQuery_getPackageName() throws Exception { 3951 // Schema registration 3952 mDb1.setSchemaAsync( 3953 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get(); 3954 3955 // Index a document 3956 AppSearchEmail inEmail = 3957 new AppSearchEmail.Builder("namespace", "id1") 3958 .setFrom("from@example.com") 3959 .setTo("to1@example.com", "to2@example.com") 3960 .setSubject("testPut example") 3961 .setBody("This is the body of the testPut email") 3962 .build(); 3963 checkIsBatchResultSuccess(mDb1.putAsync( 3964 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build())); 3965 3966 // Query for the document 3967 SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder() 3968 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 3969 .build()); 3970 3971 List<SearchResult> results; 3972 List<GenericDocument> documents = new ArrayList<>(); 3973 // keep loading next page until it's empty. 3974 do { 3975 results = searchResults.getNextPageAsync().get(); 3976 for (SearchResult result : results) { 3977 assertThat(result.getGenericDocument()).isEqualTo(inEmail); 3978 assertThat(result.getPackageName()).isEqualTo( 3979 ApplicationProvider.getApplicationContext().getPackageName()); 3980 documents.add(result.getGenericDocument()); 3981 } 3982 } while (results.size() > 0); 3983 assertThat(documents).hasSize(1); 3984 } 3985 3986 @Test testQuery_getDatabaseName()3987 public void testQuery_getDatabaseName() throws Exception { 3988 // Schema registration 3989 mDb1.setSchemaAsync( 3990 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get(); 3991 3992 // Index a document 3993 AppSearchEmail inEmail = 3994 new AppSearchEmail.Builder("namespace", "id1") 3995 .setFrom("from@example.com") 3996 .setTo("to1@example.com", "to2@example.com") 3997 .setSubject("testPut example") 3998 .setBody("This is the body of the testPut email") 3999 .build(); 4000 checkIsBatchResultSuccess(mDb1.putAsync( 4001 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build())); 4002 4003 // Query for the document 4004 SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder() 4005 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 4006 .build()); 4007 4008 List<SearchResult> results; 4009 List<GenericDocument> documents = new ArrayList<>(); 4010 // keep loading next page until it's empty. 4011 do { 4012 results = searchResults.getNextPageAsync().get(); 4013 for (SearchResult result : results) { 4014 assertThat(result.getGenericDocument()).isEqualTo(inEmail); 4015 assertThat(result.getDatabaseName()).isEqualTo(DB_NAME_1); 4016 documents.add(result.getGenericDocument()); 4017 } 4018 } while (results.size() > 0); 4019 assertThat(documents).hasSize(1); 4020 4021 // Schema registration for another database 4022 mDb2.setSchemaAsync( 4023 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get(); 4024 4025 checkIsBatchResultSuccess(mDb2.putAsync( 4026 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build())); 4027 4028 // Query for the document 4029 searchResults = mDb2.search("body", new SearchSpec.Builder() 4030 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 4031 .build()); 4032 4033 documents = new ArrayList<>(); 4034 // keep loading next page until it's empty. 4035 do { 4036 results = searchResults.getNextPageAsync().get(); 4037 for (SearchResult result : results) { 4038 assertThat(result.getGenericDocument()).isEqualTo(inEmail); 4039 assertThat(result.getDatabaseName()).isEqualTo(DB_NAME_2); 4040 documents.add(result.getGenericDocument()); 4041 } 4042 } while (results.size() > 0); 4043 assertThat(documents).hasSize(1); 4044 } 4045 4046 @Test testQuery_projection()4047 public void testQuery_projection() throws Exception { 4048 // Schema registration 4049 mDb1.setSchemaAsync( 4050 new SetSchemaRequest.Builder() 4051 .addSchemas(AppSearchEmail.SCHEMA) 4052 .addSchemas(new AppSearchSchema.Builder("Note") 4053 .addProperty(new StringPropertyConfig.Builder("title") 4054 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 4055 .setIndexingType( 4056 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 4057 .setTokenizerType( 4058 StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 4059 .build()) 4060 .addProperty(new StringPropertyConfig.Builder("body") 4061 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 4062 .setIndexingType( 4063 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 4064 .setTokenizerType( 4065 StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 4066 .build()) 4067 .build()) 4068 .build()).get(); 4069 4070 // Index two documents 4071 AppSearchEmail email = 4072 new AppSearchEmail.Builder("namespace", "id1") 4073 .setCreationTimestampMillis(1000) 4074 .setFrom("from@example.com") 4075 .setTo("to1@example.com", "to2@example.com") 4076 .setSubject("testPut example") 4077 .setBody("This is the body of the testPut email") 4078 .build(); 4079 GenericDocument note = 4080 new GenericDocument.Builder<>("namespace", "id2", "Note") 4081 .setCreationTimestampMillis(1000) 4082 .setPropertyString("title", "Note title") 4083 .setPropertyString("body", "Note body").build(); 4084 checkIsBatchResultSuccess(mDb1.putAsync( 4085 new PutDocumentsRequest.Builder() 4086 .addGenericDocuments(email, note).build())); 4087 4088 // Query with type property paths {"Email", ["body", "to"]} 4089 SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder() 4090 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 4091 .addProjection(AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("body", "to")) 4092 .build()); 4093 List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults); 4094 4095 // The email document should have been returned with only the "body" and "to" 4096 // properties. The note document should have been returned with all of its properties. 4097 AppSearchEmail expectedEmail = 4098 new AppSearchEmail.Builder("namespace", "id1") 4099 .setCreationTimestampMillis(1000) 4100 .setTo("to1@example.com", "to2@example.com") 4101 .setBody("This is the body of the testPut email") 4102 .build(); 4103 GenericDocument expectedNote = 4104 new GenericDocument.Builder<>("namespace", "id2", "Note") 4105 .setCreationTimestampMillis(1000) 4106 .setPropertyString("title", "Note title") 4107 .setPropertyString("body", "Note body").build(); 4108 assertThat(documents).containsExactly(expectedNote, expectedEmail); 4109 } 4110 4111 @Test testQuery_projectionEmpty()4112 public void testQuery_projectionEmpty() throws Exception { 4113 // Schema registration 4114 mDb1.setSchemaAsync( 4115 new SetSchemaRequest.Builder() 4116 .addSchemas(AppSearchEmail.SCHEMA) 4117 .addSchemas(new AppSearchSchema.Builder("Note") 4118 .addProperty(new StringPropertyConfig.Builder("title") 4119 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 4120 .setIndexingType( 4121 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 4122 .setTokenizerType( 4123 StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 4124 .build()) 4125 .addProperty(new StringPropertyConfig.Builder("body") 4126 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 4127 .setIndexingType( 4128 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 4129 .setTokenizerType( 4130 StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 4131 .build()) 4132 .build()) 4133 .build()).get(); 4134 4135 // Index two documents 4136 AppSearchEmail email = 4137 new AppSearchEmail.Builder("namespace", "id1") 4138 .setCreationTimestampMillis(1000) 4139 .setFrom("from@example.com") 4140 .setTo("to1@example.com", "to2@example.com") 4141 .setSubject("testPut example") 4142 .setBody("This is the body of the testPut email") 4143 .build(); 4144 GenericDocument note = 4145 new GenericDocument.Builder<>("namespace", "id2", "Note") 4146 .setCreationTimestampMillis(1000) 4147 .setPropertyString("title", "Note title") 4148 .setPropertyString("body", "Note body").build(); 4149 checkIsBatchResultSuccess(mDb1.putAsync( 4150 new PutDocumentsRequest.Builder() 4151 .addGenericDocuments(email, note).build())); 4152 4153 // Query with type property paths {"Email", []} 4154 SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder() 4155 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 4156 .addProjection(AppSearchEmail.SCHEMA_TYPE, Collections.emptyList()) 4157 .build()); 4158 List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults); 4159 4160 // The email document should have been returned without any properties. The note document 4161 // should have been returned with all of its properties. 4162 AppSearchEmail expectedEmail = 4163 new AppSearchEmail.Builder("namespace", "id1") 4164 .setCreationTimestampMillis(1000) 4165 .build(); 4166 GenericDocument expectedNote = 4167 new GenericDocument.Builder<>("namespace", "id2", "Note") 4168 .setCreationTimestampMillis(1000) 4169 .setPropertyString("title", "Note title") 4170 .setPropertyString("body", "Note body").build(); 4171 assertThat(documents).containsExactly(expectedNote, expectedEmail); 4172 } 4173 4174 @Test testQuery_projectionNonExistentType()4175 public void testQuery_projectionNonExistentType() throws Exception { 4176 // Schema registration 4177 mDb1.setSchemaAsync( 4178 new SetSchemaRequest.Builder() 4179 .addSchemas(AppSearchEmail.SCHEMA) 4180 .addSchemas(new AppSearchSchema.Builder("Note") 4181 .addProperty(new StringPropertyConfig.Builder("title") 4182 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 4183 .setIndexingType( 4184 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 4185 .setTokenizerType( 4186 StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 4187 .build()) 4188 .addProperty(new StringPropertyConfig.Builder("body") 4189 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 4190 .setIndexingType( 4191 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 4192 .setTokenizerType( 4193 StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 4194 .build()) 4195 .build()) 4196 .build()).get(); 4197 4198 // Index two documents 4199 AppSearchEmail email = 4200 new AppSearchEmail.Builder("namespace", "id1") 4201 .setCreationTimestampMillis(1000) 4202 .setFrom("from@example.com") 4203 .setTo("to1@example.com", "to2@example.com") 4204 .setSubject("testPut example") 4205 .setBody("This is the body of the testPut email") 4206 .build(); 4207 GenericDocument note = 4208 new GenericDocument.Builder<>("namespace", "id2", "Note") 4209 .setCreationTimestampMillis(1000) 4210 .setPropertyString("title", "Note title") 4211 .setPropertyString("body", "Note body").build(); 4212 checkIsBatchResultSuccess(mDb1.putAsync( 4213 new PutDocumentsRequest.Builder() 4214 .addGenericDocuments(email, note).build())); 4215 4216 // Query with type property paths {"NonExistentType", []}, {"Email", ["body", "to"]} 4217 SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder() 4218 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 4219 .addProjection("NonExistentType", Collections.emptyList()) 4220 .addProjection(AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("body", "to")) 4221 .build()); 4222 List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults); 4223 4224 // The email document should have been returned with only the "body" and "to" properties. 4225 // The note document should have been returned with all of its properties. 4226 AppSearchEmail expectedEmail = 4227 new AppSearchEmail.Builder("namespace", "id1") 4228 .setCreationTimestampMillis(1000) 4229 .setTo("to1@example.com", "to2@example.com") 4230 .setBody("This is the body of the testPut email") 4231 .build(); 4232 GenericDocument expectedNote = 4233 new GenericDocument.Builder<>("namespace", "id2", "Note") 4234 .setCreationTimestampMillis(1000) 4235 .setPropertyString("title", "Note title") 4236 .setPropertyString("body", "Note body").build(); 4237 assertThat(documents).containsExactly(expectedNote, expectedEmail); 4238 } 4239 4240 @Test testQuery_wildcardProjection()4241 public void testQuery_wildcardProjection() throws Exception { 4242 // Schema registration 4243 mDb1.setSchemaAsync( 4244 new SetSchemaRequest.Builder() 4245 .addSchemas(AppSearchEmail.SCHEMA) 4246 .addSchemas(new AppSearchSchema.Builder("Note") 4247 .addProperty(new StringPropertyConfig.Builder("title") 4248 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 4249 .setIndexingType( 4250 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 4251 .setTokenizerType( 4252 StringPropertyConfig.TOKENIZER_TYPE_PLAIN).build()) 4253 .addProperty(new StringPropertyConfig.Builder("body") 4254 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 4255 .setIndexingType( 4256 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 4257 .setTokenizerType( 4258 StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 4259 .build()) 4260 .build()) 4261 .build()).get(); 4262 4263 // Index two documents 4264 AppSearchEmail email = 4265 new AppSearchEmail.Builder("namespace", "id1") 4266 .setCreationTimestampMillis(1000) 4267 .setFrom("from@example.com") 4268 .setTo("to1@example.com", "to2@example.com") 4269 .setSubject("testPut example") 4270 .setBody("This is the body of the testPut email") 4271 .build(); 4272 GenericDocument note = 4273 new GenericDocument.Builder<>("namespace", "id2", "Note") 4274 .setCreationTimestampMillis(1000) 4275 .setPropertyString("title", "Note title") 4276 .setPropertyString("body", "Note body").build(); 4277 checkIsBatchResultSuccess(mDb1.putAsync( 4278 new PutDocumentsRequest.Builder() 4279 .addGenericDocuments(email, note).build())); 4280 4281 // Query with type property paths {"*", ["body", "to"]} 4282 SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder() 4283 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 4284 .addProjection( 4285 SearchSpec.SCHEMA_TYPE_WILDCARD, ImmutableList.of("body", "to")) 4286 .build()); 4287 List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults); 4288 4289 // The email document should have been returned with only the "body" and "to" 4290 // properties. The note document should have been returned with only the "body" property. 4291 AppSearchEmail expectedEmail = 4292 new AppSearchEmail.Builder("namespace", "id1") 4293 .setCreationTimestampMillis(1000) 4294 .setTo("to1@example.com", "to2@example.com") 4295 .setBody("This is the body of the testPut email") 4296 .build(); 4297 GenericDocument expectedNote = 4298 new GenericDocument.Builder<>("namespace", "id2", "Note") 4299 .setCreationTimestampMillis(1000) 4300 .setPropertyString("body", "Note body").build(); 4301 assertThat(documents).containsExactly(expectedNote, expectedEmail); 4302 } 4303 4304 @Test testQuery_wildcardProjectionEmpty()4305 public void testQuery_wildcardProjectionEmpty() throws Exception { 4306 // Schema registration 4307 mDb1.setSchemaAsync( 4308 new SetSchemaRequest.Builder() 4309 .addSchemas(AppSearchEmail.SCHEMA) 4310 .addSchemas(new AppSearchSchema.Builder("Note") 4311 .addProperty(new StringPropertyConfig.Builder("title") 4312 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 4313 .setIndexingType( 4314 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 4315 .setTokenizerType( 4316 StringPropertyConfig.TOKENIZER_TYPE_PLAIN).build()) 4317 .addProperty(new StringPropertyConfig.Builder("body") 4318 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 4319 .setIndexingType( 4320 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 4321 .setTokenizerType( 4322 StringPropertyConfig.TOKENIZER_TYPE_PLAIN).build()) 4323 .build()).build()).get(); 4324 4325 // Index two documents 4326 AppSearchEmail email = 4327 new AppSearchEmail.Builder("namespace", "id1") 4328 .setCreationTimestampMillis(1000) 4329 .setFrom("from@example.com") 4330 .setTo("to1@example.com", "to2@example.com") 4331 .setSubject("testPut example") 4332 .setBody("This is the body of the testPut email") 4333 .build(); 4334 GenericDocument note = 4335 new GenericDocument.Builder<>("namespace", "id2", "Note") 4336 .setCreationTimestampMillis(1000) 4337 .setPropertyString("title", "Note title") 4338 .setPropertyString("body", "Note body").build(); 4339 checkIsBatchResultSuccess(mDb1.putAsync( 4340 new PutDocumentsRequest.Builder() 4341 .addGenericDocuments(email, note).build())); 4342 4343 // Query with type property paths {"*", []} 4344 SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder() 4345 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 4346 .addProjection(SearchSpec.SCHEMA_TYPE_WILDCARD, Collections.emptyList()) 4347 .build()); 4348 List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults); 4349 4350 // The email and note documents should have been returned without any properties. 4351 AppSearchEmail expectedEmail = 4352 new AppSearchEmail.Builder("namespace", "id1") 4353 .setCreationTimestampMillis(1000) 4354 .build(); 4355 GenericDocument expectedNote = 4356 new GenericDocument.Builder<>("namespace", "id2", "Note") 4357 .setCreationTimestampMillis(1000).build(); 4358 assertThat(documents).containsExactly(expectedNote, expectedEmail); 4359 } 4360 4361 @Test testQuery_wildcardProjectionNonExistentType()4362 public void testQuery_wildcardProjectionNonExistentType() throws Exception { 4363 // Schema registration 4364 mDb1.setSchemaAsync( 4365 new SetSchemaRequest.Builder() 4366 .addSchemas(AppSearchEmail.SCHEMA) 4367 .addSchemas(new AppSearchSchema.Builder("Note") 4368 .addProperty(new StringPropertyConfig.Builder("title") 4369 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 4370 .setIndexingType( 4371 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 4372 .setTokenizerType( 4373 StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 4374 .build()) 4375 .addProperty(new StringPropertyConfig.Builder("body") 4376 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 4377 .setIndexingType( 4378 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 4379 .setTokenizerType( 4380 StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 4381 .build()) 4382 .build()) 4383 .build()).get(); 4384 4385 // Index two documents 4386 AppSearchEmail email = 4387 new AppSearchEmail.Builder("namespace", "id1") 4388 .setCreationTimestampMillis(1000) 4389 .setFrom("from@example.com") 4390 .setTo("to1@example.com", "to2@example.com") 4391 .setSubject("testPut example") 4392 .setBody("This is the body of the testPut email") 4393 .build(); 4394 GenericDocument note = 4395 new GenericDocument.Builder<>("namespace", "id2", "Note") 4396 .setCreationTimestampMillis(1000) 4397 .setPropertyString("title", "Note title") 4398 .setPropertyString("body", "Note body").build(); 4399 checkIsBatchResultSuccess(mDb1.putAsync( 4400 new PutDocumentsRequest.Builder() 4401 .addGenericDocuments(email, note).build())); 4402 4403 // Query with type property paths {"NonExistentType", []}, {"*", ["body", "to"]} 4404 SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder() 4405 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 4406 .addProjection("NonExistentType", Collections.emptyList()) 4407 .addProjection( 4408 SearchSpec.SCHEMA_TYPE_WILDCARD, ImmutableList.of("body", "to")) 4409 .build()); 4410 List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults); 4411 4412 // The email document should have been returned with only the "body" and "to" 4413 // properties. The note document should have been returned with only the "body" property. 4414 AppSearchEmail expectedEmail = 4415 new AppSearchEmail.Builder("namespace", "id1") 4416 .setCreationTimestampMillis(1000) 4417 .setTo("to1@example.com", "to2@example.com") 4418 .setBody("This is the body of the testPut email") 4419 .build(); 4420 GenericDocument expectedNote = 4421 new GenericDocument.Builder<>("namespace", "id2", "Note") 4422 .setCreationTimestampMillis(1000) 4423 .setPropertyString("body", "Note body").build(); 4424 assertThat(documents).containsExactly(expectedNote, expectedEmail); 4425 } 4426 4427 @Test testQuery_wildcardProjectionWithExistentType()4428 public void testQuery_wildcardProjectionWithExistentType() throws Exception { 4429 // Schema registration 4430 mDb1.setSchemaAsync( 4431 new SetSchemaRequest.Builder() 4432 .addSchemas(AppSearchEmail.SCHEMA) 4433 .build()).get(); 4434 4435 // Index two documents 4436 AppSearchEmail email1 = 4437 new AppSearchEmail.Builder("namespace", "id1") 4438 .setCreationTimestampMillis(1000) 4439 .setFrom("from@example.com") 4440 .setTo("to1@example.com", "to2@example.com") 4441 .setSubject("testPut example") 4442 .setBody("This is the body of the testPut email") 4443 .build(); 4444 AppSearchEmail email2 = 4445 new AppSearchEmail.Builder("namespace", "id2") 4446 .setCreationTimestampMillis(1000) 4447 .setFrom("from@example.com") 4448 .setTo("to1@example.com", "to2@example.com") 4449 .setSubject("testPut example") 4450 .setBody("This is the body of the testPut email") 4451 .build(); 4452 checkIsBatchResultSuccess(mDb1.putAsync( 4453 new PutDocumentsRequest.Builder() 4454 .addGenericDocuments(email1, email2).build())); 4455 4456 // Get with type property paths {"Email", ["subject", "to"]} 4457 // The SCHEMA_TYPE projection takes preference over PROJECTION_SCHEMA_TYPE_WILDCARD. 4458 GetByDocumentIdRequest request = new GetByDocumentIdRequest.Builder("namespace") 4459 .addIds("id1", "id2") 4460 .addProjection( 4461 GetByDocumentIdRequest.PROJECTION_SCHEMA_TYPE_WILDCARD, 4462 ImmutableList.of("subject", "to")) 4463 .addProjection(AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("from")) 4464 4465 .build(); 4466 List<GenericDocument> outDocuments = doGet(mDb1, request); 4467 4468 // The two email documents should have been returned with "from" properties. 4469 AppSearchEmail expected1 = 4470 new AppSearchEmail.Builder("namespace", "id2") 4471 .setCreationTimestampMillis(1000) 4472 .setFrom("from@example.com") 4473 .build(); 4474 AppSearchEmail expected2 = 4475 new AppSearchEmail.Builder("namespace", "id1") 4476 .setCreationTimestampMillis(1000) 4477 .setFrom("from@example.com") 4478 .build(); 4479 assertThat(outDocuments).containsExactly(expected1, expected2); 4480 } 4481 4482 @Test testQuery_projectionWithMultiplePages()4483 public void testQuery_projectionWithMultiplePages() throws Exception { 4484 // Schema registration 4485 mDb1.setSchemaAsync( 4486 new SetSchemaRequest.Builder() 4487 .addSchemas(AppSearchEmail.SCHEMA) 4488 .build()) 4489 .get(); 4490 PutDocumentsRequest.Builder putDocumentsRequestBuilder = new PutDocumentsRequest.Builder(); 4491 4492 // Index 10 documents. 4493 for (int i = 0; i < 10; i++) { 4494 AppSearchEmail email = 4495 new AppSearchEmail.Builder("namespace", "id" + i) 4496 .setFrom("from@example.com") 4497 .setTo("to1@example.com", "to2@example.com") 4498 .setSubject("testPut example") 4499 .setBody("This is the body of the testPut email " + i) 4500 .build(); 4501 putDocumentsRequestBuilder.addGenericDocuments(email); 4502 } 4503 checkIsBatchResultSuccess(mDb1.putAsync(putDocumentsRequestBuilder.build())); 4504 4505 // Query with type property paths {"Email", ["body", "to"]} 4506 // Set number of results per page to 4. 4507 SearchResults searchResults = 4508 mDb1.search( 4509 "body", 4510 new SearchSpec.Builder() 4511 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 4512 .setResultCountPerPage(4) 4513 .addProjection( 4514 AppSearchEmail.SCHEMA_TYPE, 4515 ImmutableList.of("subject", "body")) 4516 .build()); 4517 4518 // Manually populate documents instead of calling convertSearchResultsToDocuments. 4519 // This is so that the page count can be verified. 4520 List<GenericDocument> documents = new ArrayList<>(); 4521 List<SearchResult> results; 4522 int pageCount = 0; 4523 4524 // Keep loading pages until empty. 4525 do { 4526 results = searchResults.getNextPageAsync().get(); 4527 ++pageCount; 4528 for (SearchResult result : results) { 4529 documents.add(result.getGenericDocument()); 4530 } 4531 } while (results.size() > 0); 4532 4533 assertThat(pageCount).isEqualTo(4); // 3 (upper(10/4)) + 1 (final empty page) 4534 assertThat(documents).hasSize(10); 4535 4536 for (GenericDocument document : documents) { 4537 // Assert that the document has the projected properties. 4538 assertThat(document.getPropertyString("subject")).isEqualTo("testPut example"); 4539 assertThat(document.getPropertyString("body")).contains("This is the body"); 4540 4541 // Assert that a non-projected property is null. 4542 assertThat(document.getPropertyString("to")).isNull(); 4543 } 4544 } 4545 4546 @Test testQuery_projectionWithNestedDocumentsAndMultiplePages()4547 public void testQuery_projectionWithNestedDocumentsAndMultiplePages() throws Exception { 4548 // Schema registration 4549 mDb1.setSchemaAsync( 4550 new SetSchemaRequest.Builder() 4551 .addSchemas(AppSearchEmail.SCHEMA) 4552 .addSchemas( 4553 new AppSearchSchema.Builder("yesNestedIndex") 4554 .addProperty( 4555 new AppSearchSchema.DocumentPropertyConfig.Builder( 4556 "prop", AppSearchEmail.SCHEMA_TYPE) 4557 .setShouldIndexNestedProperties(true) 4558 .build()) 4559 .build()) 4560 .build()) 4561 .get(); 4562 4563 // Index 13 documents. 4564 PutDocumentsRequest.Builder putDocumentsRequestBuilder = new PutDocumentsRequest.Builder(); 4565 for (int i = 0; i < 13; i++) { 4566 AppSearchEmail email = 4567 new AppSearchEmail.Builder("namespace", "id" + i) 4568 .setCreationTimestampMillis(1000) 4569 .setFrom("from@example.com") 4570 .setTo("to1@example.com", "to2@example.com") 4571 .setSubject("testPut example " + i) 4572 .setBody("This is the body of the testPut email " + i) 4573 .build(); 4574 4575 GenericDocument nestedDocument = 4576 new GenericDocument.Builder<>("namespace", "id" + i, "yesNestedIndex") 4577 .setPropertyDocument("prop", email) 4578 .build(); 4579 4580 putDocumentsRequestBuilder.addGenericDocuments(nestedDocument); 4581 } 4582 checkIsBatchResultSuccess(mDb1.putAsync(putDocumentsRequestBuilder.build())); 4583 4584 // Query with projection on nested properties "prop.subject" and "prop.body". 4585 // Set number of results per page to 5. 4586 SearchResults searchResults = 4587 mDb1.search( 4588 "body", 4589 new SearchSpec.Builder() 4590 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 4591 .setResultCountPerPage(5) 4592 .addProjection( 4593 "yesNestedIndex", 4594 ImmutableList.of("prop.subject", "prop.body")) 4595 .build()); 4596 4597 // Manually populate documents instead of calling convertSearchResultsToDocuments. 4598 // This is so that the page count can be verified. 4599 List<GenericDocument> documents = new ArrayList<>(); 4600 List<SearchResult> results; 4601 int pageCount = 0; 4602 4603 // Keep loading pages until empty. 4604 do { 4605 results = searchResults.getNextPageAsync().get(); 4606 ++pageCount; 4607 for (SearchResult result : results) { 4608 documents.add(result.getGenericDocument()); 4609 } 4610 } while (results.size() > 0); 4611 4612 assertThat(pageCount).isEqualTo(4); // 3 (upper(13/5)) + 1 (final empty page) 4613 assertThat(documents).hasSize(13); 4614 4615 for (GenericDocument document : documents) { 4616 // Assert that the document has the projected nested properties. 4617 assertThat(document.getPropertyString("prop.subject")).contains("testPut example"); 4618 assertThat(document.getPropertyString("prop.body")).contains("This is the body"); 4619 4620 // Assert that a non-projected nested property is null. 4621 assertThat(document.getPropertyString("prop.to")).isNull(); 4622 } 4623 } 4624 4625 @Test testQuery_projectionWithMultipleNestedDocumentsAndMultiplePages()4626 public void testQuery_projectionWithMultipleNestedDocumentsAndMultiplePages() throws Exception { 4627 // Schema registration 4628 mDb1.setSchemaAsync( 4629 new SetSchemaRequest.Builder() 4630 .addSchemas(AppSearchEmail.SCHEMA) 4631 .addSchemas( 4632 new AppSearchSchema.Builder("yesOuterNestedIndex") 4633 .addProperty( 4634 new AppSearchSchema.DocumentPropertyConfig.Builder( 4635 "innerNestedProp", 4636 "yesInnerNestedIndex") 4637 .setShouldIndexNestedProperties(true) 4638 .build()) 4639 .build()) 4640 .addSchemas( 4641 new AppSearchSchema.Builder("yesInnerNestedIndex") 4642 .addProperty( 4643 new AppSearchSchema.DocumentPropertyConfig.Builder( 4644 "outerNestedProp", 4645 AppSearchEmail.SCHEMA_TYPE) 4646 .setShouldIndexNestedProperties(true) 4647 .build()) 4648 .build()) 4649 .build()) 4650 .get(); 4651 4652 // Index 28 documents 4653 PutDocumentsRequest.Builder putDocumentsRequestBuilder = new PutDocumentsRequest.Builder(); 4654 for (int i = 0; i < 28; i++) { 4655 AppSearchEmail email = 4656 new AppSearchEmail.Builder("namespace", "id" + i) 4657 .setCreationTimestampMillis(1000) 4658 .setFrom("from@example.com") 4659 .setTo("to1@example.com", "to2@example.com") 4660 .setSubject("testPut example " + i) 4661 .setBody("This is the body of the testPut email " + i) 4662 .build(); 4663 4664 GenericDocument innerNestedDocument = 4665 new GenericDocument.Builder<>("namespace", "id" + i, "yesInnerNestedIndex") 4666 .setPropertyDocument("outerNestedProp", email) 4667 .build(); 4668 4669 GenericDocument outerNestedDocument = 4670 new GenericDocument.Builder<>("namespace", "id" + i, "yesOuterNestedIndex") 4671 .setPropertyDocument("innerNestedProp", innerNestedDocument) 4672 .build(); 4673 4674 putDocumentsRequestBuilder.addGenericDocuments(outerNestedDocument); 4675 } 4676 checkIsBatchResultSuccess(mDb1.putAsync(putDocumentsRequestBuilder.build())); 4677 4678 // Query with projection on nested properties 4679 // "innerNestedProp.outerNestedProp.subject" and "innerNestedProp.outerNestedProp.body". 4680 // Set number of results per page to 7. 4681 SearchResults searchResults = 4682 mDb1.search( 4683 "body", 4684 new SearchSpec.Builder() 4685 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 4686 .setResultCountPerPage(7) 4687 .addProjection( 4688 "yesOuterNestedIndex", 4689 ImmutableList.of( 4690 "innerNestedProp.outerNestedProp.subject", 4691 "innerNestedProp.outerNestedProp.body")) 4692 .build()); 4693 4694 // Manually populate documents instead of calling convertSearchResultsToDocuments. 4695 // This is so that the page count can be verified. 4696 List<GenericDocument> documents = new ArrayList<>(); 4697 List<SearchResult> results; 4698 int pageCount = 0; 4699 4700 // Keep loading pages until empty. 4701 do { 4702 results = searchResults.getNextPageAsync().get(); 4703 ++pageCount; 4704 for (SearchResult result : results) { 4705 documents.add(result.getGenericDocument()); 4706 } 4707 } while (results.size() > 0); 4708 4709 assertThat(pageCount).isEqualTo(5); // 4 (upper(28/7)) + 1 (final empty page) 4710 assertThat(documents).hasSize(28); 4711 4712 for (GenericDocument document : documents) { 4713 // Assert that the document has the projected nested properties. 4714 assertThat(document.getPropertyString("innerNestedProp.outerNestedProp.subject")) 4715 .contains("testPut example"); 4716 assertThat(document.getPropertyString("innerNestedProp.outerNestedProp.body")) 4717 .contains("This is the body"); 4718 4719 // Assert that a non-projected nested property is null. 4720 assertThat(document.getPropertyString("innerNestedProp.outerNestedProp.to")).isNull(); 4721 } 4722 } 4723 4724 @Test testQuery_matchInfoProjection()4725 public void testQuery_matchInfoProjection() throws Exception { 4726 // Schema registration 4727 AppSearchSchema genericSchema = new AppSearchSchema.Builder("Generic") 4728 .addProperty(new StringPropertyConfig.Builder("subject") 4729 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 4730 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 4731 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 4732 .build()) 4733 .build(); 4734 mDb1.setSchemaAsync( 4735 new SetSchemaRequest.Builder().addSchemas(genericSchema).build()).get(); 4736 4737 // Index a document 4738 GenericDocument document = 4739 new GenericDocument.Builder<>("namespace", "id", "Generic") 4740 .setPropertyString("subject", "A commonly used fake word is foo. " 4741 + "Another nonsense word that’s used a lot is bar") 4742 .build(); 4743 checkIsBatchResultSuccess(mDb1.putAsync( 4744 new PutDocumentsRequest.Builder().addGenericDocuments(document).build())); 4745 4746 // Query with type property paths {"Generic", ["subject"]} 4747 SearchResults searchResults = mDb1.search("fo", 4748 new SearchSpec.Builder() 4749 .addFilterSchemas("Generic") 4750 .setSnippetCount(1) 4751 .setSnippetCountPerProperty(1) 4752 .setMaxSnippetSize(10) 4753 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX) 4754 .addProjection("Generic", ImmutableList.of("subject")) 4755 .build()); 4756 List<SearchResult> results = searchResults.getNextPageAsync().get(); 4757 assertThat(results).hasSize(1); 4758 4759 List<SearchResult.MatchInfo> matchInfos = results.get(0).getMatchInfos(); 4760 4761 assertThat(matchInfos).isNotNull(); 4762 assertThat(matchInfos).hasSize(1); 4763 SearchResult.MatchInfo matchInfo = matchInfos.get(0); 4764 assertThat(matchInfo.getPropertyPath()).isEqualTo("subject"); 4765 assertThat(matchInfo.getFullText()).isEqualTo("A commonly used fake word is foo. " 4766 + "Another nonsense word that’s used a lot is bar"); 4767 assertThat(matchInfo.getExactMatchRange()).isEqualTo( 4768 new SearchResult.MatchRange(/*start=*/29, /*end=*/32)); 4769 assertThat(matchInfo.getExactMatch().toString()).isEqualTo("foo"); 4770 assertThat(matchInfo.getSnippetRange()).isEqualTo( 4771 new SearchResult.MatchRange(/*start=*/26, /*end=*/33)); 4772 assertThat(matchInfo.getSnippet().toString()).isEqualTo("is foo."); 4773 assertThat(matchInfo.getPropertyPath()).isEqualTo("subject"); 4774 4775 if (!mDb1.getFeatures().isFeatureSupported( 4776 Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH)) { 4777 assertThrows(UnsupportedOperationException.class, matchInfo::getSubmatchRange); 4778 assertThrows(UnsupportedOperationException.class, matchInfo::getSubmatch); 4779 } else { 4780 assertThat(matchInfo.getSubmatchRange()).isEqualTo( 4781 new SearchResult.MatchRange(/*start=*/29, /*end=*/31)); 4782 assertThat(matchInfo.getSubmatch().toString()).isEqualTo("fo"); 4783 } 4784 } 4785 4786 @Test testQuery_matchInfoProjectionEmpty()4787 public void testQuery_matchInfoProjectionEmpty() throws Exception { 4788 // Schema registration 4789 AppSearchSchema genericSchema = new AppSearchSchema.Builder("Generic") 4790 .addProperty(new StringPropertyConfig.Builder("subject") 4791 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 4792 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 4793 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 4794 .build()) 4795 .build(); 4796 mDb1.setSchemaAsync( 4797 new SetSchemaRequest.Builder().addSchemas(genericSchema).build()).get(); 4798 4799 // Index a document 4800 GenericDocument document = 4801 new GenericDocument.Builder<>("namespace", "id", "Generic") 4802 .setPropertyString("subject", "A commonly used fake word is foo. " 4803 + "Another nonsense word that’s used a lot is bar") 4804 .build(); 4805 checkIsBatchResultSuccess(mDb1.putAsync( 4806 new PutDocumentsRequest.Builder().addGenericDocuments(document).build())); 4807 4808 // Query with type property paths {"Generic", []} 4809 SearchResults searchResults = mDb1.search("fo", 4810 new SearchSpec.Builder() 4811 .addFilterSchemas("Generic") 4812 .setSnippetCount(1) 4813 .setSnippetCountPerProperty(1) 4814 .setMaxSnippetSize(10) 4815 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX) 4816 .addProjection("Generic", Collections.emptyList()) 4817 .build()); 4818 List<SearchResult> results = searchResults.getNextPageAsync().get(); 4819 assertThat(results).hasSize(1); 4820 4821 List<SearchResult.MatchInfo> matchInfos = results.get(0).getMatchInfos(); 4822 assertThat(matchInfos).isEmpty(); 4823 } 4824 4825 @Test 4826 @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SEARCH_SPEC_FILTER_DOCUMENT_IDS) testQuery_documentIdFilter()4827 public void testQuery_documentIdFilter() throws Exception { 4828 assumeTrue(mDb1.getFeatures().isFeatureSupported( 4829 Features.SEARCH_SPEC_ADD_FILTER_DOCUMENT_IDS)); 4830 4831 // Schema registration 4832 mDb1.setSchemaAsync( 4833 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get(); 4834 4835 // Index 3 documents 4836 AppSearchEmail email1 = 4837 new AppSearchEmail.Builder("namespace", "id1") 4838 .setFrom("from@example.com") 4839 .setTo("to1@example.com", "to2@example.com") 4840 .setSubject("testPut example") 4841 .setBody("This is the body of the testPut email") 4842 .build(); 4843 AppSearchEmail email2 = 4844 new AppSearchEmail.Builder("namespace", "id2") 4845 .setFrom("from@example.com") 4846 .setTo("to1@example.com", "to2@example.com") 4847 .setSubject("testPut example") 4848 .setBody("This is the body of the testPut email") 4849 .build(); 4850 AppSearchEmail email3 = 4851 new AppSearchEmail.Builder("namespace", "id3") 4852 .setFrom("from@example.com") 4853 .setTo("to1@example.com", "to2@example.com") 4854 .setSubject("testPut example") 4855 .setBody("This is the body of the testPut email") 4856 .build(); 4857 checkIsBatchResultSuccess(mDb1.putAsync( 4858 new PutDocumentsRequest.Builder() 4859 .addGenericDocuments(email1, email2, email3).build())); 4860 4861 // Query for all ids by an empty document id filter. 4862 SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder() 4863 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 4864 .build()); 4865 List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults); 4866 assertThat(documents).hasSize(3); 4867 assertThat(documents).containsExactly(email1, email2, email3); 4868 4869 // Query for all ids by explicitly specifying them. 4870 searchResults = mDb1.search("body", new SearchSpec.Builder() 4871 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 4872 .addFilterDocumentIds(ImmutableSet.of("id1", "id2", "id3")) 4873 .build()); 4874 documents = convertSearchResultsToDocuments(searchResults); 4875 assertThat(documents).hasSize(3); 4876 assertThat(documents).containsExactly(email1, email2, email3); 4877 4878 // Query only for id1 4879 searchResults = mDb1.search("body", 4880 new SearchSpec.Builder() 4881 .addFilterDocumentIds(ImmutableSet.of("id1")) 4882 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 4883 .build()); 4884 documents = convertSearchResultsToDocuments(searchResults); 4885 assertThat(documents).hasSize(1); 4886 assertThat(documents).containsExactly(email1); 4887 4888 // Query only for id1 and id3 4889 searchResults = mDb1.search("body", 4890 new SearchSpec.Builder() 4891 .addFilterDocumentIds(ImmutableSet.of("id1")) 4892 .addFilterDocumentIds(ImmutableSet.of("id3")) 4893 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 4894 .build()); 4895 documents = convertSearchResultsToDocuments(searchResults); 4896 assertThat(documents).hasSize(2); 4897 assertThat(documents).containsExactly(email1, email3); 4898 } 4899 4900 @Test 4901 @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SEARCH_SPEC_FILTER_DOCUMENT_IDS) testQuery_documentIdFilter_withNamespaceFilter()4902 public void testQuery_documentIdFilter_withNamespaceFilter() throws Exception { 4903 assumeTrue(mDb1.getFeatures().isFeatureSupported( 4904 Features.SEARCH_SPEC_ADD_FILTER_DOCUMENT_IDS)); 4905 4906 // Schema registration 4907 mDb1.setSchemaAsync( 4908 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get(); 4909 4910 // Index 3 documents with "id1" in 2 different namespaces 4911 AppSearchEmail email1 = 4912 new AppSearchEmail.Builder("namespace1", "id1") 4913 .setFrom("from@example.com") 4914 .setTo("to1@example.com", "to2@example.com") 4915 .setSubject("testPut example") 4916 .setBody("This is the body of the testPut email") 4917 .build(); 4918 AppSearchEmail email2 = 4919 new AppSearchEmail.Builder("namespace1", "id2") 4920 .setFrom("from@example.com") 4921 .setTo("to1@example.com", "to2@example.com") 4922 .setSubject("testPut example") 4923 .setBody("This is the body of the testPut email") 4924 .build(); 4925 AppSearchEmail email3 = 4926 new AppSearchEmail.Builder("namespace2", "id1") 4927 .setFrom("from@example.com") 4928 .setTo("to1@example.com", "to2@example.com") 4929 .setSubject("testPut example") 4930 .setBody("This is the body of the testPut email") 4931 .build(); 4932 checkIsBatchResultSuccess(mDb1.putAsync( 4933 new PutDocumentsRequest.Builder() 4934 .addGenericDocuments(email1, email2, email3).build())); 4935 4936 // Query for id1 4937 SearchResults searchResults = mDb1.search("body", 4938 new SearchSpec.Builder() 4939 .addFilterDocumentIds(ImmutableSet.of("id1")) 4940 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 4941 .build()); 4942 List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults); 4943 assertThat(documents).hasSize(2); 4944 assertThat(documents).containsExactly(email1, email3); 4945 4946 // Query only for id1 in namespace1 4947 searchResults = mDb1.search("body", 4948 new SearchSpec.Builder() 4949 .addFilterDocumentIds(ImmutableSet.of("id1")) 4950 .addFilterNamespaces("namespace1") 4951 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 4952 .build()); 4953 documents = convertSearchResultsToDocuments(searchResults); 4954 assertThat(documents).hasSize(1); 4955 assertThat(documents).containsExactly(email1); 4956 } 4957 4958 @Test 4959 @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SEARCH_SPEC_FILTER_DOCUMENT_IDS) testQuery_documentIdFilter_nonExistentId()4960 public void testQuery_documentIdFilter_nonExistentId() throws Exception { 4961 assumeTrue(mDb1.getFeatures().isFeatureSupported( 4962 Features.SEARCH_SPEC_ADD_FILTER_DOCUMENT_IDS)); 4963 4964 // Schema registration 4965 mDb1.setSchemaAsync( 4966 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get(); 4967 4968 // Index 3 documents 4969 AppSearchEmail email1 = 4970 new AppSearchEmail.Builder("namespace", "id1") 4971 .setFrom("from@example.com") 4972 .setTo("to1@example.com", "to2@example.com") 4973 .setSubject("testPut example") 4974 .setBody("This is the body of the testPut email") 4975 .build(); 4976 AppSearchEmail email2 = 4977 new AppSearchEmail.Builder("namespace", "id2") 4978 .setFrom("from@example.com") 4979 .setTo("to1@example.com", "to2@example.com") 4980 .setSubject("testPut example") 4981 .setBody("This is the body of the testPut email") 4982 .build(); 4983 AppSearchEmail email3 = 4984 new AppSearchEmail.Builder("namespace", "id3") 4985 .setFrom("from@example.com") 4986 .setTo("to1@example.com", "to2@example.com") 4987 .setSubject("testPut example") 4988 .setBody("This is the body of the testPut email") 4989 .build(); 4990 checkIsBatchResultSuccess(mDb1.putAsync( 4991 new PutDocumentsRequest.Builder() 4992 .addGenericDocuments(email1, email2, email3).build())); 4993 4994 // Query for a non-existent id, which should return nothing. 4995 SearchResults searchResults = mDb1.search("body", 4996 new SearchSpec.Builder() 4997 .addFilterDocumentIds(ImmutableSet.of("nonExistId")) 4998 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 4999 .build()); 5000 List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults); 5001 assertThat(documents).isEmpty(); 5002 } 5003 5004 @Test 5005 @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SEARCH_SPEC_FILTER_DOCUMENT_IDS) testQuery_documentIdFilter_notSupported()5006 public void testQuery_documentIdFilter_notSupported() throws Exception { 5007 assumeFalse(mDb1.getFeatures().isFeatureSupported( 5008 Features.SEARCH_SPEC_ADD_FILTER_DOCUMENT_IDS)); 5009 5010 UnsupportedOperationException exception = assertThrows( 5011 UnsupportedOperationException.class, 5012 () -> mDb1.search("body", 5013 new SearchSpec.Builder() 5014 .addFilterDocumentIds(ImmutableSet.of("id1")) 5015 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 5016 .build())); 5017 assertThat(exception).hasMessageThat().contains( 5018 Features.SEARCH_SPEC_ADD_FILTER_DOCUMENT_IDS 5019 + " is not available on this AppSearch implementation."); 5020 } 5021 5022 @Test testSearchSpec_setSourceTag_notSupported()5023 public void testSearchSpec_setSourceTag_notSupported() { 5024 assumeFalse(mDb1.getFeatures().isFeatureSupported( 5025 Features.SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG)); 5026 // UnsupportedOperationException will be thrown with these queries so no need to 5027 // define a schema and index document. 5028 SearchSpec.Builder builder = new SearchSpec.Builder(); 5029 SearchSpec searchSpec = builder.setSearchSourceLogTag("tag").build(); 5030 5031 UnsupportedOperationException exception = assertThrows( 5032 UnsupportedOperationException.class, 5033 () -> mDb1.search("\"Hello, world!\"", searchSpec)); 5034 assertThat(exception).hasMessageThat().contains( 5035 Features.SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG 5036 + " is not available on this AppSearch implementation."); 5037 } 5038 5039 @Test testQuery_twoInstances()5040 public void testQuery_twoInstances() throws Exception { 5041 // Schema registration 5042 mDb1.setSchemaAsync(new SetSchemaRequest.Builder() 5043 .addSchemas(AppSearchEmail.SCHEMA).build()).get(); 5044 mDb2.setSchemaAsync(new SetSchemaRequest.Builder() 5045 .addSchemas(AppSearchEmail.SCHEMA).build()).get(); 5046 5047 // Index a document to instance 1. 5048 AppSearchEmail inEmail1 = 5049 new AppSearchEmail.Builder("namespace", "id1") 5050 .setFrom("from@example.com") 5051 .setTo("to1@example.com", "to2@example.com") 5052 .setSubject("testPut example") 5053 .setBody("This is the body of the testPut email") 5054 .build(); 5055 checkIsBatchResultSuccess(mDb1.putAsync( 5056 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build())); 5057 5058 // Index a document to instance 2. 5059 AppSearchEmail inEmail2 = 5060 new AppSearchEmail.Builder("namespace", "id2") 5061 .setFrom("from@example.com") 5062 .setTo("to1@example.com", "to2@example.com") 5063 .setSubject("testPut example") 5064 .setBody("This is the body of the testPut email") 5065 .build(); 5066 checkIsBatchResultSuccess(mDb2.putAsync( 5067 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail2).build())); 5068 5069 // Query for instance 1. 5070 SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder() 5071 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 5072 .build()); 5073 List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults); 5074 assertThat(documents).hasSize(1); 5075 assertThat(documents).containsExactly(inEmail1); 5076 5077 // Query for instance 2. 5078 searchResults = mDb2.search("body", new SearchSpec.Builder() 5079 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 5080 .build()); 5081 documents = convertSearchResultsToDocuments(searchResults); 5082 assertThat(documents).hasSize(1); 5083 assertThat(documents).containsExactly(inEmail2); 5084 } 5085 5086 @Test testQuery_typePropertyFilters()5087 public void testQuery_typePropertyFilters() throws Exception { 5088 assumeTrue(mDb1.getFeatures().isFeatureSupported( 5089 Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES)); 5090 // Schema registration 5091 mDb1.setSchemaAsync( 5092 new SetSchemaRequest.Builder() 5093 .addSchemas(AppSearchEmail.SCHEMA) 5094 .build()).get(); 5095 5096 // Index two documents 5097 AppSearchEmail email1 = 5098 new AppSearchEmail.Builder("namespace", "id1") 5099 .setCreationTimestampMillis(1000) 5100 .setFrom("from@example.com") 5101 .setTo("to1@example.com", "to2@example.com") 5102 .setSubject("testPut example") 5103 .setBody("This is the body of the testPut email") 5104 .build(); 5105 AppSearchEmail email2 = 5106 new AppSearchEmail.Builder("namespace", "id2") 5107 .setCreationTimestampMillis(1000) 5108 .setFrom("from@example.com") 5109 .setTo("to1@example.com", "to2@example.com") 5110 .setSubject("testPut example subject with some body") 5111 .setBody("This is the body of the testPut email") 5112 .build(); 5113 checkIsBatchResultSuccess(mDb1.putAsync( 5114 new PutDocumentsRequest.Builder() 5115 .addGenericDocuments(email1, email2).build())); 5116 5117 // Query with type property filters {"Email", ["subject", "to"]} 5118 SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder() 5119 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 5120 .addFilterProperties(AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("subject", "to")) 5121 .build()); 5122 List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults); 5123 // Only email2 should be returned because email1 doesn't have the term "body" in subject 5124 // or to fields 5125 assertThat(documents).containsExactly(email2); 5126 } 5127 5128 @Test testQuery_typePropertyFiltersWithDifferentSchemaTypes()5129 public void testQuery_typePropertyFiltersWithDifferentSchemaTypes() throws Exception { 5130 assumeTrue(mDb1.getFeatures().isFeatureSupported( 5131 Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES)); 5132 // Schema registration 5133 mDb1.setSchemaAsync( 5134 new SetSchemaRequest.Builder() 5135 .addSchemas(AppSearchEmail.SCHEMA) 5136 .addSchemas(new AppSearchSchema.Builder("Note") 5137 .addProperty(new StringPropertyConfig.Builder("title") 5138 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 5139 .setIndexingType( 5140 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 5141 .setTokenizerType( 5142 StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 5143 .build()) 5144 .addProperty(new StringPropertyConfig.Builder("body") 5145 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 5146 .setIndexingType( 5147 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 5148 .setTokenizerType( 5149 StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 5150 .build()) 5151 .build()) 5152 .build()).get(); 5153 5154 // Index two documents 5155 AppSearchEmail email = 5156 new AppSearchEmail.Builder("namespace", "id1") 5157 .setCreationTimestampMillis(1000) 5158 .setFrom("from@example.com") 5159 .setTo("to1@example.com", "to2@example.com") 5160 .setSubject("testPut example") 5161 .setBody("This is the body of the testPut email") 5162 .build(); 5163 GenericDocument note = 5164 new GenericDocument.Builder<>("namespace", "id2", "Note") 5165 .setCreationTimestampMillis(1000) 5166 .setPropertyString("title", "Note title") 5167 .setPropertyString("body", "Note body").build(); 5168 checkIsBatchResultSuccess(mDb1.putAsync( 5169 new PutDocumentsRequest.Builder() 5170 .addGenericDocuments(email, note).build())); 5171 5172 // Query with type property paths {"Email": ["subject", "to"], "Note": ["body"]}. Note 5173 // schema has body in its property filter but Email schema doesn't. 5174 SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder() 5175 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 5176 .addFilterProperties(AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("subject", "to")) 5177 .addFilterProperties("Note", ImmutableList.of("body")) 5178 .build()); 5179 List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults); 5180 // Only the note document should be returned because the email property filter doesn't 5181 // allow searching in the body. 5182 assertThat(documents).containsExactly(note); 5183 } 5184 5185 @Test testQuery_typePropertyFiltersWithWildcard()5186 public void testQuery_typePropertyFiltersWithWildcard() throws Exception { 5187 assumeTrue(mDb1.getFeatures().isFeatureSupported( 5188 Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES)); 5189 // Schema registration 5190 mDb1.setSchemaAsync( 5191 new SetSchemaRequest.Builder() 5192 .addSchemas(AppSearchEmail.SCHEMA) 5193 .addSchemas(new AppSearchSchema.Builder("Note") 5194 .addProperty(new StringPropertyConfig.Builder("title") 5195 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 5196 .setIndexingType( 5197 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 5198 .setTokenizerType( 5199 StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 5200 .build()) 5201 .addProperty(new StringPropertyConfig.Builder("body") 5202 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 5203 .setIndexingType( 5204 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 5205 .setTokenizerType( 5206 StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 5207 .build()) 5208 .build()) 5209 .build()).get(); 5210 5211 // Index two documents 5212 AppSearchEmail email = 5213 new AppSearchEmail.Builder("namespace", "id1") 5214 .setCreationTimestampMillis(1000) 5215 .setFrom("from@example.com") 5216 .setTo("to1@example.com", "to2@example.com") 5217 .setSubject("testPut example subject with some body") 5218 .setBody("This is the body of the testPut email") 5219 .build(); 5220 GenericDocument note = 5221 new GenericDocument.Builder<>("namespace", "id2", "Note") 5222 .setCreationTimestampMillis(1000) 5223 .setPropertyString("title", "Note title") 5224 .setPropertyString("body", "Note body").build(); 5225 checkIsBatchResultSuccess(mDb1.putAsync( 5226 new PutDocumentsRequest.Builder() 5227 .addGenericDocuments(email, note).build())); 5228 5229 // Query with type property paths {"*": ["subject", "title"]} 5230 SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder() 5231 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 5232 .addFilterProperties(SearchSpec.SCHEMA_TYPE_WILDCARD, 5233 ImmutableList.of("subject", "title")) 5234 .build()); 5235 List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults); 5236 // The wildcard property filter will apply to both the Email and Note schema. The email 5237 // document should be returned since it has the term "body" in its subject property. The 5238 // note document should not be returned since it doesn't have the term "body" in the title 5239 // property (subject property is not applicable for Note schema) 5240 assertThat(documents).containsExactly(email); 5241 } 5242 5243 @Test testQuery_typePropertyFiltersWithWildcardAndExplicitSchema()5244 public void testQuery_typePropertyFiltersWithWildcardAndExplicitSchema() throws Exception { 5245 assumeTrue(mDb1.getFeatures().isFeatureSupported( 5246 Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES)); 5247 // Schema registration 5248 mDb1.setSchemaAsync( 5249 new SetSchemaRequest.Builder() 5250 .addSchemas(AppSearchEmail.SCHEMA) 5251 .addSchemas(new AppSearchSchema.Builder("Note") 5252 .addProperty(new StringPropertyConfig.Builder("title") 5253 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 5254 .setIndexingType( 5255 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 5256 .setTokenizerType( 5257 StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 5258 .build()) 5259 .addProperty(new StringPropertyConfig.Builder("body") 5260 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 5261 .setIndexingType( 5262 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 5263 .setTokenizerType( 5264 StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 5265 .build()) 5266 .build()) 5267 .build()).get(); 5268 5269 // Index two documents 5270 AppSearchEmail email = 5271 new AppSearchEmail.Builder("namespace", "id1") 5272 .setCreationTimestampMillis(1000) 5273 .setFrom("from@example.com") 5274 .setTo("to1@example.com", "to2@example.com") 5275 .setSubject("testPut example subject with some body") 5276 .setBody("This is the body of the testPut email") 5277 .build(); 5278 GenericDocument note = 5279 new GenericDocument.Builder<>("namespace", "id2", "Note") 5280 .setCreationTimestampMillis(1000) 5281 .setPropertyString("title", "Note title") 5282 .setPropertyString("body", "Note body").build(); 5283 checkIsBatchResultSuccess(mDb1.putAsync( 5284 new PutDocumentsRequest.Builder() 5285 .addGenericDocuments(email, note).build())); 5286 5287 // Query with type property paths {"*": ["subject", "title"], "Note": ["body"]} 5288 SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder() 5289 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 5290 .addFilterProperties(SearchSpec.SCHEMA_TYPE_WILDCARD, 5291 ImmutableList.of("subject", "title")) 5292 .addFilterProperties("Note", ImmutableList.of("body")) 5293 .build()); 5294 List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults); 5295 // The wildcard property filter will only apply to the Email schema since Note schema has 5296 // its own explicit property filter specified. The email document should be returned since 5297 // it has the term "body" in its subject property. The note document should also be returned 5298 // since it has the term "body" in the body property. 5299 assertThat(documents).containsExactly(email, note); 5300 } 5301 5302 @Test testQuery_typePropertyFiltersNonExistentType()5303 public void testQuery_typePropertyFiltersNonExistentType() throws Exception { 5304 assumeTrue(mDb1.getFeatures().isFeatureSupported( 5305 Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES)); 5306 // Schema registration 5307 mDb1.setSchemaAsync( 5308 new SetSchemaRequest.Builder() 5309 .addSchemas(AppSearchEmail.SCHEMA) 5310 .addSchemas(new AppSearchSchema.Builder("Note") 5311 .addProperty(new StringPropertyConfig.Builder("title") 5312 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 5313 .setIndexingType( 5314 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 5315 .setTokenizerType( 5316 StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 5317 .build()) 5318 .addProperty(new StringPropertyConfig.Builder("body") 5319 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 5320 .setIndexingType( 5321 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 5322 .setTokenizerType( 5323 StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 5324 .build()) 5325 .build()) 5326 .build()).get(); 5327 5328 // Index two documents 5329 AppSearchEmail email = 5330 new AppSearchEmail.Builder("namespace", "id1") 5331 .setCreationTimestampMillis(1000) 5332 .setFrom("from@example.com") 5333 .setTo("to1@example.com", "to2@example.com") 5334 .setSubject("testPut example subject with some body") 5335 .setBody("This is the body of the testPut email") 5336 .build(); 5337 GenericDocument note = 5338 new GenericDocument.Builder<>("namespace", "id2", "Note") 5339 .setCreationTimestampMillis(1000) 5340 .setPropertyString("title", "Note title") 5341 .setPropertyString("body", "Note body").build(); 5342 checkIsBatchResultSuccess(mDb1.putAsync( 5343 new PutDocumentsRequest.Builder() 5344 .addGenericDocuments(email, note).build())); 5345 5346 // Query with type property paths {"NonExistentType": ["to", "title"]} 5347 SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder() 5348 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 5349 .addFilterProperties("NonExistentType", ImmutableList.of("to", "title")) 5350 .build()); 5351 List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults); 5352 // The supplied property filters don't apply to either schema types. Both the documents 5353 // should be returned since the term "body" is present in at least one of their properties. 5354 assertThat(documents).containsExactly(email, note); 5355 } 5356 5357 @Test testQuery_typePropertyFiltersEmpty()5358 public void testQuery_typePropertyFiltersEmpty() throws Exception { 5359 assumeTrue(mDb1.getFeatures().isFeatureSupported( 5360 Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES)); 5361 // Schema registration 5362 mDb1.setSchemaAsync( 5363 new SetSchemaRequest.Builder() 5364 .addSchemas(AppSearchEmail.SCHEMA) 5365 .addSchemas(new AppSearchSchema.Builder("Note") 5366 .addProperty(new StringPropertyConfig.Builder("title") 5367 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 5368 .setIndexingType( 5369 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 5370 .setTokenizerType( 5371 StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 5372 .build()) 5373 .addProperty(new StringPropertyConfig.Builder("body") 5374 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 5375 .setIndexingType( 5376 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 5377 .setTokenizerType( 5378 StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 5379 .build()) 5380 .build()) 5381 .build()).get(); 5382 5383 // Index two documents 5384 AppSearchEmail email = 5385 new AppSearchEmail.Builder("namespace", "id1") 5386 .setCreationTimestampMillis(1000) 5387 .setFrom("from@example.com") 5388 .setTo("to1@example.com", "to2@example.com") 5389 .setSubject("testPut example") 5390 .setBody("This is the body of the testPut email") 5391 .build(); 5392 GenericDocument note = 5393 new GenericDocument.Builder<>("namespace", "id2", "Note") 5394 .setCreationTimestampMillis(1000) 5395 .setPropertyString("title", "Note title") 5396 .setPropertyString("body", "Note body").build(); 5397 checkIsBatchResultSuccess(mDb1.putAsync( 5398 new PutDocumentsRequest.Builder() 5399 .addGenericDocuments(email, note).build())); 5400 5401 // Query with type property paths {"email": []} 5402 SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder() 5403 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 5404 .addFilterProperties(AppSearchEmail.SCHEMA_TYPE, Collections.emptyList()) 5405 .build()); 5406 List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults); 5407 // The email document should not be returned since the property filter doesn't allow 5408 // searching any property. 5409 assertThat(documents).containsExactly(note); 5410 } 5411 5412 @Test testSnippet()5413 public void testSnippet() throws Exception { 5414 // Schema registration 5415 AppSearchSchema genericSchema = new AppSearchSchema.Builder("Generic") 5416 .addProperty(new StringPropertyConfig.Builder("subject") 5417 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 5418 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 5419 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 5420 .build() 5421 ).build(); 5422 mDb1.setSchemaAsync( 5423 new SetSchemaRequest.Builder().addSchemas(genericSchema).build()).get(); 5424 5425 // Index a document 5426 GenericDocument document = 5427 new GenericDocument.Builder<>("namespace", "id", "Generic") 5428 .setPropertyString("subject", "A commonly used fake word is foo. " 5429 + "Another nonsense word that’s used a lot is bar") 5430 .build(); 5431 checkIsBatchResultSuccess(mDb1.putAsync( 5432 new PutDocumentsRequest.Builder().addGenericDocuments(document).build())); 5433 5434 // Query for the document 5435 SearchResults searchResults = mDb1.search("fo", 5436 new SearchSpec.Builder() 5437 .addFilterSchemas("Generic") 5438 .setSnippetCount(1) 5439 .setSnippetCountPerProperty(1) 5440 .setMaxSnippetSize(10) 5441 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX) 5442 .build()); 5443 List<SearchResult> results = searchResults.getNextPageAsync().get(); 5444 assertThat(results).hasSize(1); 5445 5446 List<SearchResult.MatchInfo> matchInfos = results.get(0).getMatchInfos(); 5447 assertThat(matchInfos).isNotNull(); 5448 assertThat(matchInfos).hasSize(1); 5449 SearchResult.MatchInfo matchInfo = matchInfos.get(0); 5450 assertThat(matchInfo.getFullText()).isEqualTo("A commonly used fake word is foo. " 5451 + "Another nonsense word that’s used a lot is bar"); 5452 assertThat(matchInfo.getExactMatchRange()).isEqualTo( 5453 new SearchResult.MatchRange(/*start=*/29, /*end=*/32)); 5454 assertThat(matchInfo.getExactMatch().toString()).isEqualTo("foo"); 5455 assertThat(matchInfo.getSnippetRange()).isEqualTo( 5456 new SearchResult.MatchRange(/*start=*/26, /*end=*/33)); 5457 assertThat(matchInfo.getSnippet().toString()).isEqualTo("is foo."); 5458 5459 if (!mDb1.getFeatures().isFeatureSupported( 5460 Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH)) { 5461 assertThrows(UnsupportedOperationException.class, matchInfo::getSubmatchRange); 5462 assertThrows(UnsupportedOperationException.class, matchInfo::getSubmatch); 5463 } else { 5464 assertThat(matchInfo.getSubmatchRange()).isEqualTo( 5465 new SearchResult.MatchRange(/*start=*/29, /*end=*/31)); 5466 assertThat(matchInfo.getSubmatch().toString()).isEqualTo("fo"); 5467 } 5468 } 5469 5470 @Test 5471 @RequiresFlagsEnabled(Flags.FLAG_ENABLE_EMBEDDING_MATCH_INFO) testSnippet_usingTextMatchInfo()5472 public void testSnippet_usingTextMatchInfo() throws Exception { 5473 // Schema registration 5474 AppSearchSchema genericSchema = new AppSearchSchema.Builder("Generic") 5475 .addProperty(new StringPropertyConfig.Builder("subject") 5476 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 5477 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 5478 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 5479 .build() 5480 ).build(); 5481 mDb1.setSchemaAsync( 5482 new SetSchemaRequest.Builder().addSchemas(genericSchema).build()).get(); 5483 5484 // Index a document 5485 GenericDocument document = 5486 new GenericDocument.Builder<>("namespace", "id", "Generic") 5487 .setPropertyString("subject", "A commonly used fake word is foo. " 5488 + "Another nonsense word that’s used a lot is bar") 5489 .build(); 5490 checkIsBatchResultSuccess(mDb1.putAsync( 5491 new PutDocumentsRequest.Builder().addGenericDocuments(document).build())); 5492 5493 // Query for the document 5494 SearchResults searchResults = mDb1.search("fo", 5495 new SearchSpec.Builder() 5496 .addFilterSchemas("Generic") 5497 .setSnippetCount(1) 5498 .setSnippetCountPerProperty(1) 5499 .setMaxSnippetSize(10) 5500 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX) 5501 .build()); 5502 List<SearchResult> results = searchResults.getNextPageAsync().get(); 5503 assertThat(results).hasSize(1); 5504 5505 List<SearchResult.MatchInfo> matchInfos = results.get(0).getMatchInfos(); 5506 assertThat(matchInfos).isNotNull(); 5507 assertThat(matchInfos).hasSize(1); 5508 SearchResult.MatchInfo matchInfo = matchInfos.get(0); 5509 assertThat(matchInfo.getTextMatch()).isNotNull(); 5510 assertThat(matchInfo.getTextMatch().getFullText()).isEqualTo( 5511 "A commonly used fake word is foo. " 5512 + "Another nonsense word that’s used a lot is bar"); 5513 assertThat(matchInfo.getTextMatch().getExactMatchRange()).isEqualTo( 5514 new SearchResult.MatchRange(/*start=*/29, /*end=*/32)); 5515 assertThat(matchInfo.getTextMatch().getExactMatch().toString()).isEqualTo("foo"); 5516 assertThat(matchInfo.getTextMatch().getSnippetRange()).isEqualTo( 5517 new SearchResult.MatchRange(/*start=*/26, /*end=*/33)); 5518 assertThat(matchInfo.getTextMatch().getSnippet().toString()).isEqualTo("is foo."); 5519 assertThat(matchInfo.getTextMatch().getSubmatchRange()).isEqualTo( 5520 new SearchResult.MatchRange(/*start=*/29, /*end=*/31)); 5521 assertThat(matchInfo.getTextMatch().getSubmatch().toString()).isEqualTo("fo"); 5522 } 5523 5524 @Test testSetSnippetCount()5525 public void testSetSnippetCount() throws Exception { 5526 // Schema registration 5527 AppSearchSchema genericSchema = new AppSearchSchema.Builder("Generic") 5528 .addProperty(new StringPropertyConfig.Builder("subject") 5529 .setCardinality(PropertyConfig.CARDINALITY_REPEATED) 5530 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 5531 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 5532 .build() 5533 ).build(); 5534 mDb1.setSchemaAsync( 5535 new SetSchemaRequest.Builder().addSchemas(genericSchema).build()).get(); 5536 5537 // Index documents 5538 checkIsBatchResultSuccess(mDb1.putAsync(new PutDocumentsRequest.Builder() 5539 .addGenericDocuments( 5540 new GenericDocument.Builder<>("namespace", "id1", "Generic") 5541 .setPropertyString( 5542 "subject", 5543 "I like cats", "I like dogs", "I like birds", "I like fish") 5544 .setScore(10) 5545 .build(), 5546 new GenericDocument.Builder<>("namespace", "id2", "Generic") 5547 .setPropertyString( 5548 "subject", 5549 "I like red", 5550 "I like green", 5551 "I like blue", 5552 "I like yellow") 5553 .setScore(20) 5554 .build(), 5555 new GenericDocument.Builder<>("namespace", "id3", "Generic") 5556 .setPropertyString( 5557 "subject", 5558 "I like cupcakes", 5559 "I like donuts", 5560 "I like eclairs", 5561 "I like froyo") 5562 .setScore(5) 5563 .build()) 5564 .build())); 5565 5566 // Query for the document 5567 SearchResults searchResults = mDb1.search( 5568 "like", 5569 new SearchSpec.Builder() 5570 .addFilterSchemas("Generic") 5571 .setSnippetCount(2) 5572 .setSnippetCountPerProperty(3) 5573 .setMaxSnippetSize(11) 5574 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_DOCUMENT_SCORE) 5575 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX) 5576 .build()); 5577 5578 // Check result 1 5579 List<SearchResult> results = searchResults.getNextPageAsync().get(); 5580 assertThat(results).hasSize(3); 5581 5582 assertThat(results.get(0).getGenericDocument().getId()).isEqualTo("id2"); 5583 List<SearchResult.MatchInfo> matchInfos = results.get(0).getMatchInfos(); 5584 assertThat(matchInfos).hasSize(3); 5585 assertThat(matchInfos.get(0).getSnippet()).isEqualTo("I like red"); 5586 assertThat(matchInfos.get(1).getSnippet()).isEqualTo("I like"); 5587 assertThat(matchInfos.get(2).getSnippet()).isEqualTo("I like blue"); 5588 5589 // Check result 2 5590 assertThat(results.get(1).getGenericDocument().getId()).isEqualTo("id1"); 5591 matchInfos = results.get(1).getMatchInfos(); 5592 assertThat(matchInfos).hasSize(3); 5593 assertThat(matchInfos.get(0).getSnippet()).isEqualTo("I like cats"); 5594 assertThat(matchInfos.get(1).getSnippet()).isEqualTo("I like dogs"); 5595 assertThat(matchInfos.get(2).getSnippet()).isEqualTo("I like"); 5596 5597 // Check result 2 5598 assertThat(results.get(2).getGenericDocument().getId()).isEqualTo("id3"); 5599 matchInfos = results.get(2).getMatchInfos(); 5600 assertThat(matchInfos).isEmpty(); 5601 } 5602 5603 @Test 5604 @RequiresFlagsEnabled({Flags.FLAG_ENABLE_EMBEDDING_MATCH_INFO, 5605 Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG}) testEmbeddingSnippet()5606 public void testEmbeddingSnippet() throws Exception { 5607 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_EMBEDDING_MATCH_INFO)); 5608 assumeTrue( 5609 mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG)); 5610 // Schema registration 5611 AppSearchSchema schema = new AppSearchSchema.Builder("Email") 5612 .addProperty(new StringPropertyConfig.Builder("body") 5613 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 5614 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 5615 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 5616 .build()) 5617 .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding1") 5618 .setCardinality(PropertyConfig.CARDINALITY_REPEATED) 5619 .setIndexingType( 5620 AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY) 5621 .build()) 5622 .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding2") 5623 .setCardinality(PropertyConfig.CARDINALITY_REPEATED) 5624 .setIndexingType( 5625 AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY) 5626 .build()) 5627 .build(); 5628 mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get(); 5629 5630 // Index documents 5631 EmbeddingVector embedding1Vector = 5632 new EmbeddingVector( 5633 new float[]{0.1f, 0.2f, 0.3f, 0.4f, 0.5f}, 5634 "my_model_v1"); 5635 EmbeddingVector embedding2Vector0 = new EmbeddingVector(new float[]{0.6f, 0.7f, 0.8f}, 5636 "my_model_v2"); 5637 EmbeddingVector embedding2Vector1 = new EmbeddingVector( 5638 new float[]{-0.1f, -0.2f, -0.3f, 0.4f, 0.5f}, 5639 "my_model_v1"); 5640 EmbeddingVector embedding2Vector2 = new EmbeddingVector( 5641 new float[]{-0.1f, -0.2f, -0.3f, 0.4f, 0.7f}, 5642 "my_model_v1"); 5643 GenericDocument doc0 = 5644 new GenericDocument.Builder<>("namespace", "id0", "Email") 5645 .setCreationTimestampMillis(1000) 5646 .setPropertyEmbedding("embedding1", embedding1Vector) 5647 .setPropertyEmbedding("embedding2", embedding2Vector0, embedding2Vector1, 5648 embedding2Vector2) 5649 .build(); 5650 checkIsBatchResultSuccess(mDb1.putAsync( 5651 new PutDocumentsRequest.Builder().addGenericDocuments(doc0).build())); 5652 5653 5654 // Add an embedding search with dot product semantic scores: 5655 // - document 0: -0.5 (embedding1), 0.3 (embedding2[1]), 0.1 (embedding2[2]) 5656 EmbeddingVector searchEmbedding = new EmbeddingVector( 5657 new float[]{1, -1, -1, 1, -1}, "my_model_v1"); 5658 5659 // Match documents that have embeddings with a similarity closer to 0 that is 5660 // greater than -1. 5661 // 5662 // The matched embeddings for each doc are: 5663 // - document 0: -0.5 (embedding1), 0.3 (embedding2) 5664 // The scoring expression for each doc will be evaluated as: 5665 // - document 0: sum({-0.5, 0.3, 0.1}) + sum({}) = -0.1 5666 SearchSpec searchSpec = new SearchSpec.Builder() 5667 .setDefaultEmbeddingSearchMetricType( 5668 SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT) 5669 .addEmbeddingParameters(searchEmbedding) 5670 .setRankingStrategy( 5671 "sum(this.matchedSemanticScores(getEmbeddingParameter(0)))") 5672 .setListFilterQueryLanguageEnabled(true) 5673 .setSnippetCount(1) 5674 .setSnippetCountPerProperty(2) 5675 .setRetrieveEmbeddingMatchInfos(true) 5676 .build(); 5677 5678 // Verify SearchResults 5679 SearchResults searchResults = mDb1.search( 5680 "semanticSearch(getEmbeddingParameter(0), -1, 1)", searchSpec); 5681 List<SearchResult> results = retrieveAllSearchResults(searchResults); 5682 assertThat(results).hasSize(1); 5683 assertThat(results.get(0).getGenericDocument()).isEqualTo(doc0); 5684 assertThat(results.get(0).getRankingSignal()).isWithin(0.00001).of(-0.1); 5685 5686 // Verify MatchInfo 5687 List<SearchResult.MatchInfo> matchInfos = results.get(0).getMatchInfos(); 5688 assertThat(matchInfos).isNotNull(); 5689 assertThat(matchInfos).hasSize(3); 5690 // embedding 1 5691 SearchResult.MatchInfo matchInfo0 = matchInfos.get(0); 5692 assertThat(matchInfo0.getPropertyPath()).isEqualTo("embedding1"); 5693 assertThat(matchInfo0.getTextMatch()).isNull(); 5694 assertThat(matchInfo0.getEmbeddingMatch()).isNotNull(); 5695 assertThat(matchInfo0.getEmbeddingMatch().getSemanticScore()).isWithin(0.00001).of(-0.5); 5696 assertThat(matchInfo0.getEmbeddingMatch().getQueryEmbeddingVectorIndex()).isEqualTo(0); 5697 assertThat(matchInfo0.getEmbeddingMatch().getEmbeddingSearchMetricType()).isEqualTo( 5698 SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT); 5699 // Verify that the property path returns the right embedding vector 5700 EmbeddingVector actualVector = doc0.getPropertyEmbedding(matchInfo0.getPropertyPath()); 5701 assertThat(actualVector).isEqualTo(embedding1Vector); 5702 5703 // embedding 2 vector 1 5704 SearchResult.MatchInfo matchInfo1 = matchInfos.get(1); 5705 assertThat(matchInfo1.getPropertyPath()).isEqualTo("embedding2[1]"); 5706 assertThat(matchInfo1.getTextMatch()).isNull(); 5707 assertThat(matchInfo1.getEmbeddingMatch()).isNotNull(); 5708 assertThat(matchInfo1.getEmbeddingMatch().getSemanticScore()).isWithin(0.00001).of(0.3); 5709 assertThat(matchInfo1.getEmbeddingMatch().getQueryEmbeddingVectorIndex()).isEqualTo(0); 5710 assertThat(matchInfo1.getEmbeddingMatch().getEmbeddingSearchMetricType()).isEqualTo( 5711 SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT); 5712 // Verify that the property path returns the right embedding vector 5713 actualVector = doc0.getPropertyEmbedding(matchInfo1.getPropertyPath()); 5714 assertThat(actualVector).isEqualTo(embedding2Vector1); 5715 5716 // embedding 2 vector 2 5717 SearchResult.MatchInfo matchInfo2 = matchInfos.get(2); 5718 assertThat(matchInfo2.getPropertyPath()).isEqualTo("embedding2[2]"); 5719 assertThat(matchInfo2.getTextMatch()).isNull(); 5720 assertThat(matchInfo2.getEmbeddingMatch()).isNotNull(); 5721 assertThat(matchInfo2.getEmbeddingMatch().getSemanticScore()).isWithin(0.00001).of(0.1); 5722 assertThat(matchInfo2.getEmbeddingMatch().getQueryEmbeddingVectorIndex()).isEqualTo(0); 5723 assertThat(matchInfo2.getEmbeddingMatch().getEmbeddingSearchMetricType()).isEqualTo( 5724 SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT); 5725 // Verify that the property path returns the right embedding vector 5726 actualVector = doc0.getPropertyEmbedding(matchInfo2.getPropertyPath()); 5727 assertThat(actualVector).isEqualTo(embedding2Vector2); 5728 } 5729 5730 @Test 5731 @RequiresFlagsEnabled({Flags.FLAG_ENABLE_EMBEDDING_MATCH_INFO, 5732 Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG}) testHybridSnippet()5733 public void testHybridSnippet() throws Exception { 5734 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_EMBEDDING_MATCH_INFO)); 5735 assumeTrue( 5736 mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG)); 5737 // Schema registration 5738 AppSearchSchema schema = new AppSearchSchema.Builder("Email") 5739 .addProperty(new StringPropertyConfig.Builder("body") 5740 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 5741 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 5742 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 5743 .build()) 5744 .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding1") 5745 .setCardinality(PropertyConfig.CARDINALITY_REPEATED) 5746 .setIndexingType( 5747 AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY) 5748 .build()) 5749 .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding2") 5750 .setCardinality(PropertyConfig.CARDINALITY_REPEATED) 5751 .setIndexingType( 5752 AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY) 5753 .build()) 5754 .build(); 5755 mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get(); 5756 5757 // Index documents 5758 EmbeddingVector embedding1Vector = 5759 new EmbeddingVector( 5760 new float[]{0.1f, 0.2f, 0.3f, 0.4f, 0.5f}, 5761 "my_model_v1"); 5762 EmbeddingVector embedding2Vector0 = new EmbeddingVector(new float[]{0.6f, 0.7f, 0.8f}, 5763 "my_model_v2"); 5764 EmbeddingVector embedding2Vector1 = new EmbeddingVector( 5765 new float[]{-0.1f, -0.2f, -0.3f, 0.4f, 0.5f}, 5766 "my_model_v1"); 5767 EmbeddingVector embedding2Vector2 = new EmbeddingVector( 5768 new float[]{-0.1f, -0.2f, -0.3f, 0.4f, 0.7f}, 5769 "my_model_v1"); 5770 GenericDocument doc0 = 5771 new GenericDocument.Builder<>("namespace", "id0", "Email") 5772 .setPropertyString("body", "A commonly used fake word is foo. " 5773 + "Another nonsense word that’s used a lot is bar") 5774 .setCreationTimestampMillis(1000) 5775 .setPropertyEmbedding("embedding1", embedding1Vector) 5776 .setPropertyEmbedding("embedding2", embedding2Vector0, embedding2Vector1, 5777 embedding2Vector2) 5778 .build(); 5779 checkIsBatchResultSuccess(mDb1.putAsync( 5780 new PutDocumentsRequest.Builder().addGenericDocuments(doc0).build())); 5781 5782 // Add an embedding search with dot product semantic scores: 5783 // - document 0: -0.5 (embedding1), 0.3 (embedding2[1]), 0.1 (embedding2[2]) 5784 EmbeddingVector searchEmbedding = new EmbeddingVector( 5785 new float[]{1, -1, -1, 1, -1}, "my_model_v1"); 5786 5787 // Match documents that have embeddings with a similarity closer to 0 that is 5788 // greater than -1. 5789 // 5790 // The matched embeddings for each doc are: 5791 // - document 0: -0.5 (embedding1), 0.3 (embedding2) 5792 // The scoring expression for each doc will be evaluated as: 5793 // - document 0: sum({-0.5, 0.3, 0.1}) + sum({}) = -0.1 5794 SearchSpec searchSpec = new SearchSpec.Builder() 5795 .setDefaultEmbeddingSearchMetricType( 5796 SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT) 5797 .addEmbeddingParameters(searchEmbedding) 5798 .setRankingStrategy( 5799 "sum(this.matchedSemanticScores(getEmbeddingParameter(0)))") 5800 .setListFilterQueryLanguageEnabled(true) 5801 .setSnippetCount(1) 5802 .setSnippetCountPerProperty(2) 5803 .setMaxSnippetSize(11) 5804 .setRetrieveEmbeddingMatchInfos(true) 5805 .build(); 5806 5807 // Verify SearchResults 5808 SearchResults searchResults = mDb1.search( 5809 "fo OR semanticSearch(getEmbeddingParameter(0), -1, 1)", searchSpec); 5810 List<SearchResult> results = retrieveAllSearchResults(searchResults); 5811 assertThat(results).hasSize(1); 5812 assertThat(results.get(0).getGenericDocument()).isEqualTo(doc0); 5813 5814 // Verify MatchInfo 5815 List<SearchResult.MatchInfo> matchInfos = results.get(0).getMatchInfos(); 5816 assertThat(matchInfos).isNotNull(); 5817 assertThat(matchInfos).hasSize(4); 5818 5819 // body - this is first as snippets are returned by sorted property paths order 5820 SearchResult.MatchInfo matchInfo0 = matchInfos.get(0); 5821 assertThat(matchInfo0.getPropertyPath()).isEqualTo("body"); 5822 assertThat(matchInfo0.getTextMatch()).isNotNull(); 5823 assertThat(matchInfo0.getEmbeddingMatch()).isNull(); 5824 assertThat(matchInfo0.getTextMatch().getFullText()).isEqualTo( 5825 "A commonly used fake word is foo. Another nonsense word that’s used a lot is bar"); 5826 assertThat(matchInfo0.getTextMatch().getExactMatchRange()).isEqualTo( 5827 new SearchResult.MatchRange(/*start=*/29, /*end=*/32)); 5828 assertThat(matchInfo0.getTextMatch().getExactMatch().toString()).isEqualTo("foo"); 5829 assertThat(matchInfo0.getTextMatch().getSnippetRange()).isEqualTo( 5830 new SearchResult.MatchRange(/*start=*/26, /*end=*/33)); 5831 assertThat(matchInfo0.getTextMatch().getSnippet().toString()).isEqualTo("is foo."); 5832 assertThat(matchInfo0.getTextMatch().getSubmatchRange()).isEqualTo( 5833 new SearchResult.MatchRange(/*start=*/29, /*end=*/31)); 5834 assertThat(matchInfo0.getTextMatch().getSubmatch().toString()).isEqualTo("fo"); 5835 5836 // embedding 1 5837 SearchResult.MatchInfo matchInfo1 = matchInfos.get(1); 5838 assertThat(matchInfo1.getPropertyPath()).isEqualTo("embedding1"); 5839 assertThat(matchInfo1.getTextMatch()).isNull(); 5840 assertThat(matchInfo1.getEmbeddingMatch()).isNotNull(); 5841 assertThat(matchInfo1.getEmbeddingMatch().getSemanticScore()).isWithin(0.00001).of(-0.5); 5842 assertThat(matchInfo1.getEmbeddingMatch().getQueryEmbeddingVectorIndex()).isEqualTo(0); 5843 assertThat(matchInfo1.getEmbeddingMatch().getEmbeddingSearchMetricType()).isEqualTo( 5844 SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT); 5845 // Verify that the property path returns the right embedding vector 5846 EmbeddingVector actualVector = doc0.getPropertyEmbedding(matchInfo1.getPropertyPath()); 5847 assertThat(actualVector).isEqualTo(embedding1Vector); 5848 5849 // embedding 2 vector 1 5850 SearchResult.MatchInfo matchInfo2 = matchInfos.get(2); 5851 assertThat(matchInfo2.getPropertyPath()).isEqualTo("embedding2[1]"); 5852 assertThat(matchInfo2.getTextMatch()).isNull(); 5853 assertThat(matchInfo2.getEmbeddingMatch()).isNotNull(); 5854 assertThat(matchInfo2.getEmbeddingMatch().getSemanticScore()).isWithin(0.00001).of(0.3); 5855 assertThat(matchInfo2.getEmbeddingMatch().getQueryEmbeddingVectorIndex()).isEqualTo(0); 5856 assertThat(matchInfo2.getEmbeddingMatch().getEmbeddingSearchMetricType()).isEqualTo( 5857 SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT); 5858 // Verify that the property path returns the right embedding vector 5859 actualVector = doc0.getPropertyEmbedding(matchInfo2.getPropertyPath()); 5860 assertThat(actualVector).isEqualTo(embedding2Vector1); 5861 5862 // embedding 2 vector 2 5863 SearchResult.MatchInfo matchInfo3 = matchInfos.get(3); 5864 assertThat(matchInfo3.getPropertyPath()).isEqualTo("embedding2[2]"); 5865 assertThat(matchInfo3.getTextMatch()).isNull(); 5866 assertThat(matchInfo3.getEmbeddingMatch()).isNotNull(); 5867 assertThat(matchInfo3.getEmbeddingMatch().getSemanticScore()).isWithin(0.00001).of(0.1); 5868 assertThat(matchInfo3.getEmbeddingMatch().getQueryEmbeddingVectorIndex()).isEqualTo(0); 5869 assertThat(matchInfo3.getEmbeddingMatch().getEmbeddingSearchMetricType()).isEqualTo( 5870 SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT); 5871 // Verify that the property path returns the right embedding vector 5872 actualVector = doc0.getPropertyEmbedding(matchInfo3.getPropertyPath()); 5873 assertThat(actualVector).isEqualTo(embedding2Vector2); 5874 } 5875 5876 @Test 5877 @RequiresFlagsEnabled({Flags.FLAG_ENABLE_EMBEDDING_MATCH_INFO, 5878 Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG}) testEmbeddingSnippet_withSnippetCountPerPropertyLimit()5879 public void testEmbeddingSnippet_withSnippetCountPerPropertyLimit() throws Exception { 5880 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_EMBEDDING_MATCH_INFO)); 5881 assumeTrue( 5882 mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG)); 5883 // Schema registration 5884 AppSearchSchema schema = new AppSearchSchema.Builder("Email") 5885 .addProperty(new StringPropertyConfig.Builder("body") 5886 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 5887 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 5888 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 5889 .build()) 5890 .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding1") 5891 .setCardinality(PropertyConfig.CARDINALITY_REPEATED) 5892 .setIndexingType( 5893 AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY) 5894 .build()) 5895 .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding2") 5896 .setCardinality(PropertyConfig.CARDINALITY_REPEATED) 5897 .setIndexingType( 5898 AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY) 5899 .build()) 5900 .build(); 5901 mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get(); 5902 5903 EmbeddingVector embedding1Vector = 5904 new EmbeddingVector( 5905 new float[]{0.1f, 0.2f, 0.3f, 0.4f, 0.5f}, 5906 "my_model_v1"); 5907 EmbeddingVector embedding2Vector0 = new EmbeddingVector(new float[]{0.6f, 0.7f, 0.8f}, 5908 "my_model_v2"); 5909 EmbeddingVector embedding2Vector1 = new EmbeddingVector( 5910 new float[]{-0.1f, -0.2f, -0.3f, 0.4f, 0.5f}, 5911 "my_model_v1"); 5912 EmbeddingVector embedding2Vector2 = new EmbeddingVector( 5913 new float[]{-0.1f, -0.2f, -0.3f, 0.4f, 0.7f}, 5914 "my_model_v1"); 5915 5916 // Index documents 5917 GenericDocument doc0 = 5918 new GenericDocument.Builder<>("namespace", "id0", "Email") 5919 .setCreationTimestampMillis(1000) 5920 .setPropertyEmbedding("embedding1", embedding1Vector) 5921 .setPropertyEmbedding("embedding2", embedding2Vector0, embedding2Vector1, 5922 embedding2Vector2) 5923 .build(); 5924 checkIsBatchResultSuccess(mDb1.putAsync( 5925 new PutDocumentsRequest.Builder().addGenericDocuments(doc0).build())); 5926 5927 // Add an embedding search with dot product semantic scores: 5928 // - document 0: -0.5 (embedding1), 0.3 (embedding2[1]), 0.1(embedding2[2]) 5929 EmbeddingVector searchEmbedding = new EmbeddingVector( 5930 new float[]{1, -1, -1, 1, -1}, "my_model_v1"); 5931 5932 // Match documents that have embeddings with a similarity closer to 0 that is 5933 // greater than -1. 5934 // 5935 // The matched embeddings for each doc are: 5936 // - document 0: -0.5 (embedding1), 0.3 (embedding2[1]), 0.1 (embedding2[2]) 5937 // The scoring expression for each doc will be evaluated as: 5938 // - document 0: sum({-0.5, 0.3, 0.1}) + sum({}) = 0.1 5939 // 5940 // Create a searchSpec where snippets get cut off due to the setSnippetCountPerProperty 5941 // limit 5942 SearchSpec searchSpec = new SearchSpec.Builder() 5943 .setDefaultEmbeddingSearchMetricType( 5944 SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT) 5945 .addEmbeddingParameters(searchEmbedding) 5946 .setRankingStrategy( 5947 "sum(this.matchedSemanticScores(getEmbeddingParameter(0)))") 5948 .setListFilterQueryLanguageEnabled(true) 5949 .setSnippetCount(1) 5950 .setSnippetCountPerProperty(1) 5951 .setRetrieveEmbeddingMatchInfos(true) 5952 .build(); 5953 5954 // Verify SearchResults 5955 SearchResults searchResults = mDb1.search( 5956 "semanticSearch(getEmbeddingParameter(0), -1, 1)", searchSpec); 5957 List<SearchResult> results = retrieveAllSearchResults(searchResults); 5958 assertThat(results).hasSize(1); 5959 assertThat(results.get(0).getGenericDocument()).isEqualTo(doc0); 5960 5961 // Verify MatchInfo 5962 List<SearchResult.MatchInfo> matchInfos = results.get(0).getMatchInfos(); 5963 assertThat(matchInfos).isNotNull(); 5964 // Will only fetch one matchInfo per property 5965 assertThat(matchInfos).hasSize(2); 5966 5967 // embedding 1 5968 SearchResult.MatchInfo matchInfo0 = matchInfos.get(0); 5969 assertThat(matchInfo0.getPropertyPath()).isEqualTo("embedding1"); 5970 assertThat(matchInfo0.getTextMatch()).isNull(); 5971 assertThat(matchInfo0.getEmbeddingMatch()).isNotNull(); 5972 assertThat(matchInfo0.getEmbeddingMatch().getSemanticScore()).isWithin(0.00001).of(-0.5); 5973 assertThat(matchInfo0.getEmbeddingMatch().getQueryEmbeddingVectorIndex()).isEqualTo(0); 5974 assertThat(matchInfo0.getEmbeddingMatch().getEmbeddingSearchMetricType()).isEqualTo( 5975 SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT); 5976 // Verify that the property path returns the right embedding vector 5977 EmbeddingVector actualVector = doc0.getPropertyEmbedding(matchInfo0.getPropertyPath()); 5978 assertThat(actualVector).isEqualTo(embedding1Vector); 5979 5980 // embedding 2 vector 1 5981 SearchResult.MatchInfo matchInfo1 = matchInfos.get(1); 5982 assertThat(matchInfo1.getPropertyPath()).isEqualTo("embedding2[1]"); 5983 assertThat(matchInfo1.getTextMatch()).isNull(); 5984 assertThat(matchInfo1.getEmbeddingMatch()).isNotNull(); 5985 assertThat(matchInfo1.getEmbeddingMatch().getSemanticScore()).isWithin(0.00001).of(0.3); 5986 assertThat(matchInfo1.getEmbeddingMatch().getQueryEmbeddingVectorIndex()).isEqualTo(0); 5987 assertThat(matchInfo1.getEmbeddingMatch().getEmbeddingSearchMetricType()).isEqualTo( 5988 SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT); 5989 // Verify that the property path returns the right embedding vector 5990 actualVector = doc0.getPropertyEmbedding(matchInfo1.getPropertyPath()); 5991 assertThat(actualVector).isEqualTo(embedding2Vector1); 5992 } 5993 5994 @Test testCJKSnippet()5995 public void testCJKSnippet() throws Exception { 5996 // Schema registration 5997 AppSearchSchema genericSchema = new AppSearchSchema.Builder("Generic") 5998 .addProperty(new StringPropertyConfig.Builder("subject") 5999 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 6000 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 6001 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 6002 .build() 6003 ).build(); 6004 mDb1.setSchemaAsync( 6005 new SetSchemaRequest.Builder().addSchemas(genericSchema).build()).get(); 6006 6007 String japanese = 6008 "差し出されたのが今日ランドセルでした普通の子であれば満面の笑みで俺を言うでしょうしかし私は赤いランド" 6009 + "セルを見て笑うことができませんでしたどうしたのと心配そうな仕事ガラスながら渋い顔する私書いたこと言" 6010 + "うんじゃないのカードとなる声を聞きたい私は目から涙をこぼしながらおじいちゃんの近くにかけおり頭をポ" 6011 + "ンポンと叩きピンクが良かったんだもん"; 6012 // Index a document 6013 GenericDocument document = 6014 new GenericDocument.Builder<>("namespace", "id", "Generic") 6015 .setPropertyString("subject", japanese) 6016 .build(); 6017 checkIsBatchResultSuccess(mDb1.putAsync( 6018 new PutDocumentsRequest.Builder().addGenericDocuments(document).build())); 6019 6020 // Query for the document 6021 SearchResults searchResults = mDb1.search("は", 6022 new SearchSpec.Builder() 6023 .addFilterSchemas("Generic") 6024 .setSnippetCount(1) 6025 .setSnippetCountPerProperty(1) 6026 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX) 6027 .build()); 6028 List<SearchResult> results = searchResults.getNextPageAsync().get(); 6029 assertThat(results).hasSize(1); 6030 6031 List<SearchResult.MatchInfo> matchInfos = results.get(0).getMatchInfos(); 6032 assertThat(matchInfos).isNotNull(); 6033 assertThat(matchInfos).hasSize(1); 6034 SearchResult.MatchInfo matchInfo = matchInfos.get(0); 6035 assertThat(matchInfo.getFullText()).isEqualTo(japanese); 6036 assertThat(matchInfo.getExactMatchRange()).isEqualTo( 6037 new SearchResult.MatchRange(/*start=*/44, /*end=*/45)); 6038 assertThat(matchInfo.getExactMatch()).isEqualTo("は"); 6039 6040 if (!mDb1.getFeatures().isFeatureSupported( 6041 Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH)) { 6042 assertThrows(UnsupportedOperationException.class, matchInfo::getSubmatchRange); 6043 assertThrows(UnsupportedOperationException.class, matchInfo::getSubmatch); 6044 } else { 6045 assertThat(matchInfo.getSubmatchRange()).isEqualTo( 6046 new SearchResult.MatchRange(/*start=*/44, /*end=*/45)); 6047 assertThat(matchInfo.getSubmatch()).isEqualTo("は"); 6048 } 6049 } 6050 6051 @Test testRemove()6052 public void testRemove() throws Exception { 6053 // Schema registration 6054 mDb1.setSchemaAsync( 6055 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get(); 6056 6057 // Index documents 6058 AppSearchEmail email1 = 6059 new AppSearchEmail.Builder("namespace", "id1") 6060 .setFrom("from@example.com") 6061 .setTo("to1@example.com", "to2@example.com") 6062 .setSubject("testPut example") 6063 .setBody("This is the body of the testPut email") 6064 .build(); 6065 AppSearchEmail email2 = 6066 new AppSearchEmail.Builder("namespace", "id2") 6067 .setFrom("from@example.com") 6068 .setTo("to1@example.com", "to2@example.com") 6069 .setSubject("testPut example 2") 6070 .setBody("This is the body of the testPut second email") 6071 .build(); 6072 checkIsBatchResultSuccess(mDb1.putAsync( 6073 new PutDocumentsRequest.Builder().addGenericDocuments(email1, email2).build())); 6074 6075 // Check the presence of the documents 6076 assertThat(doGet(mDb1, "namespace", "id1")).hasSize(1); 6077 assertThat(doGet(mDb1, "namespace", "id2")).hasSize(1); 6078 6079 // Delete the document 6080 checkIsBatchResultSuccess(mDb1.removeAsync( 6081 new RemoveByDocumentIdRequest.Builder("namespace").addIds( 6082 "id1").build())); 6083 6084 // Make sure it's really gone 6085 AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentIdAsync( 6086 new GetByDocumentIdRequest.Builder("namespace").addIds("id1", 6087 "id2").build()) 6088 .get(); 6089 assertThat(getResult.isSuccess()).isFalse(); 6090 assertThat(getResult.getFailures().get("id1").getResultCode()) 6091 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND); 6092 assertThat(getResult.getSuccesses().get("id2")).isEqualTo(email2); 6093 6094 // Test if we delete a nonexistent id. 6095 AppSearchBatchResult<String, Void> deleteResult = mDb1.removeAsync( 6096 new RemoveByDocumentIdRequest.Builder("namespace").addIds( 6097 "id1").build()).get(); 6098 6099 assertThat(deleteResult.getFailures().get("id1").getResultCode()).isEqualTo( 6100 AppSearchResult.RESULT_NOT_FOUND); 6101 } 6102 6103 @Test testRemove_multipleIds()6104 public void testRemove_multipleIds() throws Exception { 6105 // Schema registration 6106 mDb1.setSchemaAsync( 6107 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get(); 6108 6109 // Index documents 6110 AppSearchEmail email1 = 6111 new AppSearchEmail.Builder("namespace", "id1") 6112 .setFrom("from@example.com") 6113 .setTo("to1@example.com", "to2@example.com") 6114 .setSubject("testPut example") 6115 .setBody("This is the body of the testPut email") 6116 .build(); 6117 AppSearchEmail email2 = 6118 new AppSearchEmail.Builder("namespace", "id2") 6119 .setFrom("from@example.com") 6120 .setTo("to1@example.com", "to2@example.com") 6121 .setSubject("testPut example 2") 6122 .setBody("This is the body of the testPut second email") 6123 .build(); 6124 checkIsBatchResultSuccess(mDb1.putAsync( 6125 new PutDocumentsRequest.Builder().addGenericDocuments(email1, email2).build())); 6126 6127 // Check the presence of the documents 6128 assertThat(doGet(mDb1, "namespace", "id1")).hasSize(1); 6129 assertThat(doGet(mDb1, "namespace", "id2")).hasSize(1); 6130 6131 // Delete the document 6132 checkIsBatchResultSuccess(mDb1.removeAsync( 6133 new RemoveByDocumentIdRequest.Builder("namespace").addIds("id1", "id2").build())); 6134 6135 // Make sure it's really gone 6136 AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentIdAsync( 6137 new GetByDocumentIdRequest.Builder("namespace").addIds("id1", 6138 "id2").build()) 6139 .get(); 6140 assertThat(getResult.isSuccess()).isFalse(); 6141 assertThat(getResult.getFailures().get("id1").getResultCode()) 6142 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND); 6143 assertThat(getResult.getFailures().get("id2").getResultCode()) 6144 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND); 6145 } 6146 6147 @Test testRemoveByQuery()6148 public void testRemoveByQuery() throws Exception { 6149 // Schema registration 6150 mDb1.setSchemaAsync( 6151 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get(); 6152 6153 // Index documents 6154 AppSearchEmail email1 = 6155 new AppSearchEmail.Builder("namespace", "id1") 6156 .setFrom("from@example.com") 6157 .setTo("to1@example.com", "to2@example.com") 6158 .setSubject("foo") 6159 .setBody("This is the body of the testPut email") 6160 .build(); 6161 AppSearchEmail email2 = 6162 new AppSearchEmail.Builder("namespace", "id2") 6163 .setFrom("from@example.com") 6164 .setTo("to1@example.com", "to2@example.com") 6165 .setSubject("bar") 6166 .setBody("This is the body of the testPut second email") 6167 .build(); 6168 checkIsBatchResultSuccess(mDb1.putAsync( 6169 new PutDocumentsRequest.Builder().addGenericDocuments(email1, email2).build())); 6170 6171 // Check the presence of the documents 6172 assertThat(doGet(mDb1, "namespace", "id1")).hasSize(1); 6173 assertThat(doGet(mDb1, "namespace", "id2")).hasSize(1); 6174 6175 // Delete the email 1 by query "foo" 6176 mDb1.removeAsync("foo", 6177 new SearchSpec.Builder().setTermMatch(SearchSpec.TERM_MATCH_PREFIX).build()).get(); 6178 AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentIdAsync( 6179 new GetByDocumentIdRequest.Builder("namespace") 6180 .addIds("id1", "id2").build()) 6181 .get(); 6182 assertThat(getResult.isSuccess()).isFalse(); 6183 assertThat(getResult.getFailures().get("id1").getResultCode()) 6184 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND); 6185 assertThat(getResult.getSuccesses().get("id2")).isEqualTo(email2); 6186 6187 // Delete the email 2 by query "bar" 6188 mDb1.removeAsync("bar", 6189 new SearchSpec.Builder().setTermMatch(SearchSpec.TERM_MATCH_PREFIX).build()).get(); 6190 getResult = mDb1.getByDocumentIdAsync( 6191 new GetByDocumentIdRequest.Builder("namespace").addIds("id2").build()) 6192 .get(); 6193 assertThat(getResult.isSuccess()).isFalse(); 6194 assertThat(getResult.getFailures().get("id2").getResultCode()) 6195 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND); 6196 } 6197 6198 6199 @Test testRemoveByQuery_nonExistNamespace()6200 public void testRemoveByQuery_nonExistNamespace() throws Exception { 6201 // Schema registration 6202 mDb1.setSchemaAsync( 6203 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get(); 6204 6205 // Index documents 6206 AppSearchEmail email1 = 6207 new AppSearchEmail.Builder("namespace1", "id1") 6208 .setFrom("from@example.com") 6209 .setTo("to1@example.com", "to2@example.com") 6210 .setSubject("foo") 6211 .setBody("This is the body of the testPut email") 6212 .build(); 6213 AppSearchEmail email2 = 6214 new AppSearchEmail.Builder("namespace2", "id2") 6215 .setFrom("from@example.com") 6216 .setTo("to1@example.com", "to2@example.com") 6217 .setSubject("bar") 6218 .setBody("This is the body of the testPut second email") 6219 .build(); 6220 checkIsBatchResultSuccess(mDb1.putAsync( 6221 new PutDocumentsRequest.Builder().addGenericDocuments(email1, email2).build())); 6222 6223 // Check the presence of the documents 6224 assertThat(doGet(mDb1, "namespace1", "id1")).hasSize(1); 6225 assertThat(doGet(mDb1, "namespace2", "id2")).hasSize(1); 6226 6227 // Delete the email by nonExist namespace. 6228 mDb1.removeAsync("", 6229 new SearchSpec.Builder().setTermMatch(SearchSpec.TERM_MATCH_PREFIX) 6230 .addFilterNamespaces("nonExistNamespace").build()).get(); 6231 // None of these emails will be deleted. 6232 assertThat(doGet(mDb1, "namespace1", "id1")).hasSize(1); 6233 assertThat(doGet(mDb1, "namespace2", "id2")).hasSize(1); 6234 } 6235 6236 @Test testRemoveByQuery_packageFilter()6237 public void testRemoveByQuery_packageFilter() throws Exception { 6238 // Schema registration 6239 mDb1.setSchemaAsync( 6240 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get(); 6241 6242 // Index documents 6243 AppSearchEmail email = 6244 new AppSearchEmail.Builder("namespace", "id1") 6245 .setFrom("from@example.com") 6246 .setTo("to1@example.com", "to2@example.com") 6247 .setSubject("foo") 6248 .setBody("This is the body of the testPut email") 6249 .build(); 6250 checkIsBatchResultSuccess(mDb1.putAsync( 6251 new PutDocumentsRequest.Builder().addGenericDocuments(email).build())); 6252 6253 // Check the presence of the documents 6254 assertThat(doGet(mDb1, "namespace", "id1")).hasSize(1); 6255 6256 // Try to delete email with query "foo", but restricted to a different package name. 6257 // Won't work and email will still exist. 6258 mDb1.removeAsync("foo", 6259 new SearchSpec.Builder().setTermMatch( 6260 SearchSpec.TERM_MATCH_PREFIX).addFilterPackageNames( 6261 "some.other.package").build()).get(); 6262 assertThat(doGet(mDb1, "namespace", "id1")).hasSize(1); 6263 6264 // Delete the email by query "foo", restricted to the correct package this time. 6265 mDb1.removeAsync("foo", new SearchSpec.Builder().setTermMatch( 6266 SearchSpec.TERM_MATCH_PREFIX).addFilterPackageNames( 6267 ApplicationProvider.getApplicationContext().getPackageName()).build()).get(); 6268 AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentIdAsync( 6269 new GetByDocumentIdRequest.Builder("namespace").addIds("id1", "id2").build()) 6270 .get(); 6271 assertThat(getResult.isSuccess()).isFalse(); 6272 assertThat(getResult.getFailures().get("id1").getResultCode()) 6273 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND); 6274 } 6275 6276 @Test testRemove_twoInstances()6277 public void testRemove_twoInstances() throws Exception { 6278 // Schema registration 6279 mDb1.setSchemaAsync(new SetSchemaRequest.Builder() 6280 .addSchemas(AppSearchEmail.SCHEMA).build()).get(); 6281 6282 // Index documents 6283 AppSearchEmail email1 = 6284 new AppSearchEmail.Builder("namespace", "id1") 6285 .setFrom("from@example.com") 6286 .setTo("to1@example.com", "to2@example.com") 6287 .setSubject("testPut example") 6288 .setBody("This is the body of the testPut email") 6289 .build(); 6290 checkIsBatchResultSuccess(mDb1.putAsync( 6291 new PutDocumentsRequest.Builder().addGenericDocuments(email1).build())); 6292 6293 // Check the presence of the documents 6294 assertThat(doGet(mDb1, "namespace", "id1")).hasSize(1); 6295 6296 // Can't delete in the other instance. 6297 AppSearchBatchResult<String, Void> deleteResult = mDb2.removeAsync( 6298 new RemoveByDocumentIdRequest.Builder("namespace").addIds("id1").build()).get(); 6299 assertThat(deleteResult.getFailures().get("id1").getResultCode()).isEqualTo( 6300 AppSearchResult.RESULT_NOT_FOUND); 6301 assertThat(doGet(mDb1, "namespace", "id1")).hasSize(1); 6302 6303 // Delete the document 6304 checkIsBatchResultSuccess(mDb1.removeAsync( 6305 new RemoveByDocumentIdRequest.Builder("namespace").addIds("id1").build())); 6306 6307 // Make sure it's really gone 6308 AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentIdAsync( 6309 new GetByDocumentIdRequest.Builder("namespace").addIds("id1").build()).get(); 6310 assertThat(getResult.isSuccess()).isFalse(); 6311 assertThat(getResult.getFailures().get("id1").getResultCode()) 6312 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND); 6313 6314 // Test if we delete a nonexistent id. 6315 deleteResult = mDb1.removeAsync( 6316 new RemoveByDocumentIdRequest.Builder("namespace").addIds("id1").build()).get(); 6317 assertThat(deleteResult.getFailures().get("id1").getResultCode()).isEqualTo( 6318 AppSearchResult.RESULT_NOT_FOUND); 6319 } 6320 6321 @Test testRemoveByTypes()6322 public void testRemoveByTypes() throws Exception { 6323 // Schema registration 6324 AppSearchSchema genericSchema = new AppSearchSchema.Builder("Generic").build(); 6325 mDb1.setSchemaAsync( 6326 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).addSchemas( 6327 genericSchema).build()).get(); 6328 6329 // Index documents 6330 AppSearchEmail email1 = 6331 new AppSearchEmail.Builder("namespace", "id1") 6332 .setFrom("from@example.com") 6333 .setTo("to1@example.com", "to2@example.com") 6334 .setSubject("testPut example") 6335 .setBody("This is the body of the testPut email") 6336 .build(); 6337 AppSearchEmail email2 = 6338 new AppSearchEmail.Builder("namespace", "id2") 6339 .setFrom("from@example.com") 6340 .setTo("to1@example.com", "to2@example.com") 6341 .setSubject("testPut example 2") 6342 .setBody("This is the body of the testPut second email") 6343 .build(); 6344 GenericDocument document1 = 6345 new GenericDocument.Builder<>("namespace", "id3", "Generic").build(); 6346 checkIsBatchResultSuccess(mDb1.putAsync( 6347 new PutDocumentsRequest.Builder().addGenericDocuments(email1, email2, document1) 6348 .build())); 6349 6350 // Check the presence of the documents 6351 assertThat(doGet(mDb1, "namespace", "id1", "id2", "id3")).hasSize(3); 6352 6353 // Delete the email type 6354 mDb1.removeAsync("", new SearchSpec.Builder() 6355 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX) 6356 .addFilterSchemas(AppSearchEmail.SCHEMA_TYPE) 6357 .build()) 6358 .get(); 6359 6360 // Make sure it's really gone 6361 AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentIdAsync( 6362 new GetByDocumentIdRequest.Builder("namespace").addIds("id1", "id2", "id3").build()) 6363 .get(); 6364 assertThat(getResult.isSuccess()).isFalse(); 6365 assertThat(getResult.getFailures().get("id1").getResultCode()) 6366 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND); 6367 assertThat(getResult.getFailures().get("id2").getResultCode()) 6368 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND); 6369 assertThat(getResult.getSuccesses().get("id3")).isEqualTo(document1); 6370 } 6371 6372 @Test testRemoveByTypes_twoInstances()6373 public void testRemoveByTypes_twoInstances() throws Exception { 6374 // Schema registration 6375 mDb1.setSchemaAsync(new SetSchemaRequest.Builder() 6376 .addSchemas(AppSearchEmail.SCHEMA).build()).get(); 6377 mDb2.setSchemaAsync(new SetSchemaRequest.Builder() 6378 .addSchemas(AppSearchEmail.SCHEMA).build()).get(); 6379 6380 // Index documents 6381 AppSearchEmail email1 = 6382 new AppSearchEmail.Builder("namespace", "id1") 6383 .setFrom("from@example.com") 6384 .setTo("to1@example.com", "to2@example.com") 6385 .setSubject("testPut example") 6386 .setBody("This is the body of the testPut email") 6387 .build(); 6388 AppSearchEmail email2 = 6389 new AppSearchEmail.Builder("namespace", "id2") 6390 .setFrom("from@example.com") 6391 .setTo("to1@example.com", "to2@example.com") 6392 .setSubject("testPut example 2") 6393 .setBody("This is the body of the testPut second email") 6394 .build(); 6395 checkIsBatchResultSuccess(mDb1.putAsync( 6396 new PutDocumentsRequest.Builder().addGenericDocuments(email1).build())); 6397 checkIsBatchResultSuccess(mDb2.putAsync( 6398 new PutDocumentsRequest.Builder().addGenericDocuments(email2).build())); 6399 6400 // Check the presence of the documents 6401 assertThat(doGet(mDb1, "namespace", "id1")).hasSize(1); 6402 assertThat(doGet(mDb2, "namespace", "id2")).hasSize(1); 6403 6404 // Delete the email type in instance 1 6405 mDb1.removeAsync("", new SearchSpec.Builder() 6406 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX) 6407 .addFilterSchemas(AppSearchEmail.SCHEMA_TYPE) 6408 .build()) 6409 .get(); 6410 6411 // Make sure it's really gone in instance 1 6412 AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentIdAsync( 6413 new GetByDocumentIdRequest.Builder("namespace").addIds("id1").build()).get(); 6414 assertThat(getResult.isSuccess()).isFalse(); 6415 assertThat(getResult.getFailures().get("id1").getResultCode()) 6416 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND); 6417 6418 // Make sure it's still in instance 2. 6419 getResult = mDb2.getByDocumentIdAsync( 6420 new GetByDocumentIdRequest.Builder("namespace").addIds("id2").build()).get(); 6421 assertThat(getResult.isSuccess()).isTrue(); 6422 assertThat(getResult.getSuccesses().get("id2")).isEqualTo(email2); 6423 } 6424 6425 @Test testRemoveByNamespace()6426 public void testRemoveByNamespace() throws Exception { 6427 // Schema registration 6428 AppSearchSchema genericSchema = new AppSearchSchema.Builder("Generic") 6429 .addProperty(new StringPropertyConfig.Builder("foo") 6430 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 6431 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 6432 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 6433 .build() 6434 ).build(); 6435 mDb1.setSchemaAsync( 6436 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).addSchemas( 6437 genericSchema).build()).get(); 6438 6439 // Index documents 6440 AppSearchEmail email1 = 6441 new AppSearchEmail.Builder("email", "id1") 6442 .setFrom("from@example.com") 6443 .setTo("to1@example.com", "to2@example.com") 6444 .setSubject("testPut example") 6445 .setBody("This is the body of the testPut email") 6446 .build(); 6447 AppSearchEmail email2 = 6448 new AppSearchEmail.Builder("email", "id2") 6449 .setFrom("from@example.com") 6450 .setTo("to1@example.com", "to2@example.com") 6451 .setSubject("testPut example 2") 6452 .setBody("This is the body of the testPut second email") 6453 .build(); 6454 GenericDocument document1 = 6455 new GenericDocument.Builder<>("document", "id3", "Generic") 6456 .setPropertyString("foo", "bar").build(); 6457 checkIsBatchResultSuccess(mDb1.putAsync( 6458 new PutDocumentsRequest.Builder().addGenericDocuments(email1, email2, document1) 6459 .build())); 6460 6461 // Check the presence of the documents 6462 assertThat(doGet(mDb1, /*namespace=*/"email", "id1", "id2")).hasSize(2); 6463 assertThat(doGet(mDb1, /*namespace=*/"document", "id3")).hasSize(1); 6464 6465 // Delete the email namespace 6466 mDb1.removeAsync("", new SearchSpec.Builder() 6467 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX) 6468 .addFilterNamespaces("email") 6469 .build()) 6470 .get(); 6471 6472 // Make sure it's really gone 6473 AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentIdAsync( 6474 new GetByDocumentIdRequest.Builder("email") 6475 .addIds("id1", "id2").build()).get(); 6476 assertThat(getResult.isSuccess()).isFalse(); 6477 assertThat(getResult.getFailures().get("id1").getResultCode()) 6478 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND); 6479 assertThat(getResult.getFailures().get("id2").getResultCode()) 6480 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND); 6481 getResult = mDb1.getByDocumentIdAsync( 6482 new GetByDocumentIdRequest.Builder("document") 6483 .addIds("id3").build()).get(); 6484 assertThat(getResult.isSuccess()).isTrue(); 6485 assertThat(getResult.getSuccesses().get("id3")).isEqualTo(document1); 6486 } 6487 6488 @Test testRemoveByNamespaces_twoInstances()6489 public void testRemoveByNamespaces_twoInstances() throws Exception { 6490 // Schema registration 6491 mDb1.setSchemaAsync(new SetSchemaRequest.Builder() 6492 .addSchemas(AppSearchEmail.SCHEMA).build()).get(); 6493 mDb2.setSchemaAsync(new SetSchemaRequest.Builder() 6494 .addSchemas(AppSearchEmail.SCHEMA).build()).get(); 6495 6496 // Index documents 6497 AppSearchEmail email1 = 6498 new AppSearchEmail.Builder("email", "id1") 6499 .setFrom("from@example.com") 6500 .setTo("to1@example.com", "to2@example.com") 6501 .setSubject("testPut example") 6502 .setBody("This is the body of the testPut email") 6503 .build(); 6504 AppSearchEmail email2 = 6505 new AppSearchEmail.Builder("email", "id2") 6506 .setFrom("from@example.com") 6507 .setTo("to1@example.com", "to2@example.com") 6508 .setSubject("testPut example 2") 6509 .setBody("This is the body of the testPut second email") 6510 .build(); 6511 checkIsBatchResultSuccess(mDb1.putAsync( 6512 new PutDocumentsRequest.Builder().addGenericDocuments(email1).build())); 6513 checkIsBatchResultSuccess(mDb2.putAsync( 6514 new PutDocumentsRequest.Builder().addGenericDocuments(email2).build())); 6515 6516 // Check the presence of the documents 6517 assertThat(doGet(mDb1, /*namespace=*/"email", "id1")).hasSize(1); 6518 assertThat(doGet(mDb2, /*namespace=*/"email", "id2")).hasSize(1); 6519 6520 // Delete the email namespace in instance 1 6521 mDb1.removeAsync("", new SearchSpec.Builder() 6522 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX) 6523 .addFilterNamespaces("email") 6524 .build()) 6525 .get(); 6526 6527 // Make sure it's really gone in instance 1 6528 AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentIdAsync( 6529 new GetByDocumentIdRequest.Builder("email") 6530 .addIds("id1").build()).get(); 6531 assertThat(getResult.isSuccess()).isFalse(); 6532 assertThat(getResult.getFailures().get("id1").getResultCode()) 6533 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND); 6534 6535 // Make sure it's still in instance 2. 6536 getResult = mDb2.getByDocumentIdAsync( 6537 new GetByDocumentIdRequest.Builder("email") 6538 .addIds("id2").build()).get(); 6539 assertThat(getResult.isSuccess()).isTrue(); 6540 assertThat(getResult.getSuccesses().get("id2")).isEqualTo(email2); 6541 } 6542 6543 @Test testRemoveAll_twoInstances()6544 public void testRemoveAll_twoInstances() throws Exception { 6545 // Schema registration 6546 mDb1.setSchemaAsync(new SetSchemaRequest.Builder() 6547 .addSchemas(AppSearchEmail.SCHEMA).build()).get(); 6548 mDb2.setSchemaAsync(new SetSchemaRequest.Builder() 6549 .addSchemas(AppSearchEmail.SCHEMA).build()).get(); 6550 6551 // Index documents 6552 AppSearchEmail email1 = 6553 new AppSearchEmail.Builder("namespace", "id1") 6554 .setFrom("from@example.com") 6555 .setTo("to1@example.com", "to2@example.com") 6556 .setSubject("testPut example") 6557 .setBody("This is the body of the testPut email") 6558 .build(); 6559 AppSearchEmail email2 = 6560 new AppSearchEmail.Builder("namespace", "id2") 6561 .setFrom("from@example.com") 6562 .setTo("to1@example.com", "to2@example.com") 6563 .setSubject("testPut example 2") 6564 .setBody("This is the body of the testPut second email") 6565 .build(); 6566 checkIsBatchResultSuccess(mDb1.putAsync( 6567 new PutDocumentsRequest.Builder().addGenericDocuments(email1).build())); 6568 checkIsBatchResultSuccess(mDb2.putAsync( 6569 new PutDocumentsRequest.Builder().addGenericDocuments(email2).build())); 6570 6571 // Check the presence of the documents 6572 assertThat(doGet(mDb1, "namespace", "id1")).hasSize(1); 6573 assertThat(doGet(mDb2, "namespace", "id2")).hasSize(1); 6574 6575 // Delete the all document in instance 1 6576 mDb1.removeAsync("", new SearchSpec.Builder() 6577 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX) 6578 .build()) 6579 .get(); 6580 6581 // Make sure it's really gone in instance 1 6582 AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentIdAsync( 6583 new GetByDocumentIdRequest.Builder("namespace").addIds("id1").build()).get(); 6584 assertThat(getResult.isSuccess()).isFalse(); 6585 assertThat(getResult.getFailures().get("id1").getResultCode()) 6586 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND); 6587 6588 // Make sure it's still in instance 2. 6589 getResult = mDb2.getByDocumentIdAsync( 6590 new GetByDocumentIdRequest.Builder("namespace").addIds("id2").build()).get(); 6591 assertThat(getResult.isSuccess()).isTrue(); 6592 assertThat(getResult.getSuccesses().get("id2")).isEqualTo(email2); 6593 } 6594 6595 @Test testRemoveAll_termMatchType()6596 public void testRemoveAll_termMatchType() throws Exception { 6597 // Schema registration 6598 mDb1.setSchemaAsync(new SetSchemaRequest.Builder() 6599 .addSchemas(AppSearchEmail.SCHEMA).build()).get(); 6600 mDb2.setSchemaAsync(new SetSchemaRequest.Builder() 6601 .addSchemas(AppSearchEmail.SCHEMA).build()).get(); 6602 6603 // Index documents 6604 AppSearchEmail email1 = 6605 new AppSearchEmail.Builder("namespace", "id1") 6606 .setFrom("from@example.com") 6607 .setTo("to1@example.com", "to2@example.com") 6608 .setSubject("testPut example") 6609 .setBody("This is the body of the testPut email") 6610 .build(); 6611 AppSearchEmail email2 = 6612 new AppSearchEmail.Builder("namespace", "id2") 6613 .setFrom("from@example.com") 6614 .setTo("to1@example.com", "to2@example.com") 6615 .setSubject("testPut example 2") 6616 .setBody("This is the body of the testPut second email") 6617 .build(); 6618 AppSearchEmail email3 = 6619 new AppSearchEmail.Builder("namespace", "id3") 6620 .setFrom("from@example.com") 6621 .setTo("to1@example.com", "to2@example.com") 6622 .setSubject("testPut example 3") 6623 .setBody("This is the body of the testPut second email") 6624 .build(); 6625 AppSearchEmail email4 = 6626 new AppSearchEmail.Builder("namespace", "id4") 6627 .setFrom("from@example.com") 6628 .setTo("to1@example.com", "to2@example.com") 6629 .setSubject("testPut example 4") 6630 .setBody("This is the body of the testPut second email") 6631 .build(); 6632 checkIsBatchResultSuccess(mDb1.putAsync( 6633 new PutDocumentsRequest.Builder().addGenericDocuments(email1, email2).build())); 6634 checkIsBatchResultSuccess(mDb2.putAsync( 6635 new PutDocumentsRequest.Builder().addGenericDocuments(email3, email4).build())); 6636 6637 // Check the presence of the documents 6638 SearchResults searchResults = mDb1.search("", new SearchSpec.Builder() 6639 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 6640 .build()); 6641 List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults); 6642 assertThat(documents).hasSize(2); 6643 searchResults = mDb2.search("", new SearchSpec.Builder() 6644 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 6645 .build()); 6646 documents = convertSearchResultsToDocuments(searchResults); 6647 assertThat(documents).hasSize(2); 6648 6649 // Delete the all document in instance 1 with TERM_MATCH_PREFIX 6650 mDb1.removeAsync("", new SearchSpec.Builder() 6651 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX) 6652 .build()) 6653 .get(); 6654 searchResults = mDb1.search("", new SearchSpec.Builder() 6655 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 6656 .build()); 6657 documents = convertSearchResultsToDocuments(searchResults); 6658 assertThat(documents).isEmpty(); 6659 6660 // Delete the all document in instance 2 with TERM_MATCH_EXACT_ONLY 6661 mDb2.removeAsync("", new SearchSpec.Builder() 6662 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 6663 .build()) 6664 .get(); 6665 searchResults = mDb2.search("", new SearchSpec.Builder() 6666 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 6667 .build()); 6668 documents = convertSearchResultsToDocuments(searchResults); 6669 assertThat(documents).isEmpty(); 6670 } 6671 6672 @Test testRemoveAllAfterEmpty()6673 public void testRemoveAllAfterEmpty() throws Exception { 6674 // Schema registration 6675 mDb1.setSchemaAsync(new SetSchemaRequest.Builder() 6676 .addSchemas(AppSearchEmail.SCHEMA).build()).get(); 6677 6678 // Index documents 6679 AppSearchEmail email1 = 6680 new AppSearchEmail.Builder("namespace", "id1") 6681 .setFrom("from@example.com") 6682 .setTo("to1@example.com", "to2@example.com") 6683 .setSubject("testPut example") 6684 .setBody("This is the body of the testPut email") 6685 .build(); 6686 checkIsBatchResultSuccess(mDb1.putAsync( 6687 new PutDocumentsRequest.Builder().addGenericDocuments(email1).build())); 6688 6689 // Check the presence of the documents 6690 assertThat(doGet(mDb1, "namespace", "id1")).hasSize(1); 6691 6692 // Remove the document 6693 checkIsBatchResultSuccess( 6694 mDb1.removeAsync(new RemoveByDocumentIdRequest.Builder("namespace").addIds( 6695 "id1").build())); 6696 6697 // Make sure it's really gone 6698 AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentIdAsync( 6699 new GetByDocumentIdRequest.Builder("namespace").addIds("id1").build()).get(); 6700 assertThat(getResult.isSuccess()).isFalse(); 6701 assertThat(getResult.getFailures().get("id1").getResultCode()) 6702 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND); 6703 6704 // Delete the all documents 6705 mDb1.removeAsync("", new SearchSpec.Builder() 6706 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX).build()).get(); 6707 6708 // Make sure it's still gone 6709 getResult = mDb1.getByDocumentIdAsync( 6710 new GetByDocumentIdRequest.Builder("namespace").addIds("id1").build()).get(); 6711 assertThat(getResult.isSuccess()).isFalse(); 6712 assertThat(getResult.getFailures().get("id1").getResultCode()) 6713 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND); 6714 } 6715 6716 @Test testRemoveQueryWithJoinSpecThrowsException()6717 public void testRemoveQueryWithJoinSpecThrowsException() { 6718 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID)); 6719 6720 IllegalArgumentException e = assertThrows(IllegalArgumentException.class, 6721 () -> mDb2.removeAsync("", new SearchSpec.Builder() 6722 .setJoinSpec(new JoinSpec.Builder("entityId").build()) 6723 .build())); 6724 assertThat(e.getMessage()).isEqualTo("JoinSpec not allowed in removeByQuery, " 6725 + "but JoinSpec was provided."); 6726 } 6727 6728 @Test testCloseAndReopen()6729 public void testCloseAndReopen() throws Exception { 6730 // Schema registration 6731 mDb1.setSchemaAsync( 6732 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get(); 6733 6734 // Index a document 6735 AppSearchEmail inEmail = 6736 new AppSearchEmail.Builder("namespace", "id1") 6737 .setFrom("from@example.com") 6738 .setTo("to1@example.com", "to2@example.com") 6739 .setSubject("testPut example") 6740 .setBody("This is the body of the testPut email") 6741 .build(); 6742 checkIsBatchResultSuccess(mDb1.putAsync( 6743 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build())); 6744 6745 // close and re-open the appSearchSession 6746 mDb1.close(); 6747 mDb1 = createSearchSessionAsync(DB_NAME_1).get(); 6748 6749 // Query for the document 6750 SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder() 6751 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 6752 .build()); 6753 List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults); 6754 assertThat(documents).containsExactly(inEmail); 6755 } 6756 6757 @Test testCallAfterClose()6758 public void testCallAfterClose() throws Exception { 6759 6760 // Create a same-thread database by inject an executor which could help us maintain the 6761 // execution order of those async tasks. 6762 Context context = ApplicationProvider.getApplicationContext(); 6763 AppSearchSession sameThreadDb = createSearchSessionAsync( 6764 "sameThreadDb", MoreExecutors.newDirectExecutorService()).get(); 6765 6766 try { 6767 // Schema registration -- just mutate something 6768 sameThreadDb.setSchemaAsync( 6769 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get(); 6770 6771 // Close the database. No further call will be allowed. 6772 sameThreadDb.close(); 6773 6774 // Try to query the closed database 6775 // We are using the same-thread db here to make sure it has been closed. 6776 IllegalStateException e = assertThrows(IllegalStateException.class, () -> 6777 sameThreadDb.search("query", new SearchSpec.Builder() 6778 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 6779 .build())); 6780 assertThat(e).hasMessageThat().contains("SearchSession has already been closed"); 6781 } finally { 6782 // To clean the data that has been added in the test, need to re-open the session and 6783 // set an empty schema. 6784 AppSearchSession reopen = createSearchSessionAsync( 6785 "sameThreadDb", MoreExecutors.newDirectExecutorService()).get(); 6786 reopen.setSchemaAsync(new SetSchemaRequest.Builder() 6787 .setForceOverride(true).build()).get(); 6788 } 6789 } 6790 6791 @Test testReportUsage()6792 public void testReportUsage() throws Exception { 6793 mDb1.setSchemaAsync( 6794 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get(); 6795 6796 // Index two documents. 6797 AppSearchEmail email1 = 6798 new AppSearchEmail.Builder("namespace", "id1").build(); 6799 AppSearchEmail email2 = 6800 new AppSearchEmail.Builder("namespace", "id2").build(); 6801 checkIsBatchResultSuccess(mDb1.putAsync( 6802 new PutDocumentsRequest.Builder().addGenericDocuments(email1, email2).build())); 6803 6804 // Email 1 has more usages, but email 2 has more recent usages. 6805 mDb1.reportUsageAsync(new ReportUsageRequest.Builder("namespace", "id1") 6806 .setUsageTimestampMillis(1000).build()).get(); 6807 mDb1.reportUsageAsync(new ReportUsageRequest.Builder("namespace", "id1") 6808 .setUsageTimestampMillis(2000).build()).get(); 6809 mDb1.reportUsageAsync(new ReportUsageRequest.Builder("namespace", "id1") 6810 .setUsageTimestampMillis(3000).build()).get(); 6811 mDb1.reportUsageAsync(new ReportUsageRequest.Builder("namespace", "id2") 6812 .setUsageTimestampMillis(10000).build()).get(); 6813 mDb1.reportUsageAsync(new ReportUsageRequest.Builder("namespace", "id2") 6814 .setUsageTimestampMillis(20000).build()).get(); 6815 6816 // Query by number of usages 6817 List<SearchResult> results = retrieveAllSearchResults( 6818 mDb1.search("", new SearchSpec.Builder() 6819 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_USAGE_COUNT) 6820 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 6821 .build())); 6822 // Email 1 has three usages and email 2 has two usages. 6823 assertThat(results).hasSize(2); 6824 assertThat(results.get(0).getGenericDocument()).isEqualTo(email1); 6825 assertThat(results.get(1).getGenericDocument()).isEqualTo(email2); 6826 assertThat(results.get(0).getRankingSignal()).isEqualTo(3); 6827 assertThat(results.get(1).getRankingSignal()).isEqualTo(2); 6828 6829 // Query by most recent usage. 6830 results = retrieveAllSearchResults( 6831 mDb1.search("", new SearchSpec.Builder() 6832 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP) 6833 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 6834 .build())); 6835 assertThat(results).hasSize(2); 6836 assertThat(results.get(0).getGenericDocument()).isEqualTo(email2); 6837 assertThat(results.get(1).getGenericDocument()).isEqualTo(email1); 6838 assertThat(results.get(0).getRankingSignal()).isEqualTo(20000); 6839 assertThat(results.get(1).getRankingSignal()).isEqualTo(3000); 6840 } 6841 6842 @Test testReportUsage_invalidNamespace()6843 public void testReportUsage_invalidNamespace() throws Exception { 6844 mDb1.setSchemaAsync( 6845 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get(); 6846 AppSearchEmail email1 = new AppSearchEmail.Builder("namespace", "id1").build(); 6847 checkIsBatchResultSuccess(mDb1.putAsync( 6848 new PutDocumentsRequest.Builder().addGenericDocuments(email1).build())); 6849 6850 // Use the correct namespace; it works 6851 mDb1.reportUsageAsync(new ReportUsageRequest.Builder("namespace", "id1").build()).get(); 6852 6853 // Use an incorrect namespace; it fails 6854 ReportUsageRequest reportUsageRequest = 6855 new ReportUsageRequest.Builder("namespace2", "id1").build(); 6856 ExecutionException executionException = assertThrows(ExecutionException.class, 6857 () -> mDb1.reportUsageAsync(reportUsageRequest).get()); 6858 assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class); 6859 AppSearchException cause = (AppSearchException) executionException.getCause(); 6860 assertThat(cause.getResultCode()).isEqualTo(RESULT_NOT_FOUND); 6861 } 6862 6863 @Test testGetStorageInfo()6864 public void testGetStorageInfo() throws Exception { 6865 StorageInfo storageInfo = mDb1.getStorageInfoAsync().get(); 6866 assertThat(storageInfo.getSizeBytes()).isEqualTo(0); 6867 6868 mDb1.setSchemaAsync( 6869 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get(); 6870 6871 // Still no storage space attributed with just a schema 6872 storageInfo = mDb1.getStorageInfoAsync().get(); 6873 assertThat(storageInfo.getSizeBytes()).isEqualTo(0); 6874 6875 // Index two documents. 6876 AppSearchEmail email1 = new AppSearchEmail.Builder("namespace1", "id1").build(); 6877 AppSearchEmail email2 = new AppSearchEmail.Builder("namespace1", "id2").build(); 6878 AppSearchEmail email3 = new AppSearchEmail.Builder("namespace2", "id1").build(); 6879 checkIsBatchResultSuccess(mDb1.putAsync( 6880 new PutDocumentsRequest.Builder().addGenericDocuments(email1, email2, 6881 email3).build())); 6882 6883 // Non-zero size now 6884 storageInfo = mDb1.getStorageInfoAsync().get(); 6885 assertThat(storageInfo.getSizeBytes()).isGreaterThan(0); 6886 assertThat(storageInfo.getAliveDocumentsCount()).isEqualTo(3); 6887 assertThat(storageInfo.getAliveNamespacesCount()).isEqualTo(2); 6888 } 6889 6890 @Test testFlush()6891 public void testFlush() throws Exception { 6892 // Schema registration 6893 mDb1.setSchemaAsync( 6894 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get(); 6895 6896 // Index a document 6897 AppSearchEmail email = new AppSearchEmail.Builder("namespace", "id1") 6898 .setFrom("from@example.com") 6899 .setTo("to1@example.com", "to2@example.com") 6900 .setSubject("testPut example") 6901 .setBody("This is the body of the testPut email") 6902 .build(); 6903 6904 AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb1.putAsync( 6905 new PutDocumentsRequest.Builder().addGenericDocuments(email).build())); 6906 assertThat(result.getSuccesses()).containsExactly("id1", null); 6907 assertThat(result.getFailures()).isEmpty(); 6908 6909 // The future returned from requestFlush will be set as a void or an Exception on error. 6910 mDb1.requestFlushAsync().get(); 6911 } 6912 6913 @Test testQuery_ResultGroupingLimits()6914 public void testQuery_ResultGroupingLimits() throws Exception { 6915 // Schema registration 6916 mDb1.setSchemaAsync(new SetSchemaRequest.Builder() 6917 .addSchemas(AppSearchEmail.SCHEMA).build()).get(); 6918 6919 // Index four documents. 6920 AppSearchEmail inEmail1 = 6921 new AppSearchEmail.Builder("namespace1", "id1") 6922 .setFrom("from@example.com") 6923 .setTo("to1@example.com", "to2@example.com") 6924 .setSubject("testPut example") 6925 .setBody("This is the body of the testPut email") 6926 .build(); 6927 checkIsBatchResultSuccess(mDb1.putAsync( 6928 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build())); 6929 AppSearchEmail inEmail2 = 6930 new AppSearchEmail.Builder("namespace1", "id2") 6931 .setFrom("from@example.com") 6932 .setTo("to1@example.com", "to2@example.com") 6933 .setSubject("testPut example") 6934 .setBody("This is the body of the testPut email") 6935 .build(); 6936 checkIsBatchResultSuccess(mDb1.putAsync( 6937 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail2).build())); 6938 AppSearchEmail inEmail3 = 6939 new AppSearchEmail.Builder("namespace2", "id3") 6940 .setFrom("from@example.com") 6941 .setTo("to1@example.com", "to2@example.com") 6942 .setSubject("testPut example") 6943 .setBody("This is the body of the testPut email") 6944 .build(); 6945 checkIsBatchResultSuccess(mDb1.putAsync( 6946 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail3).build())); 6947 AppSearchEmail inEmail4 = 6948 new AppSearchEmail.Builder("namespace2", "id4") 6949 .setFrom("from@example.com") 6950 .setTo("to1@example.com", "to2@example.com") 6951 .setSubject("testPut example") 6952 .setBody("This is the body of the testPut email") 6953 .build(); 6954 checkIsBatchResultSuccess(mDb1.putAsync( 6955 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail4).build())); 6956 6957 // Query with per package result grouping. Only the last document 'email4' should be 6958 // returned. 6959 SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder() 6960 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 6961 .setResultGrouping(SearchSpec.GROUPING_TYPE_PER_PACKAGE, /*limit=*/ 1) 6962 .build()); 6963 List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults); 6964 assertThat(documents).containsExactly(inEmail4); 6965 6966 // Query with per namespace result grouping. Only the last document in each namespace should 6967 // be returned ('email4' and 'email2'). 6968 searchResults = mDb1.search("body", new SearchSpec.Builder() 6969 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 6970 .setResultGrouping(SearchSpec.GROUPING_TYPE_PER_NAMESPACE, /*limit=*/ 1) 6971 .build()); 6972 documents = convertSearchResultsToDocuments(searchResults); 6973 assertThat(documents).containsExactly(inEmail4, inEmail2); 6974 6975 // Query with per package and per namespace result grouping. Only the last document in each 6976 // namespace should be returned ('email4' and 'email2'). 6977 searchResults = mDb1.search("body", new SearchSpec.Builder() 6978 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 6979 .setResultGrouping(SearchSpec.GROUPING_TYPE_PER_NAMESPACE 6980 | SearchSpec.GROUPING_TYPE_PER_PACKAGE, /*limit=*/ 1) 6981 .build()); 6982 documents = convertSearchResultsToDocuments(searchResults); 6983 assertThat(documents).containsExactly(inEmail4, inEmail2); 6984 } 6985 6986 @Test testQuery_ResultGroupingLimits_SchemaGroupingSupported()6987 public void testQuery_ResultGroupingLimits_SchemaGroupingSupported() throws Exception { 6988 assumeTrue( 6989 mDb1.getFeatures() 6990 .isFeatureSupported(Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA)); 6991 // Schema registration 6992 AppSearchSchema genericSchema = 6993 new AppSearchSchema.Builder("Generic") 6994 .addProperty( 6995 new StringPropertyConfig.Builder("foo") 6996 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 6997 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 6998 .setIndexingType( 6999 StringPropertyConfig.INDEXING_TYPE_PREFIXES) 7000 .build()) 7001 .build(); 7002 mDb1.setSchemaAsync( 7003 new SetSchemaRequest.Builder() 7004 .addSchemas(AppSearchEmail.SCHEMA) 7005 .addSchemas(genericSchema) 7006 .build()) 7007 .get(); 7008 7009 // Index four documents. 7010 AppSearchEmail inEmail1 = 7011 new AppSearchEmail.Builder("namespace1", "id1") 7012 .setFrom("from@example.com") 7013 .setTo("to1@example.com", "to2@example.com") 7014 .setSubject("testPut example") 7015 .setBody("This is the body of the testPut email") 7016 .build(); 7017 checkIsBatchResultSuccess( 7018 mDb1.putAsync( 7019 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build())); 7020 AppSearchEmail inEmail2 = 7021 new AppSearchEmail.Builder("namespace1", "id2") 7022 .setFrom("from@example.com") 7023 .setTo("to1@example.com", "to2@example.com") 7024 .setSubject("testPut example") 7025 .setBody("This is the body of the testPut email") 7026 .build(); 7027 checkIsBatchResultSuccess( 7028 mDb1.putAsync( 7029 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail2).build())); 7030 AppSearchEmail inEmail3 = 7031 new AppSearchEmail.Builder("namespace2", "id3") 7032 .setFrom("from@example.com") 7033 .setTo("to1@example.com", "to2@example.com") 7034 .setSubject("testPut example") 7035 .setBody("This is the body of the testPut email") 7036 .build(); 7037 checkIsBatchResultSuccess( 7038 mDb1.putAsync( 7039 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail3).build())); 7040 AppSearchEmail inEmail4 = 7041 new AppSearchEmail.Builder("namespace2", "id4") 7042 .setFrom("from@example.com") 7043 .setTo("to1@example.com", "to2@example.com") 7044 .setSubject("testPut example") 7045 .setBody("This is the body of the testPut email") 7046 .build(); 7047 checkIsBatchResultSuccess( 7048 mDb1.putAsync( 7049 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail4).build())); 7050 AppSearchEmail inEmail5 = 7051 new AppSearchEmail.Builder("namespace2", "id5") 7052 .setFrom("from@example.com") 7053 .setTo("to1@example.com", "to2@example.com") 7054 .setSubject("testPut example") 7055 .setBody("This is the body of the testPut email") 7056 .build(); 7057 checkIsBatchResultSuccess( 7058 mDb1.putAsync( 7059 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail5).build())); 7060 GenericDocument inDoc1 = 7061 new GenericDocument.Builder<>("namespace3", "id6", "Generic") 7062 .setPropertyString("foo", "body") 7063 .build(); 7064 checkIsBatchResultSuccess( 7065 mDb1.putAsync( 7066 new PutDocumentsRequest.Builder().addGenericDocuments(inDoc1).build())); 7067 GenericDocument inDoc2 = 7068 new GenericDocument.Builder<>("namespace3", "id7", "Generic") 7069 .setPropertyString("foo", "body") 7070 .build(); 7071 checkIsBatchResultSuccess( 7072 mDb1.putAsync( 7073 new PutDocumentsRequest.Builder().addGenericDocuments(inDoc2).build())); 7074 GenericDocument inDoc3 = 7075 new GenericDocument.Builder<>("namespace4", "id8", "Generic") 7076 .setPropertyString("foo", "body") 7077 .build(); 7078 checkIsBatchResultSuccess( 7079 mDb1.putAsync( 7080 new PutDocumentsRequest.Builder().addGenericDocuments(inDoc3).build())); 7081 7082 // Query with per package result grouping. Only the last document 'doc3' should be 7083 // returned. 7084 SearchResults searchResults = 7085 mDb1.search( 7086 "body", 7087 new SearchSpec.Builder() 7088 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 7089 .setResultGrouping( 7090 SearchSpec.GROUPING_TYPE_PER_PACKAGE, /* limit= */ 1) 7091 .build()); 7092 List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults); 7093 assertThat(documents).containsExactly(inDoc3); 7094 7095 // Query with per namespace result grouping. Only the last document in each namespace should 7096 // be returned ('doc3', 'doc2', 'email5' and 'email2'). 7097 searchResults = 7098 mDb1.search( 7099 "body", 7100 new SearchSpec.Builder() 7101 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 7102 .setResultGrouping( 7103 SearchSpec.GROUPING_TYPE_PER_NAMESPACE, 7104 /* limit= */ 1) 7105 .build()); 7106 documents = convertSearchResultsToDocuments(searchResults); 7107 assertThat(documents).containsExactly(inDoc3, inDoc2, inEmail5, inEmail2); 7108 7109 // Query with per namespace result grouping. Two of the last documents in each namespace 7110 // should be returned ('doc3', 'doc2', 'doc1', 'email5', 'email4', 'email2', 'email1') 7111 searchResults = 7112 mDb1.search( 7113 "body", 7114 new SearchSpec.Builder() 7115 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 7116 .setResultGrouping( 7117 SearchSpec.GROUPING_TYPE_PER_NAMESPACE, 7118 /* limit= */ 2) 7119 .build()); 7120 documents = convertSearchResultsToDocuments(searchResults); 7121 assertThat(documents) 7122 .containsExactly(inDoc3, inDoc2, inDoc1, inEmail5, inEmail4, inEmail2, inEmail1); 7123 7124 // Query with per schema result grouping. Only the last document of each schema type should 7125 // be returned ('doc3', 'email5') 7126 searchResults = 7127 mDb1.search( 7128 "body", 7129 new SearchSpec.Builder() 7130 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 7131 .setResultGrouping( 7132 SearchSpec.GROUPING_TYPE_PER_SCHEMA, /* limit= */ 1) 7133 .build()); 7134 documents = convertSearchResultsToDocuments(searchResults); 7135 assertThat(documents).containsExactly(inDoc3, inEmail5); 7136 7137 // Query with per schema result grouping. Only the last two documents of each schema type 7138 // should be returned ('doc3', 'doc2', 'email5', 'email4') 7139 searchResults = 7140 mDb1.search( 7141 "body", 7142 new SearchSpec.Builder() 7143 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 7144 .setResultGrouping( 7145 SearchSpec.GROUPING_TYPE_PER_SCHEMA, /* limit= */ 2) 7146 .build()); 7147 documents = convertSearchResultsToDocuments(searchResults); 7148 assertThat(documents).containsExactly(inDoc3, inDoc2, inEmail5, inEmail4); 7149 7150 // Query with per package and per namespace result grouping. Only the last document in each 7151 // namespace should be returned ('doc3', 'doc2', 'email5' and 'email2'). 7152 searchResults = 7153 mDb1.search( 7154 "body", 7155 new SearchSpec.Builder() 7156 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 7157 .setResultGrouping( 7158 SearchSpec.GROUPING_TYPE_PER_NAMESPACE 7159 | SearchSpec.GROUPING_TYPE_PER_PACKAGE, 7160 /* limit= */ 1) 7161 .build()); 7162 documents = convertSearchResultsToDocuments(searchResults); 7163 assertThat(documents).containsExactly(inDoc3, inDoc2, inEmail5, inEmail2); 7164 7165 // Query with per package and per namespace result grouping. Only the last two documents 7166 // in each namespace should be returned ('doc3', 'doc2', 'doc1', 'email5', 'email4', 7167 // 'email2', 'email1') 7168 searchResults = 7169 mDb1.search( 7170 "body", 7171 new SearchSpec.Builder() 7172 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 7173 .setResultGrouping( 7174 SearchSpec.GROUPING_TYPE_PER_NAMESPACE 7175 | SearchSpec.GROUPING_TYPE_PER_PACKAGE, 7176 /* limit= */ 2) 7177 .build()); 7178 documents = convertSearchResultsToDocuments(searchResults); 7179 assertThat(documents) 7180 .containsExactly(inDoc3, inDoc2, inDoc1, inEmail5, inEmail4, inEmail2, inEmail1); 7181 7182 // Query with per package and per schema type result grouping. Only the last document in 7183 // each schema type should be returned. ('doc3', 'email5') 7184 searchResults = 7185 mDb1.search( 7186 "body", 7187 new SearchSpec.Builder() 7188 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 7189 .setResultGrouping( 7190 SearchSpec.GROUPING_TYPE_PER_SCHEMA 7191 | SearchSpec.GROUPING_TYPE_PER_PACKAGE, 7192 /* limit= */ 1) 7193 .build()); 7194 documents = convertSearchResultsToDocuments(searchResults); 7195 assertThat(documents).containsExactly(inDoc3, inEmail5); 7196 7197 // Query with per package and per schema type result grouping. Only the last two document in 7198 // each schema type should be returned. ('doc3', 'doc2', 'email5', 'email4') 7199 searchResults = 7200 mDb1.search( 7201 "body", 7202 new SearchSpec.Builder() 7203 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 7204 .setResultGrouping( 7205 SearchSpec.GROUPING_TYPE_PER_SCHEMA 7206 | SearchSpec.GROUPING_TYPE_PER_PACKAGE, 7207 /* limit= */ 2) 7208 .build()); 7209 documents = convertSearchResultsToDocuments(searchResults); 7210 assertThat(documents).containsExactly(inDoc3, inDoc2, inEmail5, inEmail4); 7211 7212 // Query with per namespace and per schema type result grouping. Only the last document in 7213 // each namespace should be returned. ('doc3', 'doc2', 'email5' and 'email2'). 7214 searchResults = 7215 mDb1.search( 7216 "body", 7217 new SearchSpec.Builder() 7218 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 7219 .setResultGrouping( 7220 SearchSpec.GROUPING_TYPE_PER_NAMESPACE 7221 | SearchSpec.GROUPING_TYPE_PER_SCHEMA, 7222 /* limit= */ 1) 7223 .build()); 7224 documents = convertSearchResultsToDocuments(searchResults); 7225 assertThat(documents).containsExactly(inDoc3, inDoc2, inEmail5, inEmail2); 7226 7227 // Query with per namespace and per schema type result grouping. Only the last two documents 7228 // in each namespace should be returned. ('doc3', 'doc2', 'doc1', 'email5', 'email4', 7229 // 'email2', 'email1') 7230 searchResults = 7231 mDb1.search( 7232 "body", 7233 new SearchSpec.Builder() 7234 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 7235 .setResultGrouping( 7236 SearchSpec.GROUPING_TYPE_PER_NAMESPACE 7237 | SearchSpec.GROUPING_TYPE_PER_SCHEMA, 7238 /* limit= */ 2) 7239 .build()); 7240 documents = convertSearchResultsToDocuments(searchResults); 7241 assertThat(documents) 7242 .containsExactly(inDoc3, inDoc2, inDoc1, inEmail5, inEmail4, inEmail2, inEmail1); 7243 7244 // Query with per namespace, per package and per schema type result grouping. Only the last 7245 // document in each namespace should be returned. ('doc3', 'doc2', 'email5' and 'email2') 7246 searchResults = 7247 mDb1.search( 7248 "body", 7249 new SearchSpec.Builder() 7250 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 7251 .setResultGrouping( 7252 SearchSpec.GROUPING_TYPE_PER_NAMESPACE 7253 | SearchSpec.GROUPING_TYPE_PER_SCHEMA 7254 | SearchSpec.GROUPING_TYPE_PER_PACKAGE, 7255 /* limit= */ 1) 7256 .build()); 7257 documents = convertSearchResultsToDocuments(searchResults); 7258 assertThat(documents).containsExactly(inDoc3, inDoc2, inEmail5, inEmail2); 7259 7260 // Query with per namespace, per package and per schema type result grouping. Only the last 7261 // two documents in each namespace should be returned.('doc3', 'doc2', 'doc1', 'email5', 7262 // 'email4', 'email2', 'email1') 7263 searchResults = 7264 mDb1.search( 7265 "body", 7266 new SearchSpec.Builder() 7267 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 7268 .setResultGrouping( 7269 SearchSpec.GROUPING_TYPE_PER_NAMESPACE 7270 | SearchSpec.GROUPING_TYPE_PER_SCHEMA 7271 | SearchSpec.GROUPING_TYPE_PER_PACKAGE, 7272 /* limit= */ 2) 7273 .build()); 7274 documents = convertSearchResultsToDocuments(searchResults); 7275 assertThat(documents) 7276 .containsExactly(inDoc3, inDoc2, inDoc1, inEmail5, inEmail4, inEmail2, inEmail1); 7277 } 7278 7279 @Test testQuery_ResultGroupingLimits_SchemaGroupingNotSupported()7280 public void testQuery_ResultGroupingLimits_SchemaGroupingNotSupported() throws Exception { 7281 assumeFalse( 7282 mDb1.getFeatures() 7283 .isFeatureSupported(Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA)); 7284 // Schema registration 7285 mDb1.setSchemaAsync( 7286 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()) 7287 .get(); 7288 7289 // Index four documents. 7290 AppSearchEmail inEmail1 = 7291 new AppSearchEmail.Builder("namespace1", "id1") 7292 .setFrom("from@example.com") 7293 .setTo("to1@example.com", "to2@example.com") 7294 .setSubject("testPut example") 7295 .setBody("This is the body of the testPut email") 7296 .build(); 7297 checkIsBatchResultSuccess( 7298 mDb1.putAsync( 7299 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build())); 7300 AppSearchEmail inEmail2 = 7301 new AppSearchEmail.Builder("namespace1", "id2") 7302 .setFrom("from@example.com") 7303 .setTo("to1@example.com", "to2@example.com") 7304 .setSubject("testPut example") 7305 .setBody("This is the body of the testPut email") 7306 .build(); 7307 checkIsBatchResultSuccess( 7308 mDb1.putAsync( 7309 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail2).build())); 7310 AppSearchEmail inEmail3 = 7311 new AppSearchEmail.Builder("namespace2", "id3") 7312 .setFrom("from@example.com") 7313 .setTo("to1@example.com", "to2@example.com") 7314 .setSubject("testPut example") 7315 .setBody("This is the body of the testPut email") 7316 .build(); 7317 checkIsBatchResultSuccess( 7318 mDb1.putAsync( 7319 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail3).build())); 7320 AppSearchEmail inEmail4 = 7321 new AppSearchEmail.Builder("namespace2", "id4") 7322 .setFrom("from@example.com") 7323 .setTo("to1@example.com", "to2@example.com") 7324 .setSubject("testPut example") 7325 .setBody("This is the body of the testPut email") 7326 .build(); 7327 checkIsBatchResultSuccess( 7328 mDb1.putAsync( 7329 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail4).build())); 7330 7331 // SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA is not supported. 7332 // UnsupportedOperationException will be thrown. 7333 SearchSpec searchSpec1 = 7334 new SearchSpec.Builder() 7335 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 7336 .setResultGrouping( 7337 SearchSpec.GROUPING_TYPE_PER_SCHEMA, /* limit= */ 1) 7338 .build(); 7339 UnsupportedOperationException exception = 7340 assertThrows( 7341 UnsupportedOperationException.class, 7342 () -> mDb1.search("body", searchSpec1)); 7343 assertThat(exception) 7344 .hasMessageThat() 7345 .contains( 7346 Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA 7347 + " is not available on this" 7348 + " AppSearch implementation."); 7349 7350 // SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA is not supported. 7351 // UnsupportedOperationException will be thrown. 7352 SearchSpec searchSpec2 = 7353 new SearchSpec.Builder() 7354 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 7355 .setResultGrouping( 7356 SearchSpec.GROUPING_TYPE_PER_PACKAGE 7357 | SearchSpec.GROUPING_TYPE_PER_SCHEMA, 7358 /* limit= */ 1) 7359 .build(); 7360 exception = 7361 assertThrows( 7362 UnsupportedOperationException.class, 7363 () -> mDb1.search("body", searchSpec2)); 7364 assertThat(exception) 7365 .hasMessageThat() 7366 .contains( 7367 Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA 7368 + " is not available on this" 7369 + " AppSearch implementation."); 7370 7371 // SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA is not supported. 7372 // UnsupportedOperationException will be thrown. 7373 SearchSpec searchSpec3 = 7374 new SearchSpec.Builder() 7375 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 7376 .setResultGrouping( 7377 SearchSpec.GROUPING_TYPE_PER_NAMESPACE 7378 | SearchSpec.GROUPING_TYPE_PER_SCHEMA, 7379 /* limit= */ 1) 7380 .build(); 7381 exception = 7382 assertThrows( 7383 UnsupportedOperationException.class, 7384 () -> mDb1.search("body", searchSpec3)); 7385 assertThat(exception) 7386 .hasMessageThat() 7387 .contains( 7388 Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA 7389 + " is not available on this" 7390 + " AppSearch implementation."); 7391 7392 // SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA is not supported. 7393 // UnsupportedOperationException will be thrown. 7394 SearchSpec searchSpec4 = 7395 new SearchSpec.Builder() 7396 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 7397 .setResultGrouping( 7398 SearchSpec.GROUPING_TYPE_PER_NAMESPACE 7399 | SearchSpec.GROUPING_TYPE_PER_SCHEMA 7400 | SearchSpec.GROUPING_TYPE_PER_PACKAGE, 7401 /* limit= */ 1) 7402 .build(); 7403 exception = 7404 assertThrows( 7405 UnsupportedOperationException.class, 7406 () -> mDb1.search("body", searchSpec4)); 7407 assertThat(exception) 7408 .hasMessageThat() 7409 .contains( 7410 Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA 7411 + " is not available on this" 7412 + " AppSearch implementation."); 7413 } 7414 7415 @Test testIndexNestedDocuments()7416 public void testIndexNestedDocuments() throws Exception { 7417 // Schema registration 7418 mDb1.setSchemaAsync(new SetSchemaRequest.Builder() 7419 .addSchemas(AppSearchEmail.SCHEMA) 7420 .addSchemas(new AppSearchSchema.Builder("YesNestedIndex") 7421 .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder( 7422 "prop", AppSearchEmail.SCHEMA_TYPE) 7423 .setShouldIndexNestedProperties(true) 7424 .build()) 7425 .build()) 7426 .addSchemas(new AppSearchSchema.Builder("NoNestedIndex") 7427 .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder( 7428 "prop", AppSearchEmail.SCHEMA_TYPE) 7429 .setShouldIndexNestedProperties(false) 7430 .build()) 7431 .build()) 7432 .build()) 7433 .get(); 7434 7435 // Index the documents. 7436 AppSearchEmail email = new AppSearchEmail.Builder("", "") 7437 .setSubject("This is the body") 7438 .build(); 7439 GenericDocument yesNestedIndex = 7440 new GenericDocument.Builder<>("namespace", "yesNestedIndex", "YesNestedIndex") 7441 .setPropertyDocument("prop", email) 7442 .build(); 7443 GenericDocument noNestedIndex = 7444 new GenericDocument.Builder<>("namespace", "noNestedIndex", "NoNestedIndex") 7445 .setPropertyDocument("prop", email) 7446 .build(); 7447 checkIsBatchResultSuccess(mDb1.putAsync(new PutDocumentsRequest.Builder() 7448 .addGenericDocuments(yesNestedIndex, noNestedIndex).build())); 7449 7450 // Query. 7451 SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder() 7452 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 7453 .setSnippetCount(10) 7454 .setSnippetCountPerProperty(10) 7455 .build()); 7456 List<SearchResult> page = searchResults.getNextPageAsync().get(); 7457 assertThat(page).hasSize(1); 7458 assertThat(page.get(0).getGenericDocument()).isEqualTo(yesNestedIndex); 7459 List<SearchResult.MatchInfo> matches = page.get(0).getMatchInfos(); 7460 assertThat(matches).hasSize(1); 7461 assertThat(matches.get(0).getPropertyPath()).isEqualTo("prop.subject"); 7462 assertThat(matches.get(0).getPropertyPathObject()) 7463 .isEqualTo(new PropertyPath("prop.subject")); 7464 assertThat(matches.get(0).getFullText()).isEqualTo("This is the body"); 7465 assertThat(matches.get(0).getExactMatch()).isEqualTo("body"); 7466 } 7467 7468 @Test testCJKTQuery()7469 public void testCJKTQuery() throws Exception { 7470 // Schema registration 7471 mDb1.setSchemaAsync(new SetSchemaRequest.Builder() 7472 .addSchemas(AppSearchEmail.SCHEMA).build()).get(); 7473 7474 // Index a document to instance 1. 7475 AppSearchEmail inEmail1 = 7476 new AppSearchEmail.Builder("namespace", "uri1") 7477 .setBody("他是個男孩 is a boy") 7478 .build(); 7479 checkIsBatchResultSuccess(mDb1.putAsync( 7480 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build())); 7481 7482 // Query for "他" (He) 7483 SearchResults searchResults = mDb1.search("他", new SearchSpec.Builder() 7484 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX) 7485 .build()); 7486 List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults); 7487 assertThat(documents).containsExactly(inEmail1); 7488 7489 // Query for "男孩" (boy) 7490 searchResults = mDb1.search("男孩", new SearchSpec.Builder() 7491 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX) 7492 .build()); 7493 documents = convertSearchResultsToDocuments(searchResults); 7494 assertThat(documents).containsExactly(inEmail1); 7495 7496 // Query for "boy" 7497 searchResults = mDb1.search("boy", new SearchSpec.Builder() 7498 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX) 7499 .build()); 7500 documents = convertSearchResultsToDocuments(searchResults); 7501 assertThat(documents).containsExactly(inEmail1); 7502 } 7503 7504 @Test testSetSchemaWithIncompatibleNestedSchema()7505 public void testSetSchemaWithIncompatibleNestedSchema() throws Exception { 7506 // 1. Set the original schema. This should succeed without any problems. 7507 AppSearchSchema originalNestedSchema = 7508 new AppSearchSchema.Builder("TypeA").addProperty(new StringPropertyConfig.Builder( 7509 "prop1").setCardinality( 7510 PropertyConfig.CARDINALITY_OPTIONAL).build()).build(); 7511 SetSchemaRequest originalRequest = 7512 new SetSchemaRequest.Builder().addSchemas(originalNestedSchema).build(); 7513 mDb1.setSchemaAsync(originalRequest).get(); 7514 7515 // 2. Set a new schema with a new type that refers to "TypeA" and an incompatible change to 7516 // "TypeA". This should fail. 7517 AppSearchSchema newNestedSchema = 7518 new AppSearchSchema.Builder("TypeA").addProperty(new StringPropertyConfig.Builder( 7519 "prop1").setCardinality( 7520 PropertyConfig.CARDINALITY_REQUIRED).build()).build(); 7521 AppSearchSchema newSchema = 7522 new AppSearchSchema.Builder("TypeB").addProperty( 7523 new AppSearchSchema.DocumentPropertyConfig.Builder("prop2", 7524 "TypeA").build()).build(); 7525 final SetSchemaRequest newRequest = 7526 new SetSchemaRequest.Builder().addSchemas(newNestedSchema, 7527 newSchema).build(); 7528 ExecutionException executionException = assertThrows(ExecutionException.class, 7529 () -> mDb1.setSchemaAsync(newRequest).get()); 7530 assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class); 7531 AppSearchException exception = (AppSearchException) executionException.getCause(); 7532 assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_SCHEMA); 7533 assertThat(exception).hasMessageThat().contains("Schema is incompatible."); 7534 assertThat(exception).hasMessageThat().contains("Incompatible types: {TypeA}"); 7535 7536 // 3. Now set that same set of schemas but with forceOverride=true. This should succeed. 7537 SetSchemaRequest newRequestForced = 7538 new SetSchemaRequest.Builder().addSchemas(newNestedSchema, 7539 newSchema).setForceOverride(true).build(); 7540 mDb1.setSchemaAsync(newRequestForced).get(); 7541 } 7542 7543 @Test testEmojiSnippet()7544 public void testEmojiSnippet() throws Exception { 7545 // Schema registration 7546 mDb1.setSchemaAsync(new SetSchemaRequest.Builder() 7547 .addSchemas(AppSearchEmail.SCHEMA).build()).get(); 7548 7549 // String: "Luca Brasi sleeps with the ." 7550 // ^ ^ ^ ^ ^ ^ ^ ^ ^ 7551 // UTF8 idx: 0 5 11 18 23 27 3135 39 7552 // UTF16 idx: 0 5 11 18 23 27 2931 33 7553 // Breaks into segments: "Luca", "Brasi", "sleeps", "with", "the", "", "" 7554 // and "". 7555 // Index a document to instance 1. 7556 String sicilianMessage = "Luca Brasi sleeps with the ."; 7557 AppSearchEmail inEmail1 = 7558 new AppSearchEmail.Builder("namespace", "uri1") 7559 .setBody(sicilianMessage) 7560 .build(); 7561 checkIsBatchResultSuccess(mDb1.putAsync( 7562 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build())); 7563 7564 AppSearchEmail inEmail2 = 7565 new AppSearchEmail.Builder("namespace", "uri2") 7566 .setBody("Some other content.") 7567 .build(); 7568 checkIsBatchResultSuccess(mDb1.putAsync( 7569 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail2).build())); 7570 7571 // Query for "" 7572 SearchResults searchResults = mDb1.search("", new SearchSpec.Builder() 7573 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX) 7574 .setSnippetCount(1) 7575 .setSnippetCountPerProperty(1) 7576 .build()); 7577 List<SearchResult> page = searchResults.getNextPageAsync().get(); 7578 assertThat(page).hasSize(1); 7579 assertThat(page.get(0).getGenericDocument()).isEqualTo(inEmail1); 7580 List<SearchResult.MatchInfo> matches = page.get(0).getMatchInfos(); 7581 assertThat(matches).hasSize(1); 7582 assertThat(matches.get(0).getPropertyPath()).isEqualTo("body"); 7583 assertThat(matches.get(0).getFullText()).isEqualTo(sicilianMessage); 7584 assertThat(matches.get(0).getExactMatch()).isEqualTo(""); 7585 if (mDb1.getFeatures().isFeatureSupported(Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH)) { 7586 assertThat(matches.get(0).getSubmatch()).isEqualTo(""); 7587 } 7588 } 7589 7590 @Test testRfc822()7591 public void testRfc822() throws Exception { 7592 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.TOKENIZER_TYPE_RFC822)); 7593 AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email") 7594 .addProperty(new StringPropertyConfig.Builder("address") 7595 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 7596 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 7597 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_RFC822) 7598 .build() 7599 ).build(); 7600 mDb1.setSchemaAsync(new SetSchemaRequest.Builder() 7601 .setForceOverride(true).addSchemas(emailSchema).build()).get(); 7602 7603 GenericDocument email = new GenericDocument.Builder<>("NS", "alex1", "Email") 7604 .setPropertyString("address", "Alex Saveliev <alex.sav@google.com>") 7605 .build(); 7606 mDb1.putAsync(new PutDocumentsRequest.Builder().addGenericDocuments(email).build()).get(); 7607 7608 SearchResults sr = mDb1.search("com", new SearchSpec.Builder().build()); 7609 List<SearchResult> page = sr.getNextPageAsync().get(); 7610 7611 // RFC tokenization will produce the following tokens for 7612 // "Alex Saveliev <alex.sav@google.com>" : ["Alex Saveliev <alex.sav@google.com>", "Alex", 7613 // "Saveliev", "alex.sav", "alex.sav@google.com", "alex.sav", "google", "com"]. Therefore, 7614 // a query for "com" should match the document. 7615 assertThat(page).hasSize(1); 7616 assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("alex1"); 7617 7618 // Plain tokenizer will not match this 7619 AppSearchSchema plainEmailSchema = new AppSearchSchema.Builder("Email") 7620 .addProperty(new StringPropertyConfig.Builder("address") 7621 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 7622 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 7623 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 7624 .build() 7625 ).build(); 7626 7627 // Flipping the tokenizer type is a backwards compatible change. The index will be 7628 // rebuilt with the email doc being tokenized in the new way. 7629 mDb1.setSchemaAsync( 7630 new SetSchemaRequest.Builder().addSchemas(plainEmailSchema).build()).get(); 7631 7632 sr = mDb1.search("com", new SearchSpec.Builder().build()); 7633 7634 // Plain tokenization will produce the following tokens for 7635 // "Alex Saveliev <alex.sav@google.com>" : ["Alex", "Saveliev", "<", "alex.sav", 7636 // "google.com", ">"]. So "com" will not match any of the tokens produced. 7637 assertThat(sr.getNextPageAsync().get()).hasSize(0); 7638 } 7639 7640 @Test testRfc822_unsupportedFeature_throwsException()7641 public void testRfc822_unsupportedFeature_throwsException() { 7642 assumeFalse(mDb1.getFeatures().isFeatureSupported(Features.TOKENIZER_TYPE_RFC822)); 7643 7644 AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email") 7645 .addProperty(new StringPropertyConfig.Builder("address") 7646 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 7647 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 7648 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_RFC822) 7649 .build() 7650 ).build(); 7651 7652 Exception e = assertThrows(IllegalArgumentException.class, () -> 7653 mDb1.setSchemaAsync(new SetSchemaRequest.Builder() 7654 .setForceOverride(true).addSchemas(emailSchema).build()).get()); 7655 assertThat(e.getMessage()).isEqualTo("tokenizerType is out of range of [0, 1] (too high)"); 7656 } 7657 7658 7659 @Test testQuery_verbatimSearch()7660 public void testQuery_verbatimSearch() throws Exception { 7661 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.VERBATIM_SEARCH)); 7662 AppSearchSchema verbatimSchema = new AppSearchSchema.Builder("VerbatimSchema") 7663 .addProperty(new StringPropertyConfig.Builder("verbatimProp") 7664 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 7665 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 7666 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_VERBATIM) 7667 .build() 7668 ).build(); 7669 mDb1.setSchemaAsync(new SetSchemaRequest.Builder() 7670 .setForceOverride(true).addSchemas(verbatimSchema).build()).get(); 7671 7672 GenericDocument email = new GenericDocument.Builder<>( 7673 "namespace1", "id1", "VerbatimSchema") 7674 .setPropertyString("verbatimProp", "Hello, world!") 7675 .build(); 7676 mDb1.putAsync(new PutDocumentsRequest.Builder().addGenericDocuments(email).build()).get(); 7677 7678 SearchResults sr = mDb1.search("\"Hello, world!\"", 7679 new SearchSpec.Builder().setVerbatimSearchEnabled(true).build()); 7680 List<SearchResult> page = sr.getNextPageAsync().get(); 7681 7682 // Verbatim tokenization would produce one token 'Hello, world!'. 7683 assertThat(page).hasSize(1); 7684 assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id1"); 7685 } 7686 7687 @Test testQuery_verbatimSearchWithoutEnablingFeatureFails()7688 public void testQuery_verbatimSearchWithoutEnablingFeatureFails() throws Exception { 7689 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.VERBATIM_SEARCH)); 7690 AppSearchSchema verbatimSchema = new AppSearchSchema.Builder("VerbatimSchema") 7691 .addProperty(new StringPropertyConfig.Builder("verbatimProp") 7692 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 7693 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 7694 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_VERBATIM) 7695 .build() 7696 ).build(); 7697 mDb1.setSchemaAsync(new SetSchemaRequest.Builder() 7698 .setForceOverride(true).addSchemas(verbatimSchema).build()).get(); 7699 7700 GenericDocument email = new GenericDocument.Builder<>( 7701 "namespace1", "id1", "VerbatimSchema") 7702 .setPropertyString("verbatimProp", "Hello, world!") 7703 .build(); 7704 mDb1.putAsync(new PutDocumentsRequest.Builder().addGenericDocuments(email).build()).get(); 7705 7706 // Disable VERBATIM_SEARCH in the SearchSpec. 7707 SearchResults searchResults = mDb1.search("\"Hello, world!\"", 7708 new SearchSpec.Builder() 7709 .setVerbatimSearchEnabled(false) 7710 .build()); 7711 ExecutionException executionException = assertThrows(ExecutionException.class, 7712 () -> searchResults.getNextPageAsync().get()); 7713 assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class); 7714 AppSearchException exception = (AppSearchException) executionException.getCause(); 7715 assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT); 7716 assertThat(exception).hasMessageThat().contains("Attempted use of unenabled feature"); 7717 assertThat(exception).hasMessageThat().contains(Features.VERBATIM_SEARCH); 7718 } 7719 7720 @Test testQuery_listFilterQueryWithEnablingFeatureSucceeds()7721 public void testQuery_listFilterQueryWithEnablingFeatureSucceeds() throws Exception { 7722 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_QUERY_LANGUAGE)); 7723 AppSearchSchema schema = new AppSearchSchema.Builder("Schema") 7724 .addProperty(new StringPropertyConfig.Builder("prop") 7725 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 7726 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 7727 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 7728 .build() 7729 ).build(); 7730 mDb1.setSchemaAsync(new SetSchemaRequest.Builder() 7731 .setForceOverride(true).addSchemas(schema).build()).get(); 7732 7733 GenericDocument email = new GenericDocument.Builder<>( 7734 "namespace1", "id1", "Schema") 7735 .setPropertyString("prop", "Hello, world!") 7736 .build(); 7737 mDb1.putAsync(new PutDocumentsRequest.Builder().addGenericDocuments(email).build()).get(); 7738 7739 SearchSpec searchSpec = new SearchSpec.Builder() 7740 .setListFilterQueryLanguageEnabled(true) 7741 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 7742 .build(); 7743 // Support for function calls `search`, `createList` was added in list filters 7744 SearchResults searchResults = mDb1.search("search(\"hello\", createList(\"prop\"))", 7745 searchSpec); 7746 List<SearchResult> page = searchResults.getNextPageAsync().get(); 7747 assertThat(page).hasSize(1); 7748 assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id1"); 7749 7750 // Support for prefix operator * was added in list filters. 7751 searchResults = mDb1.search("wor*", searchSpec); 7752 page = searchResults.getNextPageAsync().get(); 7753 assertThat(page).hasSize(1); 7754 assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id1"); 7755 7756 // Combining negations with compound statements and property restricts was added in list 7757 // filters. 7758 searchResults = mDb1.search("NOT (foo OR otherProp:hello)", searchSpec); 7759 page = searchResults.getNextPageAsync().get(); 7760 assertThat(page).hasSize(1); 7761 assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id1"); 7762 } 7763 7764 @Test testQuery_PropertyDefinedWithEnablingFeatureSucceeds()7765 public void testQuery_PropertyDefinedWithEnablingFeatureSucceeds() throws Exception { 7766 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_QUERY_LANGUAGE)); 7767 AppSearchSchema schema1 = new AppSearchSchema.Builder("Schema1") 7768 .addProperty(new StringPropertyConfig.Builder("prop1") 7769 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 7770 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 7771 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 7772 .build() 7773 ).build(); 7774 AppSearchSchema schema2 = new AppSearchSchema.Builder("Schema2") 7775 .addProperty(new StringPropertyConfig.Builder("prop2") 7776 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 7777 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 7778 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 7779 .build() 7780 ).build(); 7781 mDb1.setSchemaAsync(new SetSchemaRequest.Builder() 7782 .setForceOverride(true).addSchemas(schema1, schema2).build()).get(); 7783 7784 GenericDocument doc1 = new GenericDocument.Builder<>( 7785 "namespace1", "id1", "Schema1") 7786 .setPropertyString("prop1", "Hello, world!") 7787 .build(); 7788 GenericDocument doc2 = new GenericDocument.Builder<>( 7789 "namespace1", "id2", "Schema2") 7790 .setPropertyString("prop2", "Hello, world!") 7791 .build(); 7792 mDb1.putAsync( 7793 new PutDocumentsRequest.Builder().addGenericDocuments(doc1, doc2).build()).get(); 7794 7795 SearchSpec searchSpec = new SearchSpec.Builder() 7796 .setListFilterQueryLanguageEnabled(true) 7797 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 7798 .build(); 7799 // Support for function calls `search`, `createList` was added in list filters 7800 SearchResults searchResults = mDb1.search("propertyDefined(\"prop1\")", 7801 searchSpec); 7802 List<SearchResult> page = searchResults.getNextPageAsync().get(); 7803 assertThat(page).hasSize(1); 7804 assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id1"); 7805 7806 // Support for prefix operator * was added in list filters. 7807 searchResults = mDb1.search("propertyDefined(\"prop2\")", searchSpec); 7808 page = searchResults.getNextPageAsync().get(); 7809 assertThat(page).hasSize(1); 7810 assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id2"); 7811 7812 // Combining negations with compound statements and property restricts was added in list 7813 // filters. 7814 searchResults = mDb1.search("NOT propertyDefined(\"prop1\")", searchSpec); 7815 page = searchResults.getNextPageAsync().get(); 7816 assertThat(page).hasSize(1); 7817 assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id2"); 7818 } 7819 7820 @Test testQuery_listFilterQueryWithoutEnablingFeatureFails()7821 public void testQuery_listFilterQueryWithoutEnablingFeatureFails() throws Exception { 7822 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_QUERY_LANGUAGE)); 7823 AppSearchSchema schema = new AppSearchSchema.Builder("Schema") 7824 .addProperty(new StringPropertyConfig.Builder("prop") 7825 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 7826 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 7827 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 7828 .build() 7829 ).build(); 7830 mDb1.setSchemaAsync(new SetSchemaRequest.Builder() 7831 .setForceOverride(true).addSchemas(schema).build()).get(); 7832 7833 GenericDocument email = new GenericDocument.Builder<>( 7834 "namespace1", "id1", "Schema") 7835 .setPropertyString("prop", "Hello, world!") 7836 .build(); 7837 mDb1.putAsync(new PutDocumentsRequest.Builder().addGenericDocuments(email).build()).get(); 7838 7839 // Disable LIST_FILTER_QUERY_LANGUAGE in the SearchSpec. 7840 SearchSpec searchSpec = new SearchSpec.Builder() 7841 .setListFilterQueryLanguageEnabled(false) 7842 .build(); 7843 SearchResults searchResults = mDb1.search("search(\"hello\", createList(\"prop\"))", 7844 searchSpec); 7845 ExecutionException executionException = assertThrows(ExecutionException.class, 7846 () -> searchResults.getNextPageAsync().get()); 7847 assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class); 7848 AppSearchException exception = (AppSearchException) executionException.getCause(); 7849 assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT); 7850 assertThat(exception).hasMessageThat().contains("Attempted use of unenabled feature"); 7851 assertThat(exception).hasMessageThat().contains(Features.LIST_FILTER_QUERY_LANGUAGE); 7852 7853 SearchResults searchResults2 = mDb1.search("wor*", searchSpec); 7854 executionException = assertThrows(ExecutionException.class, 7855 () -> searchResults2.getNextPageAsync().get()); 7856 assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class); 7857 exception = (AppSearchException) executionException.getCause(); 7858 assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT); 7859 assertThat(exception).hasMessageThat().contains("Attempted use of unenabled feature"); 7860 assertThat(exception).hasMessageThat().contains(Features.LIST_FILTER_QUERY_LANGUAGE); 7861 7862 SearchResults searchResults3 = mDb1.search("NOT (foo OR otherProp:hello)", searchSpec); 7863 executionException = assertThrows(ExecutionException.class, 7864 () -> searchResults3.getNextPageAsync().get()); 7865 assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class); 7866 exception = (AppSearchException) executionException.getCause(); 7867 assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT); 7868 assertThat(exception).hasMessageThat().contains("Attempted use of unenabled feature"); 7869 assertThat(exception).hasMessageThat().contains(Features.LIST_FILTER_QUERY_LANGUAGE); 7870 7871 SearchResults searchResults4 = mDb1.search("propertyDefined(\"prop\")", searchSpec); 7872 executionException = assertThrows(ExecutionException.class, 7873 () -> searchResults4.getNextPageAsync().get()); 7874 assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class); 7875 exception = (AppSearchException) executionException.getCause(); 7876 assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT); 7877 assertThat(exception).hasMessageThat().contains("Attempted use of unenabled feature"); 7878 assertThat(exception).hasMessageThat().contains(Features.LIST_FILTER_QUERY_LANGUAGE); 7879 } 7880 7881 @Test testQuery_listFilterQueryFeatures_notSupported()7882 public void testQuery_listFilterQueryFeatures_notSupported() throws Exception { 7883 assumeFalse(mDb1.getFeatures().isFeatureSupported(Features.NUMERIC_SEARCH)); 7884 assumeFalse(mDb1.getFeatures().isFeatureSupported(Features.VERBATIM_SEARCH)); 7885 assumeFalse(mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_QUERY_LANGUAGE)); 7886 7887 // UnsupportedOperationException will be thrown with these queries so no need to 7888 // define a schema and index document. 7889 SearchSpec.Builder builder = new SearchSpec.Builder(); 7890 SearchSpec searchSpec1 = builder.setNumericSearchEnabled(true).build(); 7891 SearchSpec searchSpec2 = builder.setVerbatimSearchEnabled(true).build(); 7892 SearchSpec searchSpec3 = builder.setListFilterQueryLanguageEnabled(true).build(); 7893 7894 assertThrows(UnsupportedOperationException.class, () -> 7895 mDb1.search("\"Hello, world!\"", searchSpec1)); 7896 assertThrows(UnsupportedOperationException.class, () -> 7897 mDb1.search("\"Hello, world!\"", searchSpec2)); 7898 assertThrows(UnsupportedOperationException.class, () -> 7899 mDb1.search("\"Hello, world!\"", searchSpec3)); 7900 } 7901 7902 @Test testQuery_listFilterQueryHasPropertyFunction_notSupported()7903 public void testQuery_listFilterQueryHasPropertyFunction_notSupported() throws Exception { 7904 assumeFalse( 7905 mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_HAS_PROPERTY_FUNCTION)); 7906 7907 // UnsupportedOperationException will be thrown with these queries so no need to 7908 // define a schema and index document. 7909 SearchSpec.Builder builder = new SearchSpec.Builder(); 7910 SearchSpec searchSpec = builder.setListFilterHasPropertyFunctionEnabled(true).build(); 7911 7912 UnsupportedOperationException exception = assertThrows( 7913 UnsupportedOperationException.class, 7914 () -> mDb1.search("\"Hello, world!\"", searchSpec)); 7915 assertThat(exception).hasMessageThat().contains(Features.LIST_FILTER_HAS_PROPERTY_FUNCTION 7916 + " is not available on this AppSearch implementation."); 7917 } 7918 7919 @Test testQuery_hasPropertyFunction()7920 public void testQuery_hasPropertyFunction() throws Exception { 7921 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_QUERY_LANGUAGE)); 7922 assumeTrue( 7923 mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_HAS_PROPERTY_FUNCTION)); 7924 AppSearchSchema schema = new AppSearchSchema.Builder("Schema") 7925 .addProperty(new StringPropertyConfig.Builder("prop1") 7926 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 7927 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 7928 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 7929 .build() 7930 ).addProperty(new StringPropertyConfig.Builder("prop2") 7931 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 7932 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 7933 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 7934 .build() 7935 ).build(); 7936 mDb1.setSchemaAsync(new SetSchemaRequest.Builder() 7937 .setForceOverride(true).addSchemas(schema).build()).get(); 7938 7939 GenericDocument doc1 = new GenericDocument.Builder<>( 7940 "namespace", "id1", "Schema") 7941 .setPropertyString("prop1", "Hello, world!") 7942 .build(); 7943 GenericDocument doc2 = new GenericDocument.Builder<>( 7944 "namespace", "id2", "Schema") 7945 .setPropertyString("prop2", "Hello, world!") 7946 .build(); 7947 GenericDocument doc3 = new GenericDocument.Builder<>( 7948 "namespace", "id3", "Schema") 7949 .setPropertyString("prop1", "Hello, world!") 7950 .setPropertyString("prop2", "Hello, world!") 7951 .build(); 7952 mDb1.putAsync(new PutDocumentsRequest.Builder() 7953 .addGenericDocuments(doc1, doc2, doc3).build()).get(); 7954 7955 SearchSpec searchSpec = new SearchSpec.Builder() 7956 .setListFilterQueryLanguageEnabled(true) 7957 .setListFilterHasPropertyFunctionEnabled(true) 7958 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 7959 .build(); 7960 SearchResults searchResults = mDb1.search("hasProperty(\"prop1\")", 7961 searchSpec); 7962 List<SearchResult> page = searchResults.getNextPageAsync().get(); 7963 assertThat(page).hasSize(2); 7964 assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id3"); 7965 assertThat(page.get(1).getGenericDocument().getId()).isEqualTo("id1"); 7966 7967 searchResults = mDb1.search("hasProperty(\"prop2\")", searchSpec); 7968 page = searchResults.getNextPageAsync().get(); 7969 assertThat(page).hasSize(2); 7970 assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id3"); 7971 assertThat(page.get(1).getGenericDocument().getId()).isEqualTo("id2"); 7972 7973 searchResults = mDb1.search( 7974 "hasProperty(\"prop1\") AND hasProperty(\"prop2\")", 7975 searchSpec); 7976 page = searchResults.getNextPageAsync().get(); 7977 assertThat(page).hasSize(1); 7978 assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id3"); 7979 } 7980 7981 @Test testQuery_hasPropertyFunctionWithoutEnablingFeatureFails()7982 public void testQuery_hasPropertyFunctionWithoutEnablingFeatureFails() throws Exception { 7983 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_QUERY_LANGUAGE)); 7984 assumeTrue( 7985 mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_HAS_PROPERTY_FUNCTION)); 7986 AppSearchSchema schema = new AppSearchSchema.Builder("Schema") 7987 .addProperty(new StringPropertyConfig.Builder("prop") 7988 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 7989 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 7990 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 7991 .build() 7992 ).build(); 7993 mDb1.setSchemaAsync(new SetSchemaRequest.Builder() 7994 .setForceOverride(true).addSchemas(schema).build()).get(); 7995 7996 GenericDocument doc = new GenericDocument.Builder<>( 7997 "namespace1", "id1", "Schema") 7998 .setPropertyString("prop", "Hello, world!") 7999 .build(); 8000 mDb1.putAsync(new PutDocumentsRequest.Builder().addGenericDocuments(doc).build()).get(); 8001 8002 // Enable LIST_FILTER_HAS_PROPERTY_FUNCTION but disable LIST_FILTER_QUERY_LANGUAGE in the 8003 // SearchSpec. 8004 SearchSpec searchSpec = new SearchSpec.Builder() 8005 .setListFilterQueryLanguageEnabled(false) 8006 .setListFilterHasPropertyFunctionEnabled(true) 8007 .build(); 8008 SearchResults searchResults = mDb1.search("hasProperty(\"prop\")", 8009 searchSpec); 8010 ExecutionException executionException = assertThrows(ExecutionException.class, 8011 () -> searchResults.getNextPageAsync().get()); 8012 assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class); 8013 AppSearchException exception = (AppSearchException) executionException.getCause(); 8014 assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT); 8015 assertThat(exception).hasMessageThat().contains("Attempted use of unenabled feature"); 8016 assertThat(exception).hasMessageThat().contains(Features.LIST_FILTER_QUERY_LANGUAGE); 8017 8018 // Disable LIST_FILTER_HAS_PROPERTY_FUNCTION in the SearchSpec. 8019 searchSpec = new SearchSpec.Builder() 8020 .setListFilterQueryLanguageEnabled(true) 8021 .setListFilterHasPropertyFunctionEnabled(false) 8022 .build(); 8023 SearchResults searchResults2 = mDb1.search("hasProperty(\"prop\")", 8024 searchSpec); 8025 executionException = assertThrows(ExecutionException.class, 8026 () -> searchResults2.getNextPageAsync().get()); 8027 assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class); 8028 exception = (AppSearchException) executionException.getCause(); 8029 assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT); 8030 assertThat(exception).hasMessageThat().contains("Attempted use of unenabled feature"); 8031 assertThat(exception).hasMessageThat().contains("HAS_PROPERTY_FUNCTION"); 8032 } 8033 8034 @Test 8035 @RequiresFlagsEnabled(Flags.FLAG_ENABLE_LIST_FILTER_MATCH_SCORE_EXPRESSION_FUNCTION) testQuery_matchScoreExpression()8036 public void testQuery_matchScoreExpression() throws Exception { 8037 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_QUERY_LANGUAGE)); 8038 assumeTrue( 8039 mDb1.getFeatures().isFeatureSupported( 8040 Features.LIST_FILTER_MATCH_SCORE_EXPRESSION_FUNCTION)); 8041 AppSearchSchema schema = new AppSearchSchema.Builder("Schema") 8042 .addProperty(new StringPropertyConfig.Builder("prop") 8043 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 8044 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 8045 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 8046 .build() 8047 ).build(); 8048 mDb1.setSchemaAsync(new SetSchemaRequest.Builder() 8049 .setForceOverride(true).addSchemas(schema).build()).get(); 8050 8051 // Put documents with document scores 3, 4, and 5. 8052 GenericDocument doc1 = new GenericDocument.Builder<>( 8053 "namespace", "id1", "Schema") 8054 .setPropertyString("prop", "Hello, world!") 8055 .setScore(3) 8056 .build(); 8057 GenericDocument doc2 = new GenericDocument.Builder<>( 8058 "namespace", "id2", "Schema") 8059 .setPropertyString("prop", "Hello, world!") 8060 .setScore(4) 8061 .build(); 8062 GenericDocument doc3 = new GenericDocument.Builder<>( 8063 "namespace", "id3", "Schema") 8064 .setPropertyString("prop", "Hello, world!") 8065 .setScore(5) 8066 .build(); 8067 mDb1.putAsync(new PutDocumentsRequest.Builder() 8068 .addGenericDocuments(doc1, doc2, doc3).build()).get(); 8069 8070 // Get documents of scores in [3, 4], which should return doc1 and doc2. 8071 SearchSpec searchSpec = new SearchSpec.Builder() 8072 .setListFilterQueryLanguageEnabled(true) 8073 .setListFilterMatchScoreExpressionFunctionEnabled(true) 8074 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 8075 .build(); 8076 SearchResults searchResults = mDb1.search( 8077 "matchScoreExpression(\"this.documentScore()\", 3, 4)", searchSpec); 8078 List<SearchResult> page = searchResults.getNextPageAsync().get(); 8079 assertThat(page).hasSize(2); 8080 assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id2"); 8081 assertThat(page.get(1).getGenericDocument().getId()).isEqualTo("id1"); 8082 8083 // Get documents of scores in [3, 5], which should return all documents. 8084 searchResults = mDb1.search( 8085 "matchScoreExpression(\"this.documentScore()\", 3, 5)", searchSpec); 8086 page = searchResults.getNextPageAsync().get(); 8087 assertThat(page).hasSize(3); 8088 assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id3"); 8089 assertThat(page.get(1).getGenericDocument().getId()).isEqualTo("id2"); 8090 assertThat(page.get(2).getGenericDocument().getId()).isEqualTo("id1"); 8091 } 8092 8093 @Test 8094 @RequiresFlagsEnabled(Flags.FLAG_ENABLE_LIST_FILTER_MATCH_SCORE_EXPRESSION_FUNCTION) testQuery_listFilterQueryMatchScoreExpressionFunction_notSupported()8095 public void testQuery_listFilterQueryMatchScoreExpressionFunction_notSupported() 8096 throws Exception { 8097 assumeFalse( 8098 mDb1.getFeatures().isFeatureSupported( 8099 Features.LIST_FILTER_MATCH_SCORE_EXPRESSION_FUNCTION)); 8100 8101 // UnsupportedOperationException will be thrown with these queries so no need to 8102 // define a schema and index document. 8103 SearchSpec.Builder builder = new SearchSpec.Builder(); 8104 SearchSpec searchSpec = builder 8105 .setListFilterMatchScoreExpressionFunctionEnabled(true) 8106 .build(); 8107 8108 UnsupportedOperationException exception = assertThrows( 8109 UnsupportedOperationException.class, 8110 () -> mDb1.search("\"Hello, world!\"", searchSpec)); 8111 assertThat(exception).hasMessageThat().contains( 8112 Features.LIST_FILTER_MATCH_SCORE_EXPRESSION_FUNCTION 8113 + " is not available on this AppSearch implementation."); 8114 } 8115 8116 @Test 8117 @RequiresFlagsEnabled(Flags.FLAG_ENABLE_LIST_FILTER_MATCH_SCORE_EXPRESSION_FUNCTION) testQuery_matchScoreExpressionFunctionWithoutEnablingFeatureFails()8118 public void testQuery_matchScoreExpressionFunctionWithoutEnablingFeatureFails() 8119 throws Exception { 8120 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_QUERY_LANGUAGE)); 8121 assumeTrue(mDb1.getFeatures().isFeatureSupported( 8122 Features.LIST_FILTER_MATCH_SCORE_EXPRESSION_FUNCTION)); 8123 AppSearchSchema schema = new AppSearchSchema.Builder("Schema") 8124 .addProperty(new StringPropertyConfig.Builder("prop") 8125 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 8126 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 8127 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 8128 .build() 8129 ).build(); 8130 mDb1.setSchemaAsync(new SetSchemaRequest.Builder() 8131 .setForceOverride(true).addSchemas(schema).build()).get(); 8132 8133 GenericDocument doc = new GenericDocument.Builder<>( 8134 "namespace", "id", "Schema") 8135 .setPropertyString("prop", "Hello, world!") 8136 .build(); 8137 mDb1.putAsync(new PutDocumentsRequest.Builder().addGenericDocuments(doc).build()).get(); 8138 8139 // Enable LIST_FILTER_MATCH_SCORE_EXPRESSION_FUNCTION but disable 8140 // LIST_FILTER_QUERY_LANGUAGE in the SearchSpec. 8141 SearchSpec searchSpec = new SearchSpec.Builder() 8142 .setListFilterQueryLanguageEnabled(false) 8143 .setListFilterMatchScoreExpressionFunctionEnabled(true) 8144 .build(); 8145 SearchResults searchResults = mDb1.search( 8146 "matchScoreExpression(\"this.documentScore()\", 3, 4)", searchSpec); 8147 ExecutionException executionException = assertThrows(ExecutionException.class, 8148 () -> searchResults.getNextPageAsync().get()); 8149 assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class); 8150 AppSearchException exception = (AppSearchException) executionException.getCause(); 8151 assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT); 8152 assertThat(exception).hasMessageThat().contains("Attempted use of unenabled feature"); 8153 assertThat(exception).hasMessageThat().contains(Features.LIST_FILTER_QUERY_LANGUAGE); 8154 8155 // Disable LIST_FILTER_MATCH_SCORE_EXPRESSION_FUNCTION in the SearchSpec. 8156 searchSpec = new SearchSpec.Builder() 8157 .setListFilterQueryLanguageEnabled(true) 8158 .setListFilterMatchScoreExpressionFunctionEnabled(false) 8159 .build(); 8160 SearchResults searchResults2 = mDb1.search( 8161 "matchScoreExpression(\"this.documentScore()\", 3, 4)", searchSpec); 8162 executionException = assertThrows(ExecutionException.class, 8163 () -> searchResults2.getNextPageAsync().get()); 8164 assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class); 8165 exception = (AppSearchException) executionException.getCause(); 8166 assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT); 8167 assertThat(exception).hasMessageThat().contains("Attempted use of unenabled feature"); 8168 assertThat(exception).hasMessageThat().contains("MATCH_SCORE_EXPRESSION"); 8169 } 8170 8171 @Test testQuery_propertyWeightsNotSupported()8172 public void testQuery_propertyWeightsNotSupported() throws Exception { 8173 assumeFalse(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SPEC_PROPERTY_WEIGHTS)); 8174 8175 // Schema registration 8176 mDb1.setSchemaAsync( 8177 new SetSchemaRequest.Builder() 8178 .addSchemas(AppSearchEmail.SCHEMA) 8179 .build()).get(); 8180 8181 // Index two documents 8182 AppSearchEmail email1 = 8183 new AppSearchEmail.Builder("namespace", "id1") 8184 .setCreationTimestampMillis(1000) 8185 .setSubject("foo") 8186 .build(); 8187 AppSearchEmail email2 = 8188 new AppSearchEmail.Builder("namespace", "id2") 8189 .setCreationTimestampMillis(1000) 8190 .setBody("foo") 8191 .build(); 8192 checkIsBatchResultSuccess(mDb1.putAsync( 8193 new PutDocumentsRequest.Builder() 8194 .addGenericDocuments(email1, email2).build())); 8195 8196 SearchSpec searchSpec = new SearchSpec.Builder() 8197 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 8198 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE) 8199 .setOrder(SearchSpec.ORDER_DESCENDING) 8200 .setPropertyWeights(AppSearchEmail.SCHEMA_TYPE, ImmutableMap.of("subject", 8201 2.0, "body", 0.5)) 8202 .build(); 8203 UnsupportedOperationException exception = 8204 assertThrows(UnsupportedOperationException.class, 8205 () -> mDb1.search("Hello", searchSpec)); 8206 assertThat(exception).hasMessageThat().contains("Property weights are not supported"); 8207 } 8208 8209 @Test testQuery_propertyWeights()8210 public void testQuery_propertyWeights() throws Exception { 8211 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SPEC_PROPERTY_WEIGHTS)); 8212 8213 // Schema registration 8214 mDb1.setSchemaAsync( 8215 new SetSchemaRequest.Builder() 8216 .addSchemas(AppSearchEmail.SCHEMA) 8217 .build()).get(); 8218 8219 // Index two documents 8220 AppSearchEmail email1 = 8221 new AppSearchEmail.Builder("namespace", "id1") 8222 .setCreationTimestampMillis(1000) 8223 .setSubject("foo") 8224 .build(); 8225 AppSearchEmail email2 = 8226 new AppSearchEmail.Builder("namespace", "id2") 8227 .setCreationTimestampMillis(1000) 8228 .setBody("foo") 8229 .build(); 8230 checkIsBatchResultSuccess(mDb1.putAsync( 8231 new PutDocumentsRequest.Builder() 8232 .addGenericDocuments(email1, email2).build())); 8233 8234 // Query for "foo". It should match both emails. 8235 SearchResults searchResults = mDb1.search("foo", new SearchSpec.Builder() 8236 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 8237 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE) 8238 .setOrder(SearchSpec.ORDER_DESCENDING) 8239 .setPropertyWeights(AppSearchEmail.SCHEMA_TYPE, ImmutableMap.of("subject", 8240 2.0, "body", 0.5)) 8241 .build()); 8242 List<SearchResult> results = retrieveAllSearchResults(searchResults); 8243 8244 // email1 should be ranked higher because "foo" appears in the "subject" property which 8245 // has higher weight than the "body" property. 8246 assertThat(results).hasSize(2); 8247 assertThat(results.get(0).getRankingSignal()).isGreaterThan(0); 8248 assertThat(results.get(0).getRankingSignal()).isGreaterThan( 8249 results.get(1).getRankingSignal()); 8250 assertThat(results.get(0).getGenericDocument()).isEqualTo(email1); 8251 assertThat(results.get(1).getGenericDocument()).isEqualTo(email2); 8252 8253 // Query for "foo" without property weights. 8254 SearchSpec searchSpecWithoutWeights = new SearchSpec.Builder() 8255 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 8256 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE) 8257 .setOrder(SearchSpec.ORDER_DESCENDING) 8258 .build(); 8259 SearchResults searchResultsWithoutWeights = mDb1.search("foo", searchSpecWithoutWeights); 8260 List<SearchResult> resultsWithoutWeights = 8261 retrieveAllSearchResults(searchResultsWithoutWeights); 8262 8263 // email1 should have the same ranking signal as email2 as each contains the term "foo" 8264 // once. 8265 assertThat(resultsWithoutWeights).hasSize(2); 8266 assertThat(resultsWithoutWeights.get(0).getRankingSignal()).isGreaterThan(0); 8267 assertThat(resultsWithoutWeights.get(0).getRankingSignal()).isEqualTo( 8268 resultsWithoutWeights.get(1).getRankingSignal()); 8269 } 8270 8271 @Test testQuery_propertyWeightsNestedProperties()8272 public void testQuery_propertyWeightsNestedProperties() throws Exception { 8273 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SPEC_PROPERTY_WEIGHTS)); 8274 8275 // Register a schema with a nested type 8276 AppSearchSchema schema = 8277 new AppSearchSchema.Builder("TypeA").addProperty( 8278 new AppSearchSchema.DocumentPropertyConfig.Builder("nestedEmail", 8279 AppSearchEmail.SCHEMA_TYPE).setShouldIndexNestedProperties( 8280 true).build()).build(); 8281 mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA, 8282 schema).build()).get(); 8283 8284 // Index two documents 8285 AppSearchEmail nestedEmail1 = 8286 new AppSearchEmail.Builder("namespace", "id1") 8287 .setCreationTimestampMillis(1000) 8288 .setSubject("foo") 8289 .build(); 8290 GenericDocument doc1 = 8291 new GenericDocument.Builder<>("namespace", "id1", "TypeA").setPropertyDocument( 8292 "nestedEmail", nestedEmail1).build(); 8293 AppSearchEmail nestedEmail2 = 8294 new AppSearchEmail.Builder("namespace", "id2") 8295 .setCreationTimestampMillis(1000) 8296 .setBody("foo") 8297 .build(); 8298 GenericDocument doc2 = 8299 new GenericDocument.Builder<>("namespace", "id2", "TypeA").setPropertyDocument( 8300 "nestedEmail", nestedEmail2).build(); 8301 checkIsBatchResultSuccess(mDb1.putAsync( 8302 new PutDocumentsRequest.Builder() 8303 .addGenericDocuments(doc1, doc2).build())); 8304 8305 // Query for "foo". It should match both emails. 8306 SearchResults searchResults = mDb1.search("foo", new SearchSpec.Builder() 8307 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 8308 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE) 8309 .setOrder(SearchSpec.ORDER_DESCENDING) 8310 .setPropertyWeights("TypeA", ImmutableMap.of( 8311 "nestedEmail.subject", 8312 2.0, "nestedEmail.body", 0.5)) 8313 .build()); 8314 List<SearchResult> results = retrieveAllSearchResults(searchResults); 8315 8316 // email1 should be ranked higher because "foo" appears in the "nestedEmail.subject" 8317 // property which has higher weight than the "nestedEmail.body" property. 8318 assertThat(results).hasSize(2); 8319 assertThat(results.get(0).getRankingSignal()).isGreaterThan(0); 8320 assertThat(results.get(0).getRankingSignal()).isGreaterThan( 8321 results.get(1).getRankingSignal()); 8322 assertThat(results.get(0).getGenericDocument()).isEqualTo(doc1); 8323 assertThat(results.get(1).getGenericDocument()).isEqualTo(doc2); 8324 8325 // Query for "foo" without property weights. 8326 SearchSpec searchSpecWithoutWeights = new SearchSpec.Builder() 8327 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 8328 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE) 8329 .setOrder(SearchSpec.ORDER_DESCENDING) 8330 .build(); 8331 SearchResults searchResultsWithoutWeights = mDb1.search("foo", searchSpecWithoutWeights); 8332 List<SearchResult> resultsWithoutWeights = 8333 retrieveAllSearchResults(searchResultsWithoutWeights); 8334 8335 // email1 should have the same ranking signal as email2 as each contains the term "foo" 8336 // once. 8337 assertThat(resultsWithoutWeights).hasSize(2); 8338 assertThat(resultsWithoutWeights.get(0).getRankingSignal()).isGreaterThan(0); 8339 assertThat(resultsWithoutWeights.get(0).getRankingSignal()).isEqualTo( 8340 resultsWithoutWeights.get(1).getRankingSignal()); 8341 } 8342 8343 @Test testQuery_propertyWeightsDefaults()8344 public void testQuery_propertyWeightsDefaults() throws Exception { 8345 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SPEC_PROPERTY_WEIGHTS)); 8346 8347 // Schema registration 8348 mDb1.setSchemaAsync( 8349 new SetSchemaRequest.Builder() 8350 .addSchemas(AppSearchEmail.SCHEMA) 8351 .build()).get(); 8352 8353 // Index two documents 8354 AppSearchEmail email1 = 8355 new AppSearchEmail.Builder("namespace", "id1") 8356 .setCreationTimestampMillis(1000) 8357 .setSubject("foo") 8358 .build(); 8359 AppSearchEmail email2 = 8360 new AppSearchEmail.Builder("namespace", "id2") 8361 .setCreationTimestampMillis(1000) 8362 .setBody("foo bar") 8363 .build(); 8364 checkIsBatchResultSuccess(mDb1.putAsync( 8365 new PutDocumentsRequest.Builder() 8366 .addGenericDocuments(email1, email2).build())); 8367 8368 // Query for "foo" without assigning property weights for any path. 8369 SearchResults searchResults = mDb1.search("foo", new SearchSpec.Builder() 8370 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 8371 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE) 8372 .setOrder(SearchSpec.ORDER_DESCENDING) 8373 .setPropertyWeights(AppSearchEmail.SCHEMA_TYPE, ImmutableMap.of()) 8374 .build()); 8375 List<SearchResult> resultsWithoutPropertyWeights = retrieveAllSearchResults( 8376 searchResults); 8377 8378 // Query for "foo" with assigning default property weights. 8379 searchResults = mDb1.search("foo", new SearchSpec.Builder() 8380 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 8381 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE) 8382 .setOrder(SearchSpec.ORDER_DESCENDING) 8383 .setPropertyWeights(AppSearchEmail.SCHEMA_TYPE, ImmutableMap.of("subject", 1.0, 8384 "body", 1.0)) 8385 .build()); 8386 List<SearchResult> expectedResults = retrieveAllSearchResults(searchResults); 8387 8388 assertThat(resultsWithoutPropertyWeights).hasSize(2); 8389 assertThat(expectedResults).hasSize(2); 8390 8391 assertThat(resultsWithoutPropertyWeights.get(0).getGenericDocument()).isEqualTo(email1); 8392 assertThat(resultsWithoutPropertyWeights.get(1).getGenericDocument()).isEqualTo(email2); 8393 assertThat(expectedResults.get(0).getGenericDocument()).isEqualTo(email1); 8394 assertThat(expectedResults.get(1).getGenericDocument()).isEqualTo(email2); 8395 8396 // The ranking signal for results with no property path and weights set should be equal 8397 // to the ranking signal for results with explicitly set default weights. 8398 assertThat(resultsWithoutPropertyWeights.get(0).getRankingSignal()).isEqualTo( 8399 expectedResults.get(0).getRankingSignal()); 8400 assertThat(resultsWithoutPropertyWeights.get(1).getRankingSignal()).isEqualTo( 8401 expectedResults.get(1).getRankingSignal()); 8402 } 8403 8404 @Test testQuery_propertyWeightsIgnoresInvalidPropertyPaths()8405 public void testQuery_propertyWeightsIgnoresInvalidPropertyPaths() throws Exception { 8406 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SPEC_PROPERTY_WEIGHTS)); 8407 8408 // Schema registration 8409 mDb1.setSchemaAsync( 8410 new SetSchemaRequest.Builder() 8411 .addSchemas(AppSearchEmail.SCHEMA) 8412 .build()).get(); 8413 8414 // Index an email 8415 AppSearchEmail email1 = 8416 new AppSearchEmail.Builder("namespace", "id1") 8417 .setCreationTimestampMillis(1000) 8418 .setSubject("baz") 8419 .build(); 8420 checkIsBatchResultSuccess(mDb1.putAsync( 8421 new PutDocumentsRequest.Builder() 8422 .addGenericDocuments(email1).build())); 8423 8424 // Query for "baz" with property weight for "subject", a valid property in the schema type. 8425 SearchResults searchResults = mDb1.search("baz", new SearchSpec.Builder() 8426 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 8427 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE) 8428 .setOrder(SearchSpec.ORDER_DESCENDING) 8429 .setPropertyWeights(AppSearchEmail.SCHEMA_TYPE, ImmutableMap.of("subject", 2.0)) 8430 .build()); 8431 List<SearchResult> results = retrieveAllSearchResults(searchResults); 8432 8433 // Query for "baz" with property weights, one for valid property "subject" and one for a 8434 // non-existing property "invalid". 8435 searchResults = mDb1.search("baz", new SearchSpec.Builder() 8436 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 8437 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE) 8438 .setOrder(SearchSpec.ORDER_DESCENDING) 8439 .setPropertyWeights(AppSearchEmail.SCHEMA_TYPE, ImmutableMap.of("subject", 2.0, 8440 "invalid", 3.0)) 8441 .build()); 8442 List<SearchResult> resultsWithInvalidPath = retrieveAllSearchResults(searchResults); 8443 8444 assertThat(results).hasSize(1); 8445 assertThat(resultsWithInvalidPath).hasSize(1); 8446 8447 // We expect the ranking signal to be unchanged in the presence of an invalid property 8448 // weight. 8449 assertThat(results.get(0).getRankingSignal()).isGreaterThan(0); 8450 assertThat(resultsWithInvalidPath.get(0).getRankingSignal()).isEqualTo( 8451 results.get(0).getRankingSignal()); 8452 8453 assertThat(results.get(0).getGenericDocument()).isEqualTo(email1); 8454 assertThat(resultsWithInvalidPath.get(0).getGenericDocument()).isEqualTo(email1); 8455 } 8456 8457 @Test testQueryWithJoin_typePropertyFiltersOnNestedSpec()8458 public void testQueryWithJoin_typePropertyFiltersOnNestedSpec() throws Exception { 8459 assumeTrue(mDb1.getFeatures().isFeatureSupported( 8460 Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES)); 8461 assumeTrue(mDb1.getFeatures() 8462 .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID)); 8463 8464 // A full example of how join might be used with property filters in join spec 8465 AppSearchSchema actionSchema = new AppSearchSchema.Builder("ViewAction") 8466 .addProperty(new StringPropertyConfig.Builder("entityId") 8467 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 8468 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 8469 .setJoinableValueType(StringPropertyConfig 8470 .JOINABLE_VALUE_TYPE_QUALIFIED_ID) 8471 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 8472 .build() 8473 ).addProperty(new StringPropertyConfig.Builder("note") 8474 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 8475 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 8476 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 8477 .build() 8478 ).addProperty(new StringPropertyConfig.Builder("viewType") 8479 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 8480 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 8481 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 8482 .build() 8483 ).build(); 8484 8485 // Schema registration 8486 mDb1.setSchemaAsync( 8487 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA, actionSchema) 8488 .build()).get(); 8489 8490 // Index 2 email documents 8491 AppSearchEmail inEmail = 8492 new AppSearchEmail.Builder("namespace", "id1") 8493 .setFrom("from@example.com") 8494 .setTo("to1@example.com", "to2@example.com") 8495 .setSubject("testPut example") 8496 .setBody("This is the body of the testPut email") 8497 .build(); 8498 8499 AppSearchEmail inEmail2 = 8500 new AppSearchEmail.Builder("namespace", "id2") 8501 .setFrom("from@example.com") 8502 .setTo("to1@example.com", "to2@example.com") 8503 .setSubject("testPut example") 8504 .setBody("This is the body of the testPut email") 8505 .build(); 8506 8507 // Index 2 viewAction documents, one for email1 and the other for email2 8508 String qualifiedId1 = 8509 DocumentIdUtil.createQualifiedId( 8510 ApplicationProvider.getApplicationContext().getPackageName(), DB_NAME_1, 8511 "namespace", "id1"); 8512 String qualifiedId2 = 8513 DocumentIdUtil.createQualifiedId( 8514 ApplicationProvider.getApplicationContext().getPackageName(), DB_NAME_1, 8515 "namespace", "id2"); 8516 GenericDocument viewAction1 = new GenericDocument.Builder<>("NS", "id3", "ViewAction") 8517 .setPropertyString("entityId", qualifiedId1) 8518 .setPropertyString("note", "Viewed email on Monday") 8519 .setPropertyString("viewType", "Stared").build(); 8520 GenericDocument viewAction2 = new GenericDocument.Builder<>("NS", "id4", "ViewAction") 8521 .setPropertyString("entityId", qualifiedId2) 8522 .setPropertyString("note", "Viewed email on Tuesday") 8523 .setPropertyString("viewType", "Viewed").build(); 8524 checkIsBatchResultSuccess(mDb1.putAsync( 8525 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail, inEmail2, 8526 viewAction1, viewAction2) 8527 .build())); 8528 8529 // The nested search spec only allows searching the viewType property for viewAction 8530 // schema type. It also specifies a property filter for Email schema. 8531 SearchSpec nestedSearchSpec = 8532 new SearchSpec.Builder() 8533 .addFilterProperties("ViewAction", ImmutableList.of("viewType")) 8534 .addFilterProperties(AppSearchEmail.SCHEMA_TYPE, 8535 ImmutableList.of("subject")) 8536 .build(); 8537 8538 // Search for the term "Viewed" in join spec 8539 JoinSpec js = new JoinSpec.Builder("entityId") 8540 .setNestedSearch("Viewed", nestedSearchSpec) 8541 .setAggregationScoringStrategy(JoinSpec.AGGREGATION_SCORING_RESULT_COUNT) 8542 .build(); 8543 8544 SearchResults searchResults = mDb1.search("body email", new SearchSpec.Builder() 8545 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_JOIN_AGGREGATE_SCORE) 8546 .setJoinSpec(js) 8547 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 8548 .build()); 8549 8550 List<SearchResult> sr = searchResults.getNextPageAsync().get(); 8551 8552 // Both email docs are returned, email2 comes first because it has higher number of 8553 // joined documents. The property filters for Email schema specified in the nested search 8554 // specs don't apply to the outer query (otherwise none of the email documents would have 8555 // been returned). 8556 assertThat(sr).hasSize(2); 8557 8558 // Email2 has a viewAction document viewAction2 that satisfies the property filters in 8559 // the join spec, so it should be present in the joined results. 8560 assertThat(sr.get(0).getGenericDocument().getId()).isEqualTo("id2"); 8561 assertThat(sr.get(0).getRankingSignal()).isEqualTo(1.0); 8562 assertThat(sr.get(0).getJoinedResults()).hasSize(1); 8563 assertThat(sr.get(0).getJoinedResults().get(0).getGenericDocument()).isEqualTo(viewAction2); 8564 8565 // Email1 has a viewAction document viewAction1 but it doesn't satisfy the property filters 8566 // in the join spec, so it should not be present in the joined results. 8567 assertThat(sr.get(1).getGenericDocument().getId()).isEqualTo("id1"); 8568 assertThat(sr.get(1).getRankingSignal()).isEqualTo(0.0); 8569 assertThat(sr.get(1).getJoinedResults()).isEmpty(); 8570 } 8571 8572 @Test testQueryWithJoin_typePropertyFiltersOnOuterSpec()8573 public void testQueryWithJoin_typePropertyFiltersOnOuterSpec() throws Exception { 8574 assumeTrue(mDb1.getFeatures().isFeatureSupported( 8575 Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES)); 8576 assumeTrue(mDb1.getFeatures() 8577 .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID)); 8578 8579 // A full example of how join might be used with property filters in join spec 8580 AppSearchSchema actionSchema = new AppSearchSchema.Builder("ViewAction") 8581 .addProperty(new StringPropertyConfig.Builder("entityId") 8582 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 8583 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 8584 .setJoinableValueType(StringPropertyConfig 8585 .JOINABLE_VALUE_TYPE_QUALIFIED_ID) 8586 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 8587 .build() 8588 ).addProperty(new StringPropertyConfig.Builder("note") 8589 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 8590 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 8591 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 8592 .build() 8593 ).addProperty(new StringPropertyConfig.Builder("viewType") 8594 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 8595 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 8596 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 8597 .build() 8598 ).build(); 8599 8600 // Schema registration 8601 mDb1.setSchemaAsync( 8602 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA, actionSchema) 8603 .build()).get(); 8604 8605 // Index 2 email documents 8606 AppSearchEmail inEmail = 8607 new AppSearchEmail.Builder("namespace", "id1") 8608 .setFrom("from@example.com") 8609 .setTo("to1@example.com", "to2@example.com") 8610 .setSubject("testPut example") 8611 .setBody("This is the body of the testPut email") 8612 .build(); 8613 8614 AppSearchEmail inEmail2 = 8615 new AppSearchEmail.Builder("namespace", "id2") 8616 .setFrom("from@example.com") 8617 .setTo("to1@example.com", "to2@example.com") 8618 .setSubject("testPut example") 8619 .setBody("This is the body of the testPut email") 8620 .build(); 8621 8622 // Index 2 viewAction documents, one for email1 and the other for email2 8623 String qualifiedId1 = 8624 DocumentIdUtil.createQualifiedId( 8625 ApplicationProvider.getApplicationContext().getPackageName(), DB_NAME_1, 8626 "namespace", "id1"); 8627 String qualifiedId2 = 8628 DocumentIdUtil.createQualifiedId( 8629 ApplicationProvider.getApplicationContext().getPackageName(), DB_NAME_1, 8630 "namespace", "id2"); 8631 GenericDocument viewAction1 = new GenericDocument.Builder<>("NS", "id3", "ViewAction") 8632 .setPropertyString("entityId", qualifiedId1) 8633 .setPropertyString("note", "Viewed email on Monday") 8634 .setPropertyString("viewType", "Stared").build(); 8635 GenericDocument viewAction2 = new GenericDocument.Builder<>("NS", "id4", "ViewAction") 8636 .setPropertyString("entityId", qualifiedId2) 8637 .setPropertyString("note", "Viewed email on Tuesday") 8638 .setPropertyString("viewType", "Viewed").build(); 8639 checkIsBatchResultSuccess(mDb1.putAsync( 8640 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail, inEmail2, 8641 viewAction1, viewAction2) 8642 .build())); 8643 8644 // The nested search spec doesn't specify any property filters. 8645 SearchSpec nestedSearchSpec = new SearchSpec.Builder().build(); 8646 8647 // Search for the term "Viewed" in join spec 8648 JoinSpec js = new JoinSpec.Builder("entityId") 8649 .setNestedSearch("Viewed", nestedSearchSpec) 8650 .setAggregationScoringStrategy(JoinSpec.AGGREGATION_SCORING_RESULT_COUNT) 8651 .build(); 8652 8653 // Outer search spec adds property filters for both Email and ViewAction schema 8654 SearchResults searchResults = mDb1.search("body email", new SearchSpec.Builder() 8655 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_JOIN_AGGREGATE_SCORE) 8656 .setJoinSpec(js) 8657 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 8658 .addFilterProperties(AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("body")) 8659 .addFilterProperties("ViewAction", ImmutableList.of("viewType")) 8660 .build()); 8661 8662 List<SearchResult> sr = searchResults.getNextPageAsync().get(); 8663 8664 // Both email docs are returned as they both satisfy the property filters for Email, email2 8665 // comes first because it has higher id lexicographically. 8666 assertThat(sr).hasSize(2); 8667 8668 // Email2 has a viewAction document viewAction2 that satisfies the property filters in 8669 // the outer spec (although those property filters are irrelevant for joined documents), 8670 // it should be present in the joined results. 8671 assertThat(sr.get(0).getGenericDocument().getId()).isEqualTo("id2"); 8672 assertThat(sr.get(0).getRankingSignal()).isEqualTo(1.0); 8673 assertThat(sr.get(0).getJoinedResults()).hasSize(1); 8674 assertThat(sr.get(0).getJoinedResults().get(0).getGenericDocument()).isEqualTo(viewAction2); 8675 8676 // Email1 has a viewAction document viewAction1 that doesn't satisfy the property filters 8677 // in the outer spec, but property filters in the outer spec should not apply on joined 8678 // documents, so viewAction1 should be present in the joined results. 8679 assertThat(sr.get(1).getGenericDocument().getId()).isEqualTo("id1"); 8680 assertThat(sr.get(1).getRankingSignal()).isEqualTo(1.0); 8681 assertThat(sr.get(0).getJoinedResults()).hasSize(1); 8682 assertThat(sr.get(1).getJoinedResults().get(0).getGenericDocument()).isEqualTo(viewAction1); 8683 } 8684 8685 @Test testQuery_typePropertyFiltersNotSupported()8686 public void testQuery_typePropertyFiltersNotSupported() throws Exception { 8687 assumeFalse(mDb1.getFeatures().isFeatureSupported( 8688 Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES)); 8689 // Schema registration 8690 mDb1.setSchemaAsync( 8691 new SetSchemaRequest.Builder() 8692 .addSchemas(AppSearchEmail.SCHEMA) 8693 .build()).get(); 8694 8695 // Query with type property filters {"Email", ["subject", "to"]} and verify that unsupported 8696 // exception is thrown 8697 SearchSpec searchSpec = new SearchSpec.Builder() 8698 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 8699 .addFilterProperties(AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("subject", "to")) 8700 .build(); 8701 UnsupportedOperationException exception = 8702 assertThrows(UnsupportedOperationException.class, 8703 () -> mDb1.search("body", searchSpec)); 8704 assertThat(exception).hasMessageThat().contains(Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES 8705 + " is not available on this AppSearch implementation."); 8706 } 8707 8708 @Test 8709 @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SEARCH_RESULT_PARENT_TYPES) testQuery_searchResultWrapsParentTypeMapForPolymorphism()8710 public void testQuery_searchResultWrapsParentTypeMapForPolymorphism() throws Exception { 8711 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE)); 8712 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_RESULT_PARENT_TYPES)); 8713 8714 // Schema registration 8715 AppSearchSchema personSchema = 8716 new AppSearchSchema.Builder("Person") 8717 .addProperty( 8718 new StringPropertyConfig.Builder("name") 8719 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 8720 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 8721 .setIndexingType( 8722 StringPropertyConfig.INDEXING_TYPE_PREFIXES) 8723 .build()) 8724 .build(); 8725 AppSearchSchema artistSchema = 8726 new AppSearchSchema.Builder("Artist") 8727 .addParentType("Person") 8728 .addProperty( 8729 new StringPropertyConfig.Builder("name") 8730 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 8731 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 8732 .setIndexingType( 8733 StringPropertyConfig.INDEXING_TYPE_PREFIXES) 8734 .build()) 8735 .addProperty( 8736 new StringPropertyConfig.Builder("company") 8737 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 8738 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 8739 .setIndexingType( 8740 StringPropertyConfig.INDEXING_TYPE_PREFIXES) 8741 .build()) 8742 .build(); 8743 AppSearchSchema musicianSchema = 8744 new AppSearchSchema.Builder("Musician") 8745 .addParentType("Artist") 8746 .addProperty( 8747 new StringPropertyConfig.Builder("name") 8748 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 8749 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 8750 .setIndexingType( 8751 StringPropertyConfig.INDEXING_TYPE_PREFIXES) 8752 .build()) 8753 .addProperty( 8754 new StringPropertyConfig.Builder("company") 8755 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 8756 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 8757 .setIndexingType( 8758 StringPropertyConfig.INDEXING_TYPE_PREFIXES) 8759 .build()) 8760 .build(); 8761 AppSearchSchema messageSchema = 8762 new AppSearchSchema.Builder("Message") 8763 .addProperty( 8764 new AppSearchSchema.DocumentPropertyConfig.Builder( 8765 "receivers", "Person") 8766 .setCardinality(PropertyConfig.CARDINALITY_REPEATED) 8767 .setShouldIndexNestedProperties(true) 8768 .build()) 8769 .build(); 8770 mDb1.setSchemaAsync( 8771 new SetSchemaRequest.Builder() 8772 .addSchemas(personSchema) 8773 .addSchemas(artistSchema) 8774 .addSchemas(musicianSchema) 8775 .addSchemas(messageSchema) 8776 .build()) 8777 .get(); 8778 8779 // Index documents 8780 GenericDocument personDoc = 8781 new GenericDocument.Builder<>("namespace", "id1", "Person") 8782 .setPropertyString("name", "person") 8783 .build(); 8784 GenericDocument artistDoc = 8785 new GenericDocument.Builder<>("namespace", "id2", "Artist") 8786 .setPropertyString("name", "artist") 8787 .setPropertyString("company", "foo") 8788 .build(); 8789 GenericDocument musicianDoc = 8790 new GenericDocument.Builder<>("namespace", "id3", "Musician") 8791 .setPropertyString("name", "musician") 8792 .setPropertyString("company", "foo") 8793 .build(); 8794 GenericDocument messageDoc = 8795 new GenericDocument.Builder<>("namespace", "id4", "Message") 8796 .setPropertyDocument("receivers", artistDoc, musicianDoc) 8797 .build(); 8798 8799 Map<String, List<String>> expectedPersonParentTypeMap = Collections.emptyMap(); 8800 Map<String, List<String>> expectedArtistParentTypeMap = 8801 ImmutableMap.of("Artist", ImmutableList.of("Person")); 8802 Map<String, List<String>> expectedMusicianParentTypeMap = 8803 ImmutableMap.of("Musician", ImmutableList.of("Artist", "Person")); 8804 // artistDoc and musicianDoc are nested in messageDoc, so messageDoc's parent type map 8805 // should have the entries for both the Artist and Musician type. 8806 Map<String, List<String>> expectedMessageParentTypeMap = ImmutableMap.of( 8807 "Artist", ImmutableList.of("Person"), 8808 "Musician", ImmutableList.of("Artist", "Person")); 8809 8810 checkIsBatchResultSuccess( 8811 mDb1.putAsync( 8812 new PutDocumentsRequest.Builder() 8813 .addGenericDocuments(personDoc, artistDoc, musicianDoc, messageDoc) 8814 .build())); 8815 8816 // Query to get all the documents 8817 List<SearchResult> searchResults = retrieveAllSearchResults( 8818 mDb1.search("", new SearchSpec.Builder() 8819 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 8820 .build())); 8821 assertThat(searchResults).hasSize(4); 8822 assertThat(searchResults.get(0).getGenericDocument().getSchemaType()) 8823 .isEqualTo("Message"); 8824 assertThat(searchResults.get(0).getParentTypeMap()) 8825 .isEqualTo(expectedMessageParentTypeMap); 8826 8827 assertThat(searchResults.get(1).getGenericDocument().getSchemaType()) 8828 .isEqualTo("Musician"); 8829 assertThat(searchResults.get(1).getParentTypeMap()) 8830 .isEqualTo(expectedMusicianParentTypeMap); 8831 8832 assertThat(searchResults.get(2).getGenericDocument().getSchemaType()) 8833 .isEqualTo("Artist"); 8834 assertThat(searchResults.get(2).getParentTypeMap()) 8835 .isEqualTo(expectedArtistParentTypeMap); 8836 8837 assertThat(searchResults.get(3).getGenericDocument().getSchemaType()) 8838 .isEqualTo("Person"); 8839 assertThat(searchResults.get(3).getParentTypeMap()) 8840 .isEqualTo(expectedPersonParentTypeMap); 8841 } 8842 8843 @Test testSimpleJoin()8844 public void testSimpleJoin() throws Exception { 8845 assumeTrue(mDb1.getFeatures() 8846 .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID)); 8847 8848 // A full example of how join might be used 8849 AppSearchSchema actionSchema = new AppSearchSchema.Builder("ViewAction") 8850 .addProperty(new StringPropertyConfig.Builder("entityId") 8851 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 8852 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 8853 .setJoinableValueType(StringPropertyConfig 8854 .JOINABLE_VALUE_TYPE_QUALIFIED_ID) 8855 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 8856 .build() 8857 ).addProperty(new StringPropertyConfig.Builder("note") 8858 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 8859 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 8860 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 8861 .build() 8862 ).build(); 8863 8864 // Schema registration 8865 mDb1.setSchemaAsync( 8866 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA, actionSchema) 8867 .build()).get(); 8868 8869 // Index a document 8870 // While inEmail2 has a higher document score, we will rank based on the number of joined 8871 // documents. inEmail1 will have 1 joined document while inEmail2 will have 0 joined 8872 // documents. 8873 AppSearchEmail inEmail = 8874 new AppSearchEmail.Builder("namespace", "id1") 8875 .setFrom("from@example.com") 8876 .setTo("to1@example.com", "to2@example.com") 8877 .setSubject("testPut example") 8878 .setBody("This is the body of the testPut email") 8879 .setScore(1) 8880 .build(); 8881 8882 AppSearchEmail inEmail2 = 8883 new AppSearchEmail.Builder("namespace", "id2") 8884 .setFrom("from@example.com") 8885 .setTo("to1@example.com", "to2@example.com") 8886 .setSubject("testPut example") 8887 .setBody("This is the body of the testPut email") 8888 .setScore(10) 8889 .build(); 8890 8891 String qualifiedId = DocumentIdUtil.createQualifiedId(mContext.getPackageName(), DB_NAME_1, 8892 "namespace", "id1"); 8893 GenericDocument viewAction1 = new GenericDocument.Builder<>("NS", "id3", "ViewAction") 8894 .setScore(1) 8895 .setPropertyString("entityId", qualifiedId) 8896 .setPropertyString("note", "Viewed email on Monday").build(); 8897 GenericDocument viewAction2 = new GenericDocument.Builder<>("NS", "id4", "ViewAction") 8898 .setScore(2) 8899 .setPropertyString("entityId", qualifiedId) 8900 .setPropertyString("note", "Viewed email on Tuesday").build(); 8901 checkIsBatchResultSuccess(mDb1.putAsync( 8902 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail, inEmail2, 8903 viewAction1, viewAction2) 8904 .build())); 8905 8906 SearchSpec nestedSearchSpec = 8907 new SearchSpec.Builder() 8908 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_DOCUMENT_SCORE) 8909 .setOrder(SearchSpec.ORDER_ASCENDING) 8910 .build(); 8911 8912 JoinSpec js = new JoinSpec.Builder("entityId") 8913 .setNestedSearch("", nestedSearchSpec) 8914 .setAggregationScoringStrategy(JoinSpec.AGGREGATION_SCORING_RESULT_COUNT) 8915 .setMaxJoinedResultCount(1) 8916 .build(); 8917 8918 SearchResults searchResults = mDb1.search("body email", new SearchSpec.Builder() 8919 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_JOIN_AGGREGATE_SCORE) 8920 .setJoinSpec(js) 8921 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 8922 .build()); 8923 8924 List<SearchResult> sr = searchResults.getNextPageAsync().get(); 8925 8926 // Both email docs are returned, but id1 comes first due to the join 8927 assertThat(sr).hasSize(2); 8928 8929 assertThat(sr.get(0).getGenericDocument().getId()).isEqualTo("id1"); 8930 assertThat(sr.get(0).getJoinedResults()).hasSize(1); 8931 assertThat(sr.get(0).getJoinedResults().get(0).getGenericDocument()).isEqualTo(viewAction1); 8932 // SearchSpec.Builder#setMaxJoinedResultCount only limits the number of child documents 8933 // returned. It does not affect the number of child documents that are scored. So the score 8934 // (the COUNT of the number of children) is 2, even though only one child is returned. 8935 assertThat(sr.get(0).getRankingSignal()).isEqualTo(2.0); 8936 8937 assertThat(sr.get(1).getGenericDocument().getId()).isEqualTo("id2"); 8938 assertThat(sr.get(1).getRankingSignal()).isEqualTo(0.0); 8939 assertThat(sr.get(1).getJoinedResults()).isEmpty(); 8940 } 8941 8942 @Test testJoin_unsupportedFeature_throwsException()8943 public void testJoin_unsupportedFeature_throwsException() throws Exception { 8944 assumeFalse(mDb1.getFeatures().isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID)); 8945 8946 SearchSpec nestedSearchSpec = new SearchSpec.Builder().build(); 8947 JoinSpec js = new JoinSpec.Builder("entityId").setNestedSearch("", nestedSearchSpec) 8948 .build(); 8949 Exception e = assertThrows(UnsupportedOperationException.class, () -> mDb1.search( 8950 /*queryExpression */ "", 8951 new SearchSpec.Builder() 8952 .setJoinSpec(js) 8953 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 8954 .build())); 8955 assertThat(e.getMessage()).isEqualTo("JoinSpec is not available on this AppSearch " 8956 + "implementation."); 8957 } 8958 8959 @Test testSearchSuggestion_notSupported()8960 public void testSearchSuggestion_notSupported() throws Exception { 8961 assumeFalse(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION)); 8962 8963 assertThrows(UnsupportedOperationException.class, () -> 8964 mDb1.searchSuggestionAsync( 8965 /*suggestionQueryExpression=*/"t", 8966 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/2).build()).get()); 8967 } 8968 8969 @Test testSearchSuggestion()8970 public void testSearchSuggestion() throws Exception { 8971 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION)); 8972 // Schema registration 8973 AppSearchSchema schema = new AppSearchSchema.Builder("Type").addProperty( 8974 new StringPropertyConfig.Builder("body") 8975 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 8976 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 8977 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 8978 .build()) 8979 .build(); 8980 mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get(); 8981 8982 // Index documents 8983 GenericDocument doc1 = new GenericDocument.Builder<>("namespace", "id1", "Type") 8984 .setPropertyString("body", "termOne termTwo termThree termFour") 8985 .build(); 8986 GenericDocument doc2 = new GenericDocument.Builder<>("namespace", "id2", "Type") 8987 .setPropertyString("body", "termOne termTwo termThree") 8988 .build(); 8989 GenericDocument doc3 = new GenericDocument.Builder<>("namespace", "id3", "Type") 8990 .setPropertyString("body", "termOne termTwo") 8991 .build(); 8992 GenericDocument doc4 = new GenericDocument.Builder<>("namespace", "id4", "Type") 8993 .setPropertyString("body", "termOne") 8994 .build(); 8995 8996 checkIsBatchResultSuccess(mDb1.putAsync( 8997 new PutDocumentsRequest.Builder().addGenericDocuments(doc1, doc2, doc3, doc4) 8998 .build())); 8999 9000 SearchSuggestionResult resultOne = 9001 new SearchSuggestionResult.Builder().setSuggestedResult("termone").build(); 9002 SearchSuggestionResult resultTwo = 9003 new SearchSuggestionResult.Builder().setSuggestedResult("termtwo").build(); 9004 SearchSuggestionResult resultThree = 9005 new SearchSuggestionResult.Builder().setSuggestedResult("termthree").build(); 9006 SearchSuggestionResult resultFour = 9007 new SearchSuggestionResult.Builder().setSuggestedResult("termfour").build(); 9008 9009 List<SearchSuggestionResult> suggestions = mDb1.searchSuggestionAsync( 9010 /*suggestionQueryExpression=*/"t", 9011 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get(); 9012 assertThat(suggestions).containsExactly(resultOne, resultTwo, resultThree, resultFour) 9013 .inOrder(); 9014 9015 // Query first 2 suggestions, and they will be ranked. 9016 suggestions = mDb1.searchSuggestionAsync( 9017 /*suggestionQueryExpression=*/"t", 9018 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/2).build()).get(); 9019 assertThat(suggestions).containsExactly(resultOne, resultTwo).inOrder(); 9020 } 9021 9022 @Test testSearchSuggestion_namespaceFilter()9023 public void testSearchSuggestion_namespaceFilter() throws Exception { 9024 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION)); 9025 // Schema registration 9026 AppSearchSchema schema = new AppSearchSchema.Builder("Type").addProperty( 9027 new StringPropertyConfig.Builder("body") 9028 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 9029 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 9030 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 9031 .build()) 9032 .build(); 9033 mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get(); 9034 9035 // Index documents 9036 GenericDocument doc1 = new GenericDocument.Builder<>("namespace1", "id1", "Type") 9037 .setPropertyString("body", "fo foo") 9038 .build(); 9039 GenericDocument doc2 = new GenericDocument.Builder<>("namespace2", "id2", "Type") 9040 .setPropertyString("body", "foo") 9041 .build(); 9042 GenericDocument doc3 = new GenericDocument.Builder<>("namespace3", "id3", "Type") 9043 .setPropertyString("body", "fool") 9044 .build(); 9045 9046 checkIsBatchResultSuccess(mDb1.putAsync( 9047 new PutDocumentsRequest.Builder().addGenericDocuments(doc1, doc2, doc3).build())); 9048 9049 SearchSuggestionResult resultFo = 9050 new SearchSuggestionResult.Builder().setSuggestedResult("fo").build(); 9051 SearchSuggestionResult resultFoo = 9052 new SearchSuggestionResult.Builder().setSuggestedResult("foo").build(); 9053 SearchSuggestionResult resultFool = 9054 new SearchSuggestionResult.Builder().setSuggestedResult("fool").build(); 9055 9056 // namespace1 has 2 results. 9057 List<SearchSuggestionResult> suggestions = mDb1.searchSuggestionAsync( 9058 /*suggestionQueryExpression=*/"f", 9059 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10) 9060 .addFilterNamespaces("namespace1").build()).get(); 9061 assertThat(suggestions).containsExactly(resultFoo, resultFo).inOrder(); 9062 9063 // namespace2 has 1 result. 9064 suggestions = mDb1.searchSuggestionAsync( 9065 /*suggestionQueryExpression=*/"f", 9066 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10) 9067 .addFilterNamespaces("namespace2").build()).get(); 9068 assertThat(suggestions).containsExactly(resultFoo).inOrder(); 9069 9070 // namespace2 and 3 has 2 results. 9071 suggestions = mDb1.searchSuggestionAsync( 9072 /*suggestionQueryExpression=*/"f", 9073 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10) 9074 .addFilterNamespaces("namespace2", "namespace3") 9075 .build()).get(); 9076 assertThat(suggestions).containsExactly(resultFoo, resultFool); 9077 9078 // non exist namespace has empty result 9079 suggestions = mDb1.searchSuggestionAsync( 9080 /*suggestionQueryExpression=*/"f", 9081 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10) 9082 .addFilterNamespaces("nonExistNamespace").build()).get(); 9083 assertThat(suggestions).isEmpty(); 9084 } 9085 9086 @Test testSearchSuggestion_documentIdFilter()9087 public void testSearchSuggestion_documentIdFilter() throws Exception { 9088 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION)); 9089 // Schema registration 9090 AppSearchSchema schema = new AppSearchSchema.Builder("Type").addProperty( 9091 new StringPropertyConfig.Builder("body") 9092 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 9093 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 9094 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 9095 .build()) 9096 .build(); 9097 mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get(); 9098 9099 // Index documents 9100 GenericDocument doc1 = new GenericDocument.Builder<>("namespace1", "id1", "Type") 9101 .setPropertyString("body", "termone") 9102 .build(); 9103 GenericDocument doc2 = new GenericDocument.Builder<>("namespace1", "id2", "Type") 9104 .setPropertyString("body", "termtwo") 9105 .build(); 9106 GenericDocument doc3 = new GenericDocument.Builder<>("namespace2", "id3", "Type") 9107 .setPropertyString("body", "termthree") 9108 .build(); 9109 GenericDocument doc4 = new GenericDocument.Builder<>("namespace2", "id4", "Type") 9110 .setPropertyString("body", "termfour") 9111 .build(); 9112 9113 checkIsBatchResultSuccess(mDb1.putAsync(new PutDocumentsRequest.Builder() 9114 .addGenericDocuments(doc1, doc2, doc3, doc4).build())); 9115 9116 SearchSuggestionResult resultOne = 9117 new SearchSuggestionResult.Builder().setSuggestedResult("termone").build(); 9118 SearchSuggestionResult resultTwo = 9119 new SearchSuggestionResult.Builder().setSuggestedResult("termtwo").build(); 9120 SearchSuggestionResult resultThree = 9121 new SearchSuggestionResult.Builder().setSuggestedResult("termthree").build(); 9122 SearchSuggestionResult resultFour = 9123 new SearchSuggestionResult.Builder().setSuggestedResult("termfour").build(); 9124 9125 // Only search for namespace1/doc1 9126 List<SearchSuggestionResult> suggestions = mDb1.searchSuggestionAsync( 9127 /*suggestionQueryExpression=*/"t", 9128 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10) 9129 .addFilterNamespaces("namespace1") 9130 .addFilterDocumentIds("namespace1", "id1") 9131 .build()).get(); 9132 assertThat(suggestions).containsExactly(resultOne); 9133 9134 // Only search for namespace1/doc1 and namespace1/doc2 9135 suggestions = mDb1.searchSuggestionAsync( 9136 /*suggestionQueryExpression=*/"t", 9137 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10) 9138 .addFilterNamespaces("namespace1") 9139 .addFilterDocumentIds("namespace1", ImmutableList.of("id1", "id2")) 9140 .build()).get(); 9141 assertThat(suggestions).containsExactly(resultOne, resultTwo); 9142 9143 // Only search for namespace1/doc1 and namespace2/doc3 9144 suggestions = mDb1.searchSuggestionAsync( 9145 /*suggestionQueryExpression=*/"t", 9146 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10) 9147 .addFilterNamespaces("namespace1", "namespace2") 9148 .addFilterDocumentIds("namespace1", "id1") 9149 .addFilterDocumentIds("namespace2", ImmutableList.of("id3")) 9150 .build()).get(); 9151 assertThat(suggestions).containsExactly(resultOne, resultThree); 9152 9153 // Only search for namespace1/doc1 and everything in namespace2 9154 suggestions = mDb1.searchSuggestionAsync( 9155 /*suggestionQueryExpression=*/"t", 9156 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10) 9157 .addFilterDocumentIds("namespace1", "id1") 9158 .build()).get(); 9159 assertThat(suggestions).containsExactly(resultOne, resultThree, resultFour); 9160 } 9161 9162 @Test testSearchSuggestion_schemaFilter()9163 public void testSearchSuggestion_schemaFilter() throws Exception { 9164 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION)); 9165 // Schema registration 9166 AppSearchSchema schemaType1 = new AppSearchSchema.Builder("Type1").addProperty( 9167 new StringPropertyConfig.Builder("body") 9168 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 9169 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 9170 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 9171 .build()) 9172 .build(); 9173 AppSearchSchema schemaType2 = new AppSearchSchema.Builder("Type2").addProperty( 9174 new StringPropertyConfig.Builder("body") 9175 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 9176 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 9177 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 9178 .build()) 9179 .build(); 9180 AppSearchSchema schemaType3 = new AppSearchSchema.Builder("Type3").addProperty( 9181 new StringPropertyConfig.Builder("body") 9182 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 9183 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 9184 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 9185 .build()) 9186 .build(); 9187 mDb1.setSchemaAsync(new SetSchemaRequest.Builder() 9188 .addSchemas(schemaType1, schemaType2, schemaType3).build()).get(); 9189 9190 // Index documents 9191 GenericDocument doc1 = new GenericDocument.Builder<>("namespace", "id1", "Type1") 9192 .setPropertyString("body", "fo foo") 9193 .build(); 9194 GenericDocument doc2 = new GenericDocument.Builder<>("namespace", "id2", "Type2") 9195 .setPropertyString("body", "foo") 9196 .build(); 9197 GenericDocument doc3 = new GenericDocument.Builder<>("namespace", "id3", "Type3") 9198 .setPropertyString("body", "fool") 9199 .build(); 9200 9201 checkIsBatchResultSuccess(mDb1.putAsync( 9202 new PutDocumentsRequest.Builder().addGenericDocuments(doc1, doc2, doc3).build())); 9203 9204 SearchSuggestionResult resultFo = 9205 new SearchSuggestionResult.Builder().setSuggestedResult("fo").build(); 9206 SearchSuggestionResult resultFoo = 9207 new SearchSuggestionResult.Builder().setSuggestedResult("foo").build(); 9208 SearchSuggestionResult resultFool = 9209 new SearchSuggestionResult.Builder().setSuggestedResult("fool").build(); 9210 9211 // Type1 has 2 results. 9212 List<SearchSuggestionResult> suggestions = mDb1.searchSuggestionAsync( 9213 /*suggestionQueryExpression=*/"f", 9214 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10) 9215 .addFilterSchemas("Type1").build()).get(); 9216 assertThat(suggestions).containsExactly(resultFoo, resultFo).inOrder(); 9217 9218 // Type2 has 1 result. 9219 suggestions = mDb1.searchSuggestionAsync( 9220 /*suggestionQueryExpression=*/"f", 9221 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10) 9222 .addFilterSchemas("Type2").build()).get(); 9223 assertThat(suggestions).containsExactly(resultFoo).inOrder(); 9224 9225 // Type2 and 3 has 2 results. 9226 suggestions = mDb1.searchSuggestionAsync( 9227 /*suggestionQueryExpression=*/"f", 9228 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10) 9229 .addFilterSchemas("Type2", "Type3") 9230 .build()).get(); 9231 assertThat(suggestions).containsExactly(resultFoo, resultFool); 9232 9233 // non exist type has empty result. 9234 suggestions = mDb1.searchSuggestionAsync( 9235 /*suggestionQueryExpression=*/"f", 9236 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10) 9237 .addFilterSchemas("nonExistType").build()).get(); 9238 assertThat(suggestions).isEmpty(); 9239 } 9240 9241 @Test testSearchSuggestion_differentPrefix()9242 public void testSearchSuggestion_differentPrefix() throws Exception { 9243 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION)); 9244 // Schema registration 9245 AppSearchSchema schema = new AppSearchSchema.Builder("Type").addProperty( 9246 new StringPropertyConfig.Builder("body") 9247 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 9248 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 9249 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 9250 .build()) 9251 .build(); 9252 mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get(); 9253 9254 // Index documents 9255 GenericDocument doc1 = new GenericDocument.Builder<>("namespace", "id1", "Type") 9256 .setPropertyString("body", "foo") 9257 .build(); 9258 GenericDocument doc2 = new GenericDocument.Builder<>("namespace", "id2", "Type") 9259 .setPropertyString("body", "fool") 9260 .build(); 9261 GenericDocument doc3 = new GenericDocument.Builder<>("namespace", "id3", "Type") 9262 .setPropertyString("body", "bar") 9263 .build(); 9264 GenericDocument doc4 = new GenericDocument.Builder<>("namespace", "id4", "Type") 9265 .setPropertyString("body", "baz") 9266 .build(); 9267 9268 checkIsBatchResultSuccess(mDb1.putAsync( 9269 new PutDocumentsRequest.Builder().addGenericDocuments(doc1, doc2, doc3, doc4) 9270 .build())); 9271 9272 SearchSuggestionResult resultFoo = 9273 new SearchSuggestionResult.Builder().setSuggestedResult("foo").build(); 9274 SearchSuggestionResult resultFool = 9275 new SearchSuggestionResult.Builder().setSuggestedResult("fool").build(); 9276 SearchSuggestionResult resultBar = 9277 new SearchSuggestionResult.Builder().setSuggestedResult("bar").build(); 9278 SearchSuggestionResult resultBaz = 9279 new SearchSuggestionResult.Builder().setSuggestedResult("baz").build(); 9280 9281 // prefix f has 2 results. 9282 List<SearchSuggestionResult> suggestions = mDb1.searchSuggestionAsync( 9283 /*suggestionQueryExpression=*/"f", 9284 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get(); 9285 assertThat(suggestions).containsExactly(resultFoo, resultFool); 9286 9287 // prefix b has 2 results. 9288 suggestions = mDb1.searchSuggestionAsync( 9289 /*suggestionQueryExpression=*/"b", 9290 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get(); 9291 assertThat(suggestions).containsExactly(resultBar, resultBaz); 9292 } 9293 9294 @Test testSearchSuggestion_differentRankingStrategy()9295 public void testSearchSuggestion_differentRankingStrategy() throws Exception { 9296 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION)); 9297 // Schema registration 9298 AppSearchSchema schema = new AppSearchSchema.Builder("Type").addProperty( 9299 new StringPropertyConfig.Builder("body") 9300 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 9301 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 9302 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 9303 .build()) 9304 .build(); 9305 mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get(); 9306 9307 // Index documents 9308 // term1 appears 3 times in all 3 docs. 9309 // term2 appears 4 times in 2 docs. 9310 // term3 appears 5 times in 1 doc. 9311 GenericDocument doc1 = new GenericDocument.Builder<>("namespace", "id1", "Type") 9312 .setPropertyString("body", "term1 term3 term3 term3 term3 term3") 9313 .build(); 9314 GenericDocument doc2 = new GenericDocument.Builder<>("namespace", "id2", "Type") 9315 .setPropertyString("body", "term1 term2 term2 term2") 9316 .build(); 9317 GenericDocument doc3 = new GenericDocument.Builder<>("namespace", "id3", "Type") 9318 .setPropertyString("body", "term1 term2") 9319 .build(); 9320 9321 checkIsBatchResultSuccess(mDb1.putAsync( 9322 new PutDocumentsRequest.Builder().addGenericDocuments(doc1, doc2, doc3) 9323 .build())); 9324 9325 SearchSuggestionResult result1 = 9326 new SearchSuggestionResult.Builder().setSuggestedResult("term1").build(); 9327 SearchSuggestionResult result2 = 9328 new SearchSuggestionResult.Builder().setSuggestedResult("term2").build(); 9329 SearchSuggestionResult result3 = 9330 new SearchSuggestionResult.Builder().setSuggestedResult("term3").build(); 9331 9332 9333 // rank by NONE, the order should be arbitrary but all terms appear. 9334 List<SearchSuggestionResult> suggestions = mDb1.searchSuggestionAsync( 9335 /*suggestionQueryExpression=*/"t", 9336 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10) 9337 .setRankingStrategy(SearchSuggestionSpec 9338 .SUGGESTION_RANKING_STRATEGY_NONE) 9339 .build()).get(); 9340 assertThat(suggestions).containsExactly(result2, result1, result3); 9341 9342 // rank by document count, the order should be term1:3 > term2:2 > term3:1 9343 suggestions = mDb1.searchSuggestionAsync( 9344 /*suggestionQueryExpression=*/"t", 9345 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10) 9346 .setRankingStrategy(SearchSuggestionSpec 9347 .SUGGESTION_RANKING_STRATEGY_DOCUMENT_COUNT) 9348 .build()).get(); 9349 assertThat(suggestions).containsExactly(result1, result2, result3).inOrder(); 9350 9351 // rank by term frequency, the order should be term3:5 > term2:4 > term1:3 9352 suggestions = mDb1.searchSuggestionAsync( 9353 /*suggestionQueryExpression=*/"t", 9354 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10) 9355 .setRankingStrategy(SearchSuggestionSpec 9356 .SUGGESTION_RANKING_STRATEGY_TERM_FREQUENCY) 9357 .build()).get(); 9358 assertThat(suggestions).containsExactly(result3, result2, result1).inOrder(); 9359 } 9360 9361 @Test testSearchSuggestion_removeDocument()9362 public void testSearchSuggestion_removeDocument() throws Exception { 9363 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION)); 9364 // Schema registration 9365 AppSearchSchema schema = new AppSearchSchema.Builder("Type").addProperty( 9366 new StringPropertyConfig.Builder("body") 9367 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 9368 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 9369 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 9370 .build()) 9371 .build(); 9372 mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get(); 9373 9374 // Index documents 9375 GenericDocument docTwo = new GenericDocument.Builder<>("namespace", "idTwo", "Type") 9376 .setPropertyString("body", "two") 9377 .build(); 9378 GenericDocument docThree = new GenericDocument.Builder<>("namespace", "idThree", "Type") 9379 .setPropertyString("body", "three") 9380 .build(); 9381 GenericDocument docTart = new GenericDocument.Builder<>("namespace", "idTart", "Type") 9382 .setPropertyString("body", "tart") 9383 .build(); 9384 9385 checkIsBatchResultSuccess(mDb1.putAsync( 9386 new PutDocumentsRequest.Builder().addGenericDocuments(docTwo, docThree, docTart) 9387 .build())); 9388 9389 SearchSuggestionResult resultTwo = 9390 new SearchSuggestionResult.Builder().setSuggestedResult("two").build(); 9391 SearchSuggestionResult resultThree = 9392 new SearchSuggestionResult.Builder().setSuggestedResult("three").build(); 9393 SearchSuggestionResult resultTart = 9394 new SearchSuggestionResult.Builder().setSuggestedResult("tart").build(); 9395 9396 // prefix t has 3 results. 9397 List<SearchSuggestionResult> suggestions = mDb1.searchSuggestionAsync( 9398 /*suggestionQueryExpression=*/"t", 9399 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get(); 9400 assertThat(suggestions).containsExactly(resultTwo, resultThree, resultTart); 9401 9402 // Delete the document 9403 checkIsBatchResultSuccess(mDb1.removeAsync( 9404 new RemoveByDocumentIdRequest.Builder("namespace").addIds( 9405 "idTwo").build())); 9406 9407 // now prefix t has 2 results. 9408 suggestions = mDb1.searchSuggestionAsync( 9409 /*suggestionQueryExpression=*/"t", 9410 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get(); 9411 assertThat(suggestions).containsExactly(resultThree, resultTart); 9412 } 9413 9414 @Test testSearchSuggestion_replacementDocument()9415 public void testSearchSuggestion_replacementDocument() throws Exception { 9416 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION)); 9417 // Schema registration 9418 AppSearchSchema schema = new AppSearchSchema.Builder("Type").addProperty( 9419 new StringPropertyConfig.Builder("body") 9420 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 9421 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 9422 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 9423 .build()) 9424 .build(); 9425 mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get(); 9426 9427 // Index documents 9428 GenericDocument doc = new GenericDocument.Builder<>("namespace", "id", "Type") 9429 .setPropertyString("body", "two three tart") 9430 .build(); 9431 9432 checkIsBatchResultSuccess(mDb1.putAsync( 9433 new PutDocumentsRequest.Builder().addGenericDocuments(doc) 9434 .build())); 9435 9436 SearchSuggestionResult resultTwo = 9437 new SearchSuggestionResult.Builder().setSuggestedResult("two").build(); 9438 SearchSuggestionResult resultThree = 9439 new SearchSuggestionResult.Builder().setSuggestedResult("three").build(); 9440 SearchSuggestionResult resultTart = 9441 new SearchSuggestionResult.Builder().setSuggestedResult("tart").build(); 9442 SearchSuggestionResult resultTwist = 9443 new SearchSuggestionResult.Builder().setSuggestedResult("twist").build(); 9444 9445 // prefix t has 3 results. 9446 List<SearchSuggestionResult> suggestions = mDb1.searchSuggestionAsync( 9447 /*suggestionQueryExpression=*/"t", 9448 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get(); 9449 assertThat(suggestions).containsExactly(resultTwo, resultThree, resultTart); 9450 9451 // replace the document 9452 GenericDocument replaceDoc = new GenericDocument.Builder<>("namespace", "id", "Type") 9453 .setPropertyString("body", "twist three") 9454 .build(); 9455 checkIsBatchResultSuccess(mDb1.putAsync( 9456 new PutDocumentsRequest.Builder().addGenericDocuments(replaceDoc) 9457 .build())); 9458 9459 // prefix t has 2 results for now. 9460 suggestions = mDb1.searchSuggestionAsync( 9461 /*suggestionQueryExpression=*/"t", 9462 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get(); 9463 assertThat(suggestions).containsExactly(resultThree, resultTwist); 9464 } 9465 9466 @Test testSearchSuggestion_twoInstances()9467 public void testSearchSuggestion_twoInstances() throws Exception { 9468 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION)); 9469 // Schema registration 9470 AppSearchSchema schema = new AppSearchSchema.Builder("Type").addProperty( 9471 new StringPropertyConfig.Builder("body") 9472 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 9473 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 9474 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 9475 .build()) 9476 .build(); 9477 mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get(); 9478 mDb2.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get(); 9479 9480 // Index documents to database 1. 9481 GenericDocument doc1 = new GenericDocument.Builder<>("namespace", "id1", "Type") 9482 .setPropertyString("body", "termOne termTwo") 9483 .build(); 9484 GenericDocument doc2 = new GenericDocument.Builder<>("namespace", "id2", "Type") 9485 .setPropertyString("body", "termOne") 9486 .build(); 9487 checkIsBatchResultSuccess(mDb1.putAsync( 9488 new PutDocumentsRequest.Builder().addGenericDocuments(doc1, doc2) 9489 .build())); 9490 9491 SearchSuggestionResult resultOne = 9492 new SearchSuggestionResult.Builder().setSuggestedResult("termone").build(); 9493 SearchSuggestionResult resultTwo = 9494 new SearchSuggestionResult.Builder().setSuggestedResult("termtwo").build(); 9495 9496 // database 1 could get suggestion results 9497 List<SearchSuggestionResult> suggestions = mDb1.searchSuggestionAsync( 9498 /*suggestionQueryExpression=*/"t", 9499 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get(); 9500 assertThat(suggestions).containsExactly(resultOne, resultTwo).inOrder(); 9501 9502 // database 2 couldn't get suggestion results 9503 suggestions = mDb2.searchSuggestionAsync( 9504 /*suggestionQueryExpression=*/"t", 9505 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get(); 9506 assertThat(suggestions).isEmpty(); 9507 } 9508 9509 @Test testSearchSuggestion_multipleTerms()9510 public void testSearchSuggestion_multipleTerms() throws Exception { 9511 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION)); 9512 // Schema registration 9513 AppSearchSchema schema = new AppSearchSchema.Builder("Type").addProperty( 9514 new StringPropertyConfig.Builder("body") 9515 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 9516 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 9517 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 9518 .build()) 9519 .build(); 9520 mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get(); 9521 9522 // Index documents 9523 GenericDocument doc1 = new GenericDocument.Builder<>("namespace", "id1", "Type") 9524 .setPropertyString("body", "bar fo") 9525 .build(); 9526 GenericDocument doc2 = new GenericDocument.Builder<>("namespace", "id2", "Type") 9527 .setPropertyString("body", "cat foo") 9528 .build(); 9529 GenericDocument doc3 = new GenericDocument.Builder<>("namespace", "id3", "Type") 9530 .setPropertyString("body", "fool") 9531 .build(); 9532 checkIsBatchResultSuccess(mDb1.putAsync( 9533 new PutDocumentsRequest.Builder().addGenericDocuments(doc1, doc2, doc3) 9534 .build())); 9535 9536 // Search "bar AND f" only document 1 should match the search. 9537 List<SearchSuggestionResult> suggestions = mDb1.searchSuggestionAsync( 9538 /*suggestionQueryExpression=*/"bar f", 9539 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get(); 9540 SearchSuggestionResult barFo = 9541 new SearchSuggestionResult.Builder().setSuggestedResult("bar fo").build(); 9542 assertThat(suggestions).containsExactly(barFo); 9543 9544 // Search for "(bar OR cat) AND f" both document1 "bar fo" and document2 "cat foo" could 9545 // match. 9546 suggestions = mDb1.searchSuggestionAsync( 9547 /*suggestionQueryExpression=*/"bar OR cat f", 9548 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get(); 9549 SearchSuggestionResult barCatFo = 9550 new SearchSuggestionResult.Builder().setSuggestedResult("bar OR cat fo").build(); 9551 SearchSuggestionResult barCatFoo = 9552 new SearchSuggestionResult.Builder().setSuggestedResult("bar OR cat foo").build(); 9553 assertThat(suggestions).containsExactly(barCatFo, barCatFoo); 9554 9555 // Search for "(bar AND cat) OR f", all documents could match. 9556 suggestions = mDb1.searchSuggestionAsync( 9557 /*suggestionQueryExpression=*/"(bar cat) OR f", 9558 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get(); 9559 SearchSuggestionResult barCatOrFo = 9560 new SearchSuggestionResult.Builder().setSuggestedResult("(bar cat) OR fo").build(); 9561 SearchSuggestionResult barCatOrFoo = 9562 new SearchSuggestionResult.Builder().setSuggestedResult("(bar cat) OR foo").build(); 9563 SearchSuggestionResult barCatOrFool = 9564 new SearchSuggestionResult.Builder() 9565 .setSuggestedResult("(bar cat) OR fool").build(); 9566 assertThat(suggestions).containsExactly(barCatOrFo, barCatOrFoo, barCatOrFool); 9567 9568 // Search for "-bar f", document2 "cat foo" could and document3 "fool" could match. 9569 suggestions = mDb1.searchSuggestionAsync( 9570 /*suggestionQueryExpression=*/"-bar f", 9571 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get(); 9572 SearchSuggestionResult noBarFoo = 9573 new SearchSuggestionResult.Builder().setSuggestedResult("-bar foo").build(); 9574 SearchSuggestionResult noBarFool = 9575 new SearchSuggestionResult.Builder().setSuggestedResult("-bar fool").build(); 9576 assertThat(suggestions).containsExactly(noBarFoo, noBarFool); 9577 } 9578 9579 @Test testSearchSuggestion_propertyFilter()9580 public void testSearchSuggestion_propertyFilter() throws Exception { 9581 assumeTrue(mDb1.getFeatures().isFeatureSupported( 9582 Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES)); 9583 // Schema registration 9584 AppSearchSchema schemaType1 = 9585 new AppSearchSchema.Builder("Type1") 9586 .addProperty( 9587 new StringPropertyConfig.Builder("propertyone") 9588 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 9589 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 9590 .setIndexingType( 9591 StringPropertyConfig.INDEXING_TYPE_PREFIXES) 9592 .build()) 9593 .addProperty( 9594 new StringPropertyConfig.Builder("propertytwo") 9595 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 9596 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 9597 .setIndexingType( 9598 StringPropertyConfig.INDEXING_TYPE_PREFIXES) 9599 .build()) 9600 .build(); 9601 AppSearchSchema schemaType2 = 9602 new AppSearchSchema.Builder("Type2") 9603 .addProperty( 9604 new StringPropertyConfig.Builder("propertythree") 9605 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 9606 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 9607 .setIndexingType( 9608 StringPropertyConfig.INDEXING_TYPE_PREFIXES) 9609 .build()) 9610 .addProperty( 9611 new StringPropertyConfig.Builder("propertyfour") 9612 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 9613 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 9614 .setIndexingType( 9615 StringPropertyConfig.INDEXING_TYPE_PREFIXES) 9616 .build()) 9617 .build(); 9618 mDb1.setSchemaAsync( 9619 new SetSchemaRequest.Builder().addSchemas(schemaType1, schemaType2).build()) 9620 .get(); 9621 9622 // Index documents 9623 GenericDocument doc1 = 9624 new GenericDocument.Builder<>("namespace", "id1", "Type1") 9625 .setPropertyString("propertyone", "termone") 9626 .setPropertyString("propertytwo", "termtwo") 9627 .build(); 9628 GenericDocument doc2 = 9629 new GenericDocument.Builder<>("namespace", "id2", "Type2") 9630 .setPropertyString("propertythree", "termthree") 9631 .setPropertyString("propertyfour", "termfour") 9632 .build(); 9633 9634 checkIsBatchResultSuccess( 9635 mDb1.putAsync( 9636 new PutDocumentsRequest.Builder().addGenericDocuments(doc1, doc2).build())); 9637 9638 SearchSuggestionResult resultOne = 9639 new SearchSuggestionResult.Builder().setSuggestedResult("termone").build(); 9640 SearchSuggestionResult resultTwo = 9641 new SearchSuggestionResult.Builder().setSuggestedResult("termtwo").build(); 9642 SearchSuggestionResult resultThree = 9643 new SearchSuggestionResult.Builder().setSuggestedResult("termthree").build(); 9644 SearchSuggestionResult resultFour = 9645 new SearchSuggestionResult.Builder().setSuggestedResult("termfour").build(); 9646 9647 // Only search for type1/propertyone 9648 List<SearchSuggestionResult> suggestions = 9649 mDb1.searchSuggestionAsync( 9650 /* suggestionQueryExpression= */ "t", 9651 new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10) 9652 .addFilterSchemas("Type1") 9653 .addFilterProperties( 9654 "Type1", ImmutableList.of("propertyone")) 9655 .build()) 9656 .get(); 9657 assertThat(suggestions).containsExactly(resultOne); 9658 9659 // Only search for type1/propertyone and type1/propertytwo 9660 suggestions = 9661 mDb1.searchSuggestionAsync( 9662 /* suggestionQueryExpression= */ "t", 9663 new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10) 9664 .addFilterSchemas("Type1") 9665 .addFilterProperties( 9666 "Type1", 9667 ImmutableList.of("propertyone", "propertytwo")) 9668 .build()) 9669 .get(); 9670 assertThat(suggestions).containsExactly(resultOne, resultTwo); 9671 9672 // Only search for type1/propertyone and type2/propertythree 9673 suggestions = 9674 mDb1.searchSuggestionAsync( 9675 /* suggestionQueryExpression= */ "t", 9676 new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10) 9677 .addFilterSchemas("Type1", "Type2") 9678 .addFilterProperties( 9679 "Type1", ImmutableList.of("propertyone")) 9680 .addFilterProperties( 9681 "Type2", ImmutableList.of("propertythree")) 9682 .build()) 9683 .get(); 9684 assertThat(suggestions).containsExactly(resultOne, resultThree); 9685 9686 // Only search for type1/propertyone and type2/propertyfour, in addFilterPropertyPaths 9687 suggestions = 9688 mDb1.searchSuggestionAsync( 9689 /* suggestionQueryExpression= */ "t", 9690 new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10) 9691 .addFilterSchemas("Type1", "Type2") 9692 .addFilterProperties( 9693 "Type1", ImmutableList.of("propertyone")) 9694 .addFilterPropertyPaths( 9695 "Type2", 9696 ImmutableList.of(new PropertyPath("propertyfour"))) 9697 .build()) 9698 .get(); 9699 assertThat(suggestions).containsExactly(resultOne, resultFour); 9700 9701 // Only search for type1/propertyone and everything in type2 9702 suggestions = 9703 mDb1.searchSuggestionAsync( 9704 /* suggestionQueryExpression= */ "t", 9705 new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10) 9706 .addFilterProperties( 9707 "Type1", ImmutableList.of("propertyone")) 9708 .build()) 9709 .get(); 9710 assertThat(suggestions).containsExactly(resultOne, resultThree, resultFour); 9711 } 9712 9713 @Test testSearchSuggestion_propertyFilter_notSupported()9714 public void testSearchSuggestion_propertyFilter_notSupported() throws Exception { 9715 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION)); 9716 assumeFalse(mDb1.getFeatures().isFeatureSupported( 9717 Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES)); 9718 9719 SearchSuggestionSpec searchSuggestionSpec = 9720 new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10) 9721 .addFilterSchemas("Type1") 9722 .addFilterProperties("Type1", ImmutableList.of("property")) 9723 .build(); 9724 9725 // Search suggest with type property filters {"Email", ["property"]} and verify that 9726 // unsupported exception is thrown 9727 UnsupportedOperationException exception = 9728 assertThrows(UnsupportedOperationException.class, 9729 () -> mDb1.searchSuggestionAsync( 9730 /* suggestionQueryExpression= */ "t", searchSuggestionSpec).get()); 9731 assertThat(exception).hasMessageThat().contains(Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES 9732 + " is not available on this AppSearch implementation."); 9733 } 9734 9735 @Test testSearchSuggestion_PropertyRestriction()9736 public void testSearchSuggestion_PropertyRestriction() throws Exception { 9737 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION)); 9738 // Schema registration 9739 AppSearchSchema schema = new AppSearchSchema.Builder("Type") 9740 .addProperty(new StringPropertyConfig.Builder("subject") 9741 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 9742 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 9743 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 9744 .build()) 9745 .addProperty(new StringPropertyConfig.Builder("body") 9746 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 9747 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 9748 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 9749 .build()) 9750 .build(); 9751 mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get(); 9752 9753 // Index documents 9754 GenericDocument doc1 = new GenericDocument.Builder<>("namespace", "id1", "Type") 9755 .setPropertyString("subject", "bar fo") 9756 .setPropertyString("body", "fool") 9757 .build(); 9758 GenericDocument doc2 = new GenericDocument.Builder<>("namespace", "id2", "Type") 9759 .setPropertyString("subject", "bar cat foo") 9760 .setPropertyString("body", "fool") 9761 .build(); 9762 GenericDocument doc3 = new GenericDocument.Builder<>("namespace", "ide", "Type") 9763 .setPropertyString("subject", "fool") 9764 .setPropertyString("body", "fool") 9765 .build(); 9766 checkIsBatchResultSuccess(mDb1.putAsync( 9767 new PutDocumentsRequest.Builder().addGenericDocuments(doc1, doc2, doc3) 9768 .build())); 9769 9770 // Search for "bar AND subject:f" 9771 List<SearchSuggestionResult> suggestions = mDb1.searchSuggestionAsync( 9772 /*suggestionQueryExpression=*/"bar subject:f", 9773 new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get(); 9774 SearchSuggestionResult barSubjectFo = 9775 new SearchSuggestionResult.Builder().setSuggestedResult("bar subject:fo").build(); 9776 SearchSuggestionResult barSubjectFoo = 9777 new SearchSuggestionResult.Builder().setSuggestedResult("bar subject:foo").build(); 9778 assertThat(suggestions).containsExactly(barSubjectFo, barSubjectFoo); 9779 } 9780 9781 @Test testGetSchema_parentTypes()9782 public void testGetSchema_parentTypes() throws Exception { 9783 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE)); 9784 AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email").build(); 9785 AppSearchSchema messageSchema = new AppSearchSchema.Builder("Message").build(); 9786 AppSearchSchema emailMessageSchema = 9787 new AppSearchSchema.Builder("EmailMessage") 9788 .addProperty( 9789 new StringPropertyConfig.Builder("sender") 9790 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 9791 .build()) 9792 .addProperty( 9793 new StringPropertyConfig.Builder("email") 9794 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 9795 .build()) 9796 .addProperty( 9797 new StringPropertyConfig.Builder("content") 9798 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 9799 .build()) 9800 .addParentType("Email") 9801 .addParentType("Message") 9802 .build(); 9803 9804 SetSchemaRequest request = 9805 new SetSchemaRequest.Builder() 9806 .addSchemas(emailMessageSchema) 9807 .addSchemas(emailSchema) 9808 .addSchemas(messageSchema) 9809 .build(); 9810 9811 mDb1.setSchemaAsync(request).get(); 9812 9813 Set<AppSearchSchema> actual = mDb1.getSchemaAsync().get().getSchemas(); 9814 assertThat(actual).hasSize(3); 9815 assertThat(actual).isEqualTo(request.getSchemas()); 9816 9817 // Check that calling getParentType() for the EmailMessage schema returns Email and Message 9818 for (AppSearchSchema schema : actual) { 9819 if (schema.getSchemaType().equals("EmailMessage")) { 9820 assertThat(schema.getParentTypes()).containsExactly("Email", "Message"); 9821 } 9822 } 9823 } 9824 9825 @Test testGetSchema_parentTypes_notSupported()9826 public void testGetSchema_parentTypes_notSupported() throws Exception { 9827 assumeFalse(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE)); 9828 AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email").build(); 9829 AppSearchSchema messageSchema = new AppSearchSchema.Builder("Message").build(); 9830 AppSearchSchema emailMessageSchema = 9831 new AppSearchSchema.Builder("EmailMessage") 9832 .addParentType("Email") 9833 .addParentType("Message") 9834 .build(); 9835 9836 SetSchemaRequest request = 9837 new SetSchemaRequest.Builder() 9838 .addSchemas(emailMessageSchema) 9839 .addSchemas(emailSchema) 9840 .addSchemas(messageSchema) 9841 .build(); 9842 9843 UnsupportedOperationException e = 9844 assertThrows( 9845 UnsupportedOperationException.class, 9846 () -> mDb1.setSchemaAsync(request).get()); 9847 assertThat(e) 9848 .hasMessageThat() 9849 .contains( 9850 Features.SCHEMA_ADD_PARENT_TYPE 9851 + " is not available on this AppSearch implementation."); 9852 } 9853 9854 @Test testGetSchema_indexableNestedPropsList()9855 public void testGetSchema_indexableNestedPropsList() throws Exception { 9856 assumeTrue( 9857 mDb1.getFeatures() 9858 .isFeatureSupported(Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES)); 9859 9860 AppSearchSchema personSchema = 9861 new AppSearchSchema.Builder("Person") 9862 .addProperty( 9863 new StringPropertyConfig.Builder("name") 9864 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 9865 .setIndexingType( 9866 StringPropertyConfig.INDEXING_TYPE_PREFIXES) 9867 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 9868 .build()) 9869 .addProperty( 9870 new AppSearchSchema.DocumentPropertyConfig.Builder( 9871 "worksFor", "Organization") 9872 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 9873 .setShouldIndexNestedProperties(false) 9874 .addIndexableNestedProperties(Collections.singleton("name")) 9875 .build()) 9876 .build(); 9877 AppSearchSchema organizationSchema = 9878 new AppSearchSchema.Builder("Organization") 9879 .addProperty( 9880 new StringPropertyConfig.Builder("name") 9881 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 9882 .setIndexingType( 9883 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 9884 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 9885 .build()) 9886 .addProperty( 9887 new StringPropertyConfig.Builder("notes") 9888 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 9889 .setIndexingType( 9890 StringPropertyConfig.INDEXING_TYPE_PREFIXES) 9891 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 9892 .build()) 9893 .build(); 9894 9895 SetSchemaRequest setSchemaRequest = 9896 new SetSchemaRequest.Builder() 9897 .addSchemas(personSchema, organizationSchema) 9898 .build(); 9899 mDb1.setSchemaAsync(setSchemaRequest).get(); 9900 9901 Set<AppSearchSchema> actual = mDb1.getSchemaAsync().get().getSchemas(); 9902 assertThat(actual).hasSize(2); 9903 assertThat(actual).isEqualTo(setSchemaRequest.getSchemas()); 9904 9905 for (AppSearchSchema schema : actual) { 9906 if (schema.getSchemaType().equals("Person")) { 9907 for (PropertyConfig property : schema.getProperties()) { 9908 if (property.getName().equals("worksFor")) { 9909 assertThat( 9910 ((DocumentPropertyConfig) property) 9911 .getIndexableNestedProperties()).containsExactly("name"); 9912 } 9913 } 9914 } 9915 } 9916 } 9917 9918 @Test testSetSchema_dataTypeIncompatibleWithParentTypes()9919 public void testSetSchema_dataTypeIncompatibleWithParentTypes() throws Exception { 9920 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE)); 9921 AppSearchSchema messageSchema = 9922 new AppSearchSchema.Builder("Message") 9923 .addProperty( 9924 new AppSearchSchema.LongPropertyConfig.Builder("sender") 9925 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 9926 .build()) 9927 .build(); 9928 AppSearchSchema emailSchema = 9929 new AppSearchSchema.Builder("Email") 9930 .addParentType("Message") 9931 .addProperty( 9932 new StringPropertyConfig.Builder("sender") 9933 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 9934 .build()) 9935 .build(); 9936 9937 SetSchemaRequest request = 9938 new SetSchemaRequest.Builder() 9939 .addSchemas(messageSchema) 9940 .addSchemas(emailSchema) 9941 .build(); 9942 9943 ExecutionException executionException = 9944 assertThrows(ExecutionException.class, () -> mDb1.setSchemaAsync(request).get()); 9945 assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class); 9946 AppSearchException exception = (AppSearchException) executionException.getCause(); 9947 assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT); 9948 assertThat(exception) 9949 .hasMessageThat() 9950 .containsMatch( 9951 "Property sender from child type .*\\$/Email is not compatible" 9952 + " to the parent type .*\\$/Message."); 9953 } 9954 9955 @Test testSetSchema_documentTypeIncompatibleWithParentTypes()9956 public void testSetSchema_documentTypeIncompatibleWithParentTypes() throws Exception { 9957 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE)); 9958 AppSearchSchema personSchema = new AppSearchSchema.Builder("Person").build(); 9959 AppSearchSchema artistSchema = 9960 new AppSearchSchema.Builder("Artist").addParentType("Person").build(); 9961 AppSearchSchema messageSchema = 9962 new AppSearchSchema.Builder("Message") 9963 .addProperty( 9964 new AppSearchSchema.DocumentPropertyConfig.Builder( 9965 "sender", "Artist") 9966 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 9967 .build()) 9968 .build(); 9969 AppSearchSchema emailSchema = 9970 new AppSearchSchema.Builder("Email") 9971 .addParentType("Message") 9972 // "sender" is defined as an Artist in the parent type Message, which 9973 // requires "sender"'s type here to be a subtype of Artist. Thus, this is 9974 // incompatible because Person is not a subtype of Artist. 9975 .addProperty( 9976 new AppSearchSchema.DocumentPropertyConfig.Builder( 9977 "sender", "Person") 9978 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 9979 .build()) 9980 .build(); 9981 9982 SetSchemaRequest request = 9983 new SetSchemaRequest.Builder() 9984 .addSchemas(personSchema) 9985 .addSchemas(artistSchema) 9986 .addSchemas(messageSchema) 9987 .addSchemas(emailSchema) 9988 .build(); 9989 9990 ExecutionException executionException = 9991 assertThrows(ExecutionException.class, () -> mDb1.setSchemaAsync(request).get()); 9992 assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class); 9993 AppSearchException exception = (AppSearchException) executionException.getCause(); 9994 assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT); 9995 assertThat(exception) 9996 .hasMessageThat() 9997 .containsMatch( 9998 "Property sender from child type .*\\$/Email is not compatible" 9999 + " to the parent type .*\\$/Message."); 10000 } 10001 10002 @Test testSetSchema_compatibleWithParentTypes()10003 public void testSetSchema_compatibleWithParentTypes() throws Exception { 10004 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE)); 10005 AppSearchSchema personSchema = new AppSearchSchema.Builder("Person").build(); 10006 AppSearchSchema artistSchema = 10007 new AppSearchSchema.Builder("Artist").addParentType("Person").build(); 10008 AppSearchSchema messageSchema = 10009 new AppSearchSchema.Builder("Message") 10010 .addProperty( 10011 new AppSearchSchema.DocumentPropertyConfig.Builder( 10012 "sender", "Person") 10013 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 10014 .build()) 10015 .addProperty( 10016 new StringPropertyConfig.Builder("note") 10017 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 10018 .setIndexingType( 10019 StringPropertyConfig.INDEXING_TYPE_PREFIXES) 10020 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 10021 .build()) 10022 .build(); 10023 AppSearchSchema emailSchema = 10024 new AppSearchSchema.Builder("Email") 10025 .addParentType("Message") 10026 .addProperty( 10027 // Artist is a subtype of Person, so compatible 10028 new AppSearchSchema.DocumentPropertyConfig.Builder( 10029 "sender", "Artist") 10030 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 10031 .build()) 10032 .addProperty( 10033 new StringPropertyConfig.Builder("note") 10034 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 10035 // A different indexing or tokenizer type is ok. 10036 .setIndexingType( 10037 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 10038 .setTokenizerType( 10039 StringPropertyConfig.TOKENIZER_TYPE_VERBATIM) 10040 .build()) 10041 .build(); 10042 10043 SetSchemaRequest request = 10044 new SetSchemaRequest.Builder() 10045 .addSchemas(personSchema) 10046 .addSchemas(artistSchema) 10047 .addSchemas(messageSchema) 10048 .addSchemas(emailSchema) 10049 .build(); 10050 10051 mDb1.setSchemaAsync(request).get(); 10052 10053 Set<AppSearchSchema> actual = mDb1.getSchemaAsync().get().getSchemas(); 10054 assertThat(actual).hasSize(4); 10055 assertThat(actual).isEqualTo(request.getSchemas()); 10056 } 10057 10058 @Test testSetSchema_indexableNestedPropsList()10059 public void testSetSchema_indexableNestedPropsList() throws Exception { 10060 assumeTrue( 10061 mDb1.getFeatures() 10062 .isFeatureSupported(Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES)); 10063 10064 AppSearchSchema personSchema = 10065 new AppSearchSchema.Builder("Person") 10066 .addProperty( 10067 new StringPropertyConfig.Builder("name") 10068 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 10069 .setIndexingType( 10070 StringPropertyConfig.INDEXING_TYPE_PREFIXES) 10071 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 10072 .build()) 10073 .addProperty( 10074 new AppSearchSchema.DocumentPropertyConfig.Builder( 10075 "worksFor", "Organization") 10076 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 10077 .setShouldIndexNestedProperties(false) 10078 .addIndexableNestedProperties(Collections.singleton("name")) 10079 .build()) 10080 .build(); 10081 AppSearchSchema organizationSchema = 10082 new AppSearchSchema.Builder("Organization") 10083 .addProperty( 10084 new StringPropertyConfig.Builder("name") 10085 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 10086 .setIndexingType( 10087 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 10088 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 10089 .build()) 10090 .addProperty( 10091 new StringPropertyConfig.Builder("notes") 10092 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 10093 .setIndexingType( 10094 StringPropertyConfig.INDEXING_TYPE_PREFIXES) 10095 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 10096 .build()) 10097 .build(); 10098 10099 mDb1.setSchemaAsync( 10100 new SetSchemaRequest.Builder() 10101 .addSchemas(personSchema, organizationSchema) 10102 .build()) 10103 .get(); 10104 10105 // Test that properties in Person's indexable_nested_properties_list are indexed and 10106 // searchable 10107 GenericDocument org1 = 10108 new GenericDocument.Builder<>("namespace", "org1", "Organization") 10109 .setPropertyString("name", "Org1") 10110 .setPropertyString("notes", "Some notes") 10111 .build(); 10112 GenericDocument person1 = 10113 new GenericDocument.Builder<>("namespace", "person1", "Person") 10114 .setPropertyString("name", "Jane") 10115 .setPropertyDocument("worksFor", org1) 10116 .build(); 10117 10118 AppSearchBatchResult<String, Void> putResult = 10119 checkIsBatchResultSuccess( 10120 mDb1.putAsync( 10121 new PutDocumentsRequest.Builder() 10122 .addGenericDocuments(person1, org1) 10123 .build())); 10124 assertThat(putResult.getSuccesses()).containsExactly("person1", null, "org1", null); 10125 assertThat(putResult.getFailures()).isEmpty(); 10126 10127 GetByDocumentIdRequest getByDocumentIdRequest = 10128 new GetByDocumentIdRequest.Builder("namespace").addIds("person1", "org1").build(); 10129 List<GenericDocument> outDocuments = doGet(mDb1, getByDocumentIdRequest); 10130 assertThat(outDocuments).hasSize(2); 10131 assertThat(outDocuments).containsExactly(person1, org1); 10132 10133 // Both org1 and person should be returned for query "Org1" 10134 // For org1 this matches the 'name' property and for person1 this matches the 10135 // 'worksFor.name' property. 10136 SearchResults searchResults = 10137 mDb1.search( 10138 "Org1", 10139 new SearchSpec.Builder() 10140 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 10141 .build()); 10142 outDocuments = convertSearchResultsToDocuments(searchResults); 10143 assertThat(outDocuments).hasSize(2); 10144 assertThat(outDocuments).containsExactly(person1, org1); 10145 10146 // Only org1 should be returned for query "notes", since 'worksFor.notes' is not indexed 10147 // for the Person-type. 10148 searchResults = 10149 mDb1.search( 10150 "notes", 10151 new SearchSpec.Builder() 10152 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 10153 .build()); 10154 outDocuments = convertSearchResultsToDocuments(searchResults); 10155 assertThat(outDocuments).hasSize(1); 10156 assertThat(outDocuments).containsExactly(org1); 10157 } 10158 10159 @Test testSetSchema_indexableNestedPropsList_notSupported()10160 public void testSetSchema_indexableNestedPropsList_notSupported() throws Exception { 10161 assumeFalse( 10162 mDb1.getFeatures() 10163 .isFeatureSupported(Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES)); 10164 10165 AppSearchSchema personSchema = 10166 new AppSearchSchema.Builder("Person") 10167 .addProperty( 10168 new StringPropertyConfig.Builder("name") 10169 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 10170 .setIndexingType( 10171 StringPropertyConfig.INDEXING_TYPE_PREFIXES) 10172 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 10173 .build()) 10174 .addProperty( 10175 new AppSearchSchema.DocumentPropertyConfig.Builder( 10176 "worksFor", "Organization") 10177 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 10178 .setShouldIndexNestedProperties(false) 10179 .addIndexableNestedProperties(Collections.singleton("name")) 10180 .build()) 10181 .build(); 10182 AppSearchSchema organizationSchema = 10183 new AppSearchSchema.Builder("Organization") 10184 .addProperty( 10185 new StringPropertyConfig.Builder("name") 10186 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 10187 .setIndexingType( 10188 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 10189 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 10190 .build()) 10191 .addProperty( 10192 new StringPropertyConfig.Builder("notes") 10193 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 10194 .setIndexingType( 10195 StringPropertyConfig.INDEXING_TYPE_PREFIXES) 10196 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 10197 .build()) 10198 .build(); 10199 10200 SetSchemaRequest setSchemaRequest = 10201 new SetSchemaRequest.Builder().addSchemas(personSchema, organizationSchema).build(); 10202 UnsupportedOperationException e = 10203 assertThrows( 10204 UnsupportedOperationException.class, 10205 () -> mDb1.setSchemaAsync(setSchemaRequest).get()); 10206 assertThat(e) 10207 .hasMessageThat() 10208 .contains( 10209 "DocumentPropertyConfig.addIndexableNestedProperties is not supported on" 10210 + " this AppSearch implementation."); 10211 } 10212 10213 @Test testSetSchema_indexableNestedPropsList_nonIndexableProp()10214 public void testSetSchema_indexableNestedPropsList_nonIndexableProp() throws Exception { 10215 assumeTrue( 10216 mDb1.getFeatures() 10217 .isFeatureSupported(Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES)); 10218 10219 AppSearchSchema personSchema = 10220 new AppSearchSchema.Builder("Person") 10221 .addProperty( 10222 new StringPropertyConfig.Builder("name") 10223 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 10224 .setIndexingType( 10225 StringPropertyConfig.INDEXING_TYPE_PREFIXES) 10226 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 10227 .build()) 10228 .addProperty( 10229 new AppSearchSchema.DocumentPropertyConfig.Builder( 10230 "worksFor", "Organization") 10231 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 10232 .setShouldIndexNestedProperties(false) 10233 .addIndexableNestedProperties(Collections.singleton("name")) 10234 .build()) 10235 .build(); 10236 AppSearchSchema organizationSchema = 10237 new AppSearchSchema.Builder("Organization") 10238 .addProperty( 10239 new StringPropertyConfig.Builder("name") 10240 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 10241 .setIndexingType( 10242 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 10243 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 10244 .build()) 10245 .addProperty( 10246 new StringPropertyConfig.Builder("notes") 10247 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 10248 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_NONE) 10249 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_NONE) 10250 .build()) 10251 .build(); 10252 10253 mDb1.setSchemaAsync( 10254 new SetSchemaRequest.Builder() 10255 .addSchemas(personSchema, organizationSchema) 10256 .build()) 10257 .get(); 10258 10259 // Test that Person's nested properties are indexed correctly. 10260 GenericDocument org1 = 10261 new GenericDocument.Builder<>("namespace", "org1", "Organization") 10262 .setPropertyString("name", "Org1") 10263 .setPropertyString("notes", "Some notes") 10264 .build(); 10265 GenericDocument person1 = 10266 new GenericDocument.Builder<>("namespace", "person1", "Person") 10267 .setPropertyString("name", "Jane") 10268 .setPropertyDocument("worksFor", org1) 10269 .build(); 10270 10271 AppSearchBatchResult<String, Void> putResult = 10272 checkIsBatchResultSuccess( 10273 mDb1.putAsync( 10274 new PutDocumentsRequest.Builder() 10275 .addGenericDocuments(person1, org1) 10276 .build())); 10277 assertThat(putResult.getSuccesses()).containsExactly("person1", null, "org1", null); 10278 assertThat(putResult.getFailures()).isEmpty(); 10279 10280 GetByDocumentIdRequest getByDocumentIdRequest = 10281 new GetByDocumentIdRequest.Builder("namespace").addIds("person1", "org1").build(); 10282 List<GenericDocument> outDocuments = doGet(mDb1, getByDocumentIdRequest); 10283 assertThat(outDocuments).hasSize(2); 10284 assertThat(outDocuments).containsExactly(person1, org1); 10285 10286 // Both org1 and person should be returned for query "Org1" 10287 // For org1 this matches the 'name' property and for person1 this matches the 10288 // 'worksFor.name' property. 10289 SearchResults searchResults = 10290 mDb1.search( 10291 "Org1", 10292 new SearchSpec.Builder() 10293 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 10294 .build()); 10295 outDocuments = convertSearchResultsToDocuments(searchResults); 10296 assertThat(outDocuments).hasSize(2); 10297 assertThat(outDocuments).containsExactly(person1, org1); 10298 10299 // No documents should match for "notes", since both 'Organization:notes' 10300 // and 'Person:worksFor.notes' are non-indexable. 10301 searchResults = 10302 mDb1.search( 10303 "notes", 10304 new SearchSpec.Builder() 10305 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 10306 .build()); 10307 outDocuments = convertSearchResultsToDocuments(searchResults); 10308 assertThat(outDocuments).hasSize(0); 10309 } 10310 10311 @Test testSetSchema_indexableNestedPropsList_multipleNestedLevels()10312 public void testSetSchema_indexableNestedPropsList_multipleNestedLevels() throws Exception { 10313 assumeTrue( 10314 mDb1.getFeatures() 10315 .isFeatureSupported(Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES)); 10316 10317 AppSearchSchema emailSchema = 10318 new AppSearchSchema.Builder("Email") 10319 .addProperty( 10320 new StringPropertyConfig.Builder("subject") 10321 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 10322 .setIndexingType( 10323 StringPropertyConfig.INDEXING_TYPE_PREFIXES) 10324 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 10325 .build()) 10326 .addProperty( 10327 new AppSearchSchema.DocumentPropertyConfig.Builder( 10328 "sender", "Person") 10329 .setCardinality(PropertyConfig.CARDINALITY_REPEATED) 10330 .setShouldIndexNestedProperties(false) 10331 .addIndexableNestedProperties( 10332 Arrays.asList( 10333 "name", "worksFor.name", "worksFor.notes")) 10334 .build()) 10335 .addProperty( 10336 new AppSearchSchema.DocumentPropertyConfig.Builder( 10337 "recipient", "Person") 10338 .setCardinality(PropertyConfig.CARDINALITY_REPEATED) 10339 .setShouldIndexNestedProperties(true) 10340 .build()) 10341 .build(); 10342 AppSearchSchema personSchema = 10343 new AppSearchSchema.Builder("Person") 10344 .addProperty( 10345 new StringPropertyConfig.Builder("name") 10346 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 10347 .setIndexingType( 10348 StringPropertyConfig.INDEXING_TYPE_PREFIXES) 10349 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 10350 .build()) 10351 .addProperty( 10352 new StringPropertyConfig.Builder("age") 10353 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 10354 .setIndexingType( 10355 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 10356 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 10357 .build()) 10358 .addProperty( 10359 new AppSearchSchema.DocumentPropertyConfig.Builder( 10360 "worksFor", "Organization") 10361 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 10362 .setShouldIndexNestedProperties(false) 10363 .addIndexableNestedProperties(Arrays.asList("name", "id")) 10364 .build()) 10365 .build(); 10366 AppSearchSchema organizationSchema = 10367 new AppSearchSchema.Builder("Organization") 10368 .addProperty( 10369 new StringPropertyConfig.Builder("name") 10370 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 10371 .setIndexingType( 10372 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 10373 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 10374 .build()) 10375 .addProperty( 10376 new StringPropertyConfig.Builder("notes") 10377 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 10378 .setIndexingType( 10379 StringPropertyConfig.INDEXING_TYPE_PREFIXES) 10380 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 10381 .build()) 10382 .addProperty( 10383 new StringPropertyConfig.Builder("id") 10384 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 10385 .setIndexingType( 10386 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 10387 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 10388 .build()) 10389 .build(); 10390 10391 mDb1.setSchemaAsync( 10392 new SetSchemaRequest.Builder() 10393 .addSchemas(emailSchema, personSchema, organizationSchema) 10394 .build()) 10395 .get(); 10396 10397 // Test that Email and Person's nested properties are indexed correctly. 10398 GenericDocument org1 = 10399 new GenericDocument.Builder<>("namespace", "org1", "Organization") 10400 .setPropertyString("name", "Org1") 10401 .setPropertyString("notes", "Some notes") 10402 .setPropertyString("id", "1234") 10403 .build(); 10404 GenericDocument person1 = 10405 new GenericDocument.Builder<>("namespace", "person1", "Person") 10406 .setPropertyString("name", "Jane") 10407 .setPropertyString("age", "20") 10408 .setPropertyDocument("worksFor", org1) 10409 .build(); 10410 GenericDocument person2 = 10411 new GenericDocument.Builder<>("namespace", "person2", "Person") 10412 .setPropertyString("name", "John") 10413 .setPropertyString("age", "30") 10414 .setPropertyDocument("worksFor", org1) 10415 .build(); 10416 GenericDocument email1 = 10417 new GenericDocument.Builder<>("namespace", "email1", "Email") 10418 .setPropertyString("subject", "Greetings!") 10419 .setPropertyDocument("sender", person1) 10420 .setPropertyDocument("recipient", person2) 10421 .build(); 10422 AppSearchBatchResult<String, Void> putResult = 10423 checkIsBatchResultSuccess( 10424 mDb1.putAsync( 10425 new PutDocumentsRequest.Builder() 10426 .addGenericDocuments(person1, org1, person2, email1) 10427 .build())); 10428 assertThat(putResult.getSuccesses()) 10429 .containsExactly("person1", null, "org1", null, "person2", null, "email1", null); 10430 assertThat(putResult.getFailures()).isEmpty(); 10431 10432 GetByDocumentIdRequest getByDocumentIdRequest = 10433 new GetByDocumentIdRequest.Builder("namespace") 10434 .addIds("person1", "org1", "person2", "email1") 10435 .build(); 10436 List<GenericDocument> outDocuments = doGet(mDb1, getByDocumentIdRequest); 10437 assertThat(outDocuments).hasSize(4); 10438 assertThat(outDocuments).containsExactly(person1, org1, person2, email1); 10439 10440 // Indexed properties: 10441 // Email: 'subject', 'sender.name', 'sender.worksFor.name', 'sender.worksFor.notes', 10442 // 'recipient.name', 'recipient.age', 'recipient.worksFor.name', 10443 // 'recipient.worksFor.id' 10444 // (Email:recipient sets index_nested_props=true, so it follows the same indexing 10445 // configs as the next schema-type level (person)) 10446 // Person: 'name', 'age', 'worksFor.name', 'worksFor.id' 10447 // Organization: 'name', 'notes', 'id' 10448 // 10449 // All documents should be returned for query 'Org1' because all schemaTypes index the 10450 // 'Organization:name' property. 10451 SearchResults searchResults = 10452 mDb1.search( 10453 "Org1", 10454 new SearchSpec.Builder() 10455 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 10456 .build()); 10457 outDocuments = convertSearchResultsToDocuments(searchResults); 10458 assertThat(outDocuments).hasSize(4); 10459 assertThat(outDocuments).containsExactly(person1, org1, person2, email1); 10460 10461 // org1 and email1 should be returned for query 'notes' 10462 searchResults = 10463 mDb1.search( 10464 "notes", 10465 new SearchSpec.Builder() 10466 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 10467 .build()); 10468 outDocuments = convertSearchResultsToDocuments(searchResults); 10469 assertThat(outDocuments).hasSize(2); 10470 assertThat(outDocuments).containsExactly(org1, email1); 10471 10472 // all docs should be returned for query "1234" 10473 searchResults = 10474 mDb1.search( 10475 "1234", 10476 new SearchSpec.Builder() 10477 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 10478 .build()); 10479 outDocuments = convertSearchResultsToDocuments(searchResults); 10480 assertThat(outDocuments).hasSize(4); 10481 assertThat(outDocuments).containsExactly(person1, org1, person2, email1); 10482 10483 // email1 should be returned for query "30", but not for "20" since sender.age is not 10484 // indexed, but recipient.age is. 10485 // For query "30", person2 should also be returned 10486 // For query "20, person1 should be returned. 10487 searchResults = 10488 mDb1.search( 10489 "30", 10490 new SearchSpec.Builder() 10491 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 10492 .build()); 10493 outDocuments = convertSearchResultsToDocuments(searchResults); 10494 assertThat(outDocuments).hasSize(2); 10495 assertThat(outDocuments).containsExactly(person2, email1); 10496 10497 searchResults = 10498 mDb1.search( 10499 "20", 10500 new SearchSpec.Builder() 10501 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 10502 .build()); 10503 outDocuments = convertSearchResultsToDocuments(searchResults); 10504 assertThat(outDocuments).hasSize(1); 10505 assertThat(outDocuments).containsExactly(person1); 10506 } 10507 10508 @Test testSetSchema_indexableNestedPropsList_circularRefs()10509 public void testSetSchema_indexableNestedPropsList_circularRefs() throws Exception { 10510 assumeTrue( 10511 mDb1.getFeatures() 10512 .isFeatureSupported(Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES)); 10513 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SET_SCHEMA_CIRCULAR_REFERENCES)); 10514 10515 // Create schema with valid cycle: Person -> Organization -> Person... 10516 AppSearchSchema personSchema = 10517 new AppSearchSchema.Builder("Person") 10518 .addProperty( 10519 new StringPropertyConfig.Builder("name") 10520 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 10521 .setIndexingType( 10522 StringPropertyConfig.INDEXING_TYPE_PREFIXES) 10523 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 10524 .build()) 10525 .addProperty( 10526 new StringPropertyConfig.Builder("address") 10527 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 10528 .setIndexingType( 10529 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 10530 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 10531 .build()) 10532 .addProperty( 10533 new AppSearchSchema.DocumentPropertyConfig.Builder( 10534 "worksFor", "Organization") 10535 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 10536 .setShouldIndexNestedProperties(false) 10537 .addIndexableNestedProperties( 10538 Arrays.asList("name", "notes", "funder.name")) 10539 .build()) 10540 .build(); 10541 AppSearchSchema organizationSchema = 10542 new AppSearchSchema.Builder("Organization") 10543 .addProperty( 10544 new StringPropertyConfig.Builder("name") 10545 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 10546 .setIndexingType( 10547 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 10548 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 10549 .build()) 10550 .addProperty( 10551 new StringPropertyConfig.Builder("notes") 10552 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 10553 .setIndexingType( 10554 StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS) 10555 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 10556 .build()) 10557 .addProperty( 10558 new AppSearchSchema.DocumentPropertyConfig.Builder( 10559 "funder", "Person") 10560 .setCardinality(PropertyConfig.CARDINALITY_REPEATED) 10561 .setShouldIndexNestedProperties(false) 10562 .addIndexableNestedProperties( 10563 Arrays.asList( 10564 "name", 10565 "worksFor.name", 10566 "worksFor.funder.address", 10567 "worksFor.funder.worksFor.notes")) 10568 .build()) 10569 .build(); 10570 mDb1.setSchemaAsync( 10571 new SetSchemaRequest.Builder() 10572 .addSchemas(personSchema, organizationSchema) 10573 .build()) 10574 .get(); 10575 10576 // Test that documents following the circular schema are indexed correctly, and that its 10577 // sections are searchable 10578 GenericDocument person1 = 10579 new GenericDocument.Builder<>("namespace", "person1", "Person") 10580 .setPropertyString("name", "Person1") 10581 .setPropertyString("address", "someAddress") 10582 .build(); 10583 GenericDocument org1 = 10584 new GenericDocument.Builder<>("namespace", "org1", "Organization") 10585 .setPropertyString("name", "Org1") 10586 .setPropertyString("notes", "someNote") 10587 .setPropertyDocument("funder", person1) 10588 .build(); 10589 GenericDocument person2 = 10590 new GenericDocument.Builder<>("namespace", "person2", "Person") 10591 .setPropertyString("name", "Person2") 10592 .setPropertyString("address", "anotherAddress") 10593 .setPropertyDocument("worksFor", org1) 10594 .build(); 10595 GenericDocument org2 = 10596 new GenericDocument.Builder<>("namespace", "org2", "Organization") 10597 .setPropertyString("name", "Org2") 10598 .setPropertyString("notes", "anotherNote") 10599 .setPropertyDocument("funder", person2) 10600 .build(); 10601 10602 AppSearchBatchResult<String, Void> putResult = 10603 checkIsBatchResultSuccess( 10604 mDb1.putAsync( 10605 new PutDocumentsRequest.Builder() 10606 .addGenericDocuments(person1, org1, person2, org2) 10607 .build())); 10608 assertThat(putResult.getSuccesses()) 10609 .containsExactly("person1", null, "org1", null, "person2", null, "org2", null); 10610 assertThat(putResult.getFailures()).isEmpty(); 10611 10612 GetByDocumentIdRequest getByDocumentIdRequest = 10613 new GetByDocumentIdRequest.Builder("namespace") 10614 .addIds("person1", "person2", "org1", "org2") 10615 .build(); 10616 List<GenericDocument> outDocuments = doGet(mDb1, getByDocumentIdRequest); 10617 assertThat(outDocuments).hasSize(4); 10618 assertThat(outDocuments).containsExactly(person1, person2, org1, org2); 10619 10620 // Indexed properties: 10621 // Person: 'name', 'address', 'worksFor.name', 'worksFor.notes', 'worksFor.funder.name' 10622 // Organization: 'name', 'notes', 'funder.name', 'funder.worksFor.name', 10623 // 'funder.worksFor.funder.address', 'funder.worksFor.funder.worksFor.notes' 10624 // 10625 // "Person1" should match person1 (name), org1 (funder.name) and person2 10626 // (worksFor.funder.name) 10627 SearchResults searchResults = 10628 mDb1.search( 10629 "Person1", 10630 new SearchSpec.Builder() 10631 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 10632 .build()); 10633 outDocuments = convertSearchResultsToDocuments(searchResults); 10634 assertThat(outDocuments).hasSize(3); 10635 assertThat(outDocuments).containsExactly(person1, org1, person2); 10636 10637 // "someAddress" should match person1 (address) and org2 (funder.worksFor.funder.address) 10638 searchResults = 10639 mDb1.search( 10640 "someAddress", 10641 new SearchSpec.Builder() 10642 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 10643 .build()); 10644 outDocuments = convertSearchResultsToDocuments(searchResults); 10645 assertThat(outDocuments).hasSize(2); 10646 assertThat(outDocuments).containsExactly(person1, org2); 10647 10648 // "Org1" should match org1 (name), person2 (worksFor.name) and org2 (funder.worksFor.name) 10649 searchResults = 10650 mDb1.search( 10651 "Org1", 10652 new SearchSpec.Builder() 10653 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 10654 .build()); 10655 outDocuments = convertSearchResultsToDocuments(searchResults); 10656 assertThat(outDocuments).hasSize(3); 10657 assertThat(outDocuments).containsExactly(org1, person2, org2); 10658 10659 // "someNote" should match org1 (notes) and person2 (worksFor.notes) 10660 searchResults = 10661 mDb1.search( 10662 "someNote", 10663 new SearchSpec.Builder() 10664 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 10665 .build()); 10666 outDocuments = convertSearchResultsToDocuments(searchResults); 10667 assertThat(outDocuments).hasSize(2); 10668 assertThat(outDocuments).containsExactly(org1, person2); 10669 10670 // "Person2" should match person2 (name), org2 (funder.name) 10671 searchResults = 10672 mDb1.search( 10673 "Person2", 10674 new SearchSpec.Builder() 10675 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 10676 .build()); 10677 outDocuments = convertSearchResultsToDocuments(searchResults); 10678 assertThat(outDocuments).hasSize(2); 10679 assertThat(outDocuments).containsExactly(person2, org2); 10680 10681 // "anotherAddress" should match only person2 (address) 10682 searchResults = 10683 mDb1.search( 10684 "anotherAddress", 10685 new SearchSpec.Builder() 10686 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 10687 .build()); 10688 outDocuments = convertSearchResultsToDocuments(searchResults); 10689 assertThat(outDocuments).hasSize(1); 10690 assertThat(outDocuments).containsExactly(person2); 10691 10692 // "Org2" and "anotherNote" should both match only org2 (name, notes) 10693 searchResults = 10694 mDb1.search( 10695 "Org2", 10696 new SearchSpec.Builder() 10697 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 10698 .build()); 10699 outDocuments = convertSearchResultsToDocuments(searchResults); 10700 assertThat(outDocuments).hasSize(1); 10701 assertThat(outDocuments).containsExactly(org2); 10702 10703 searchResults = 10704 mDb1.search( 10705 "anotherNote", 10706 new SearchSpec.Builder() 10707 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 10708 .build()); 10709 outDocuments = convertSearchResultsToDocuments(searchResults); 10710 assertThat(outDocuments).hasSize(1); 10711 assertThat(outDocuments).containsExactly(org2); 10712 } 10713 10714 @Test testSetSchema_toString_containsIndexableNestedPropsList()10715 public void testSetSchema_toString_containsIndexableNestedPropsList() throws Exception { 10716 assumeTrue( 10717 mDb1.getFeatures() 10718 .isFeatureSupported(Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES)); 10719 10720 AppSearchSchema emailSchema = 10721 new AppSearchSchema.Builder("Email") 10722 .addProperty( 10723 new StringPropertyConfig.Builder("subject") 10724 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 10725 .setIndexingType( 10726 StringPropertyConfig.INDEXING_TYPE_PREFIXES) 10727 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 10728 .build()) 10729 .addProperty( 10730 new AppSearchSchema.DocumentPropertyConfig.Builder( 10731 "sender", "Person") 10732 .setCardinality(PropertyConfig.CARDINALITY_REPEATED) 10733 .setShouldIndexNestedProperties(false) 10734 .addIndexableNestedProperties( 10735 Arrays.asList( 10736 "name", "worksFor.name", "worksFor.notes")) 10737 .build()) 10738 .addProperty( 10739 new AppSearchSchema.DocumentPropertyConfig.Builder( 10740 "recipient", "Person") 10741 .setCardinality(PropertyConfig.CARDINALITY_REPEATED) 10742 .setShouldIndexNestedProperties(true) 10743 .build()) 10744 .build(); 10745 String expectedIndexableNestedPropertyMessage = 10746 "indexableNestedProperties: [name, worksFor.notes, worksFor.name]"; 10747 10748 assertThat(emailSchema.toString()).contains(expectedIndexableNestedPropertyMessage); 10749 10750 } 10751 10752 @Test 10753 @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG) testEmbeddingSearch_simple()10754 public void testEmbeddingSearch_simple() throws Exception { 10755 assumeTrue( 10756 mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG)); 10757 10758 // Schema registration 10759 AppSearchSchema schema = new AppSearchSchema.Builder("Email") 10760 .addProperty(new StringPropertyConfig.Builder("body") 10761 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 10762 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 10763 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 10764 .build()) 10765 .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding1") 10766 .setCardinality(PropertyConfig.CARDINALITY_REPEATED) 10767 .setIndexingType( 10768 AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY) 10769 .build()) 10770 .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding2") 10771 .setCardinality(PropertyConfig.CARDINALITY_REPEATED) 10772 .setIndexingType( 10773 AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY) 10774 .build()) 10775 .build(); 10776 mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get(); 10777 10778 // Index documents 10779 GenericDocument doc0 = 10780 new GenericDocument.Builder<>("namespace", "id0", "Email") 10781 .setPropertyString("body", "foo") 10782 .setCreationTimestampMillis(1000) 10783 .setPropertyEmbedding("embedding1", new EmbeddingVector( 10784 new float[]{0.1f, 0.2f, 0.3f, 0.4f, 0.5f}, "my_model_v1")) 10785 .setPropertyEmbedding("embedding2", new EmbeddingVector( 10786 new float[]{-0.1f, -0.2f, -0.3f, 0.4f, 0.5f}, 10787 "my_model_v1"), 10788 new EmbeddingVector( 10789 new float[]{0.6f, 0.7f, 0.8f}, "my_model_v2")) 10790 .build(); 10791 GenericDocument doc1 = 10792 new GenericDocument.Builder<>("namespace", "id1", "Email") 10793 .setPropertyString("body", "bar") 10794 .setCreationTimestampMillis(1000) 10795 .setPropertyEmbedding("embedding1", new EmbeddingVector( 10796 new float[]{-0.1f, 0.2f, -0.3f, -0.4f, 0.5f}, "my_model_v1")) 10797 .setPropertyEmbedding("embedding2", new EmbeddingVector( 10798 new float[]{0.6f, 0.7f, -0.8f}, "my_model_v2")) 10799 .build(); 10800 checkIsBatchResultSuccess(mDb1.putAsync( 10801 new PutDocumentsRequest.Builder().addGenericDocuments(doc0, doc1).build())); 10802 10803 // Add an embedding search with dot product semantic scores: 10804 // - document 0: -0.5 (embedding1), 0.3 (embedding2) 10805 // - document 1: -0.9 (embedding1) 10806 EmbeddingVector searchEmbedding = new EmbeddingVector( 10807 new float[]{1, -1, -1, 1, -1}, "my_model_v1"); 10808 10809 // Match documents that have embeddings with a similarity closer to 0 that is 10810 // greater than -1. 10811 // 10812 // The matched embeddings for each doc are: 10813 // - document 0: -0.5 (embedding1), 0.3 (embedding2) 10814 // - document 1: -0.9 (embedding1) 10815 // The scoring expression for each doc will be evaluated as: 10816 // - document 0: sum({-0.5, 0.3}) + sum({}) = -0.2 10817 // - document 1: sum({-0.9}) + sum({}) = -0.9 10818 SearchSpec searchSpec = new SearchSpec.Builder() 10819 .setDefaultEmbeddingSearchMetricType( 10820 SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT) 10821 .addEmbeddingParameters(searchEmbedding) 10822 .setRankingStrategy( 10823 "sum(this.matchedSemanticScores(getEmbeddingParameter(0)))") 10824 .setListFilterQueryLanguageEnabled(true) 10825 .build(); 10826 SearchResults searchResults = mDb1.search( 10827 "semanticSearch(getEmbeddingParameter(0), -1, 1)", searchSpec); 10828 List<SearchResult> results = retrieveAllSearchResults(searchResults); 10829 assertThat(results).hasSize(2); 10830 assertThat(results.get(0).getGenericDocument()).isEqualTo(doc0); 10831 assertThat(results.get(0).getRankingSignal()).isWithin(0.00001).of(-0.2); 10832 assertThat(results.get(1).getGenericDocument()).isEqualTo(doc1); 10833 assertThat(results.get(1).getRankingSignal()).isWithin(0.00001).of(-0.9); 10834 } 10835 10836 @Test 10837 @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG) testEmbeddingSearch_propertyRestriction()10838 public void testEmbeddingSearch_propertyRestriction() throws Exception { 10839 assumeTrue( 10840 mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG)); 10841 10842 // Schema registration 10843 AppSearchSchema schema = new AppSearchSchema.Builder("Email") 10844 .addProperty(new StringPropertyConfig.Builder("body") 10845 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 10846 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 10847 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 10848 .build()) 10849 .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding1") 10850 .setCardinality(PropertyConfig.CARDINALITY_REPEATED) 10851 .setIndexingType( 10852 AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY) 10853 .build()) 10854 .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding2") 10855 .setCardinality(PropertyConfig.CARDINALITY_REPEATED) 10856 .setIndexingType( 10857 AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY) 10858 .build()) 10859 .build(); 10860 mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get(); 10861 10862 // Index documents 10863 GenericDocument doc0 = 10864 new GenericDocument.Builder<>("namespace", "id0", "Email") 10865 .setPropertyString("body", "foo") 10866 .setCreationTimestampMillis(1000) 10867 .setPropertyEmbedding("embedding1", new EmbeddingVector( 10868 new float[]{0.1f, 0.2f, 0.3f, 0.4f, 0.5f}, "my_model_v1")) 10869 .setPropertyEmbedding("embedding2", new EmbeddingVector( 10870 new float[]{-0.1f, -0.2f, -0.3f, 0.4f, 0.5f}, 10871 "my_model_v1"), 10872 new EmbeddingVector( 10873 new float[]{0.6f, 0.7f, 0.8f}, "my_model_v2")) 10874 .build(); 10875 GenericDocument doc1 = 10876 new GenericDocument.Builder<>("namespace", "id1", "Email") 10877 .setPropertyString("body", "bar") 10878 .setCreationTimestampMillis(1000) 10879 .setPropertyEmbedding("embedding1", new EmbeddingVector( 10880 new float[]{-0.1f, 0.2f, -0.3f, -0.4f, 0.5f}, "my_model_v1")) 10881 .setPropertyEmbedding("embedding2", new EmbeddingVector( 10882 new float[]{0.6f, 0.7f, -0.8f}, "my_model_v2")) 10883 .build(); 10884 checkIsBatchResultSuccess(mDb1.putAsync( 10885 new PutDocumentsRequest.Builder().addGenericDocuments(doc0, doc1).build())); 10886 10887 // Add an embedding search with dot product semantic scores: 10888 // - document 0: -0.5 (embedding1), 0.3 (embedding2) 10889 // - document 1: -0.9 (embedding1) 10890 EmbeddingVector searchEmbedding = new EmbeddingVector( 10891 new float[]{1, -1, -1, 1, -1}, "my_model_v1"); 10892 10893 // Create a query similar as above but with a property restriction, which still matches 10894 // document 0 and document 1 but the semantic score 0.3 should be removed from document 0. 10895 // 10896 // The matched embeddings for each doc are: 10897 // - document 0: -0.5 (embedding1) 10898 // - document 1: -0.9 (embedding1) 10899 // The scoring expression for each doc will be evaluated as: 10900 // - document 0: sum({-0.5}) = -0.5 10901 // - document 1: sum({-0.9}) = -0.9 10902 SearchSpec searchSpec = new SearchSpec.Builder() 10903 .setDefaultEmbeddingSearchMetricType( 10904 SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT) 10905 .addEmbeddingParameters(searchEmbedding) 10906 .setRankingStrategy("sum(this.matchedSemanticScores(getEmbeddingParameter(0)))") 10907 .setListFilterQueryLanguageEnabled(true) 10908 .build(); 10909 SearchResults searchResults = mDb1.search( 10910 "embedding1:semanticSearch(getEmbeddingParameter(0), -1, 1)", searchSpec); 10911 List<SearchResult> results = retrieveAllSearchResults(searchResults); 10912 assertThat(results).hasSize(2); 10913 assertThat(results.get(0).getGenericDocument()).isEqualTo(doc0); 10914 assertThat(results.get(0).getRankingSignal()).isWithin(0.00001).of(-0.5); 10915 assertThat(results.get(1).getGenericDocument()).isEqualTo(doc1); 10916 assertThat(results.get(1).getRankingSignal()).isWithin(0.00001).of(-0.9); 10917 } 10918 10919 @Test 10920 @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG) testEmbeddingSearch_multipleSearchEmbeddings()10921 public void testEmbeddingSearch_multipleSearchEmbeddings() throws Exception { 10922 assumeTrue( 10923 mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG)); 10924 10925 // Schema registration 10926 AppSearchSchema schema = new AppSearchSchema.Builder("Email") 10927 .addProperty(new StringPropertyConfig.Builder("body") 10928 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 10929 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 10930 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 10931 .build()) 10932 .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding1") 10933 .setCardinality(PropertyConfig.CARDINALITY_REPEATED) 10934 .setIndexingType( 10935 AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY) 10936 .build()) 10937 .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding2") 10938 .setCardinality(PropertyConfig.CARDINALITY_REPEATED) 10939 .setIndexingType( 10940 AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY) 10941 .build()) 10942 .build(); 10943 mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get(); 10944 10945 // Index documents 10946 GenericDocument doc0 = 10947 new GenericDocument.Builder<>("namespace", "id0", "Email") 10948 .setPropertyString("body", "foo") 10949 .setCreationTimestampMillis(1000) 10950 .setPropertyEmbedding("embedding1", new EmbeddingVector( 10951 new float[]{0.1f, 0.2f, 0.3f, 0.4f, 0.5f}, "my_model_v1")) 10952 .setPropertyEmbedding("embedding2", new EmbeddingVector( 10953 new float[]{-0.1f, -0.2f, -0.3f, 0.4f, 0.5f}, 10954 "my_model_v1"), 10955 new EmbeddingVector( 10956 new float[]{0.6f, 0.7f, 0.8f}, "my_model_v2")) 10957 .build(); 10958 GenericDocument doc1 = 10959 new GenericDocument.Builder<>("namespace", "id1", "Email") 10960 .setPropertyString("body", "bar") 10961 .setCreationTimestampMillis(1000) 10962 .setPropertyEmbedding("embedding1", new EmbeddingVector( 10963 new float[]{-0.1f, 0.2f, -0.3f, -0.4f, 0.5f}, "my_model_v1")) 10964 .setPropertyEmbedding("embedding2", new EmbeddingVector( 10965 new float[]{0.6f, 0.7f, -0.8f}, "my_model_v2")) 10966 .build(); 10967 checkIsBatchResultSuccess(mDb1.putAsync( 10968 new PutDocumentsRequest.Builder().addGenericDocuments(doc0, doc1).build())); 10969 10970 // Add an embedding search with dot product semantic scores: 10971 // - document 0: -0.5 (embedding1), 0.3 (embedding2) 10972 // - document 1: -0.9 (embedding1) 10973 EmbeddingVector searchEmbedding1 = new EmbeddingVector( 10974 new float[]{1, -1, -1, 1, -1}, "my_model_v1"); 10975 // Add an embedding search with dot product semantic scores: 10976 // - document 0: -0.5 (embedding2) 10977 // - document 1: -2.1 (embedding2) 10978 EmbeddingVector searchEmbedding2 = new EmbeddingVector( 10979 new float[]{-1, -1, 1}, "my_model_v2"); 10980 10981 // Create a complex query that matches all hits from all documents. 10982 // 10983 // The matched embeddings for each doc are: 10984 // - document 0: -0.5 (embedding1), 0.3 (embedding2), -0.5 (embedding2) 10985 // - document 1: -0.9 (embedding1), -2.1 (embedding2) 10986 // The scoring expression for each doc will be evaluated as: 10987 // - document 0: sum({-0.5, 0.3}) + sum({-0.5}) = -0.7 10988 // - document 1: sum({-0.9}) + sum({-2.1}) = -3 10989 SearchSpec searchSpec = new SearchSpec.Builder() 10990 .setDefaultEmbeddingSearchMetricType( 10991 SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT) 10992 .addEmbeddingParameters(searchEmbedding1, searchEmbedding2) 10993 .setRankingStrategy("sum(this.matchedSemanticScores(getEmbeddingParameter(0))) + " 10994 + "sum(this.matchedSemanticScores(getEmbeddingParameter(1)))") 10995 .setListFilterQueryLanguageEnabled(true) 10996 .build(); 10997 SearchResults searchResults = mDb1.search( 10998 "semanticSearch(getEmbeddingParameter(0)) OR " 10999 + "semanticSearch(getEmbeddingParameter(1))", searchSpec); 11000 List<SearchResult> results = retrieveAllSearchResults(searchResults); 11001 assertThat(results).hasSize(2); 11002 assertThat(results.get(0).getGenericDocument()).isEqualTo(doc0); 11003 assertThat(results.get(0).getRankingSignal()).isWithin(0.00001).of(-0.7); 11004 assertThat(results.get(1).getGenericDocument()).isEqualTo(doc1); 11005 assertThat(results.get(1).getRankingSignal()).isWithin(0.00001).of(-3); 11006 } 11007 11008 @Test 11009 @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG) testEmbeddingSearch_hybrid()11010 public void testEmbeddingSearch_hybrid() throws Exception { 11011 assumeTrue( 11012 mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG)); 11013 11014 // Schema registration 11015 AppSearchSchema schema = new AppSearchSchema.Builder("Email") 11016 .addProperty(new StringPropertyConfig.Builder("body") 11017 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 11018 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 11019 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 11020 .build()) 11021 .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding1") 11022 .setCardinality(PropertyConfig.CARDINALITY_REPEATED) 11023 .setIndexingType( 11024 AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY) 11025 .build()) 11026 .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding2") 11027 .setCardinality(PropertyConfig.CARDINALITY_REPEATED) 11028 .setIndexingType( 11029 AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY) 11030 .build()) 11031 .build(); 11032 mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get(); 11033 11034 // Index documents 11035 GenericDocument doc0 = 11036 new GenericDocument.Builder<>("namespace", "id0", "Email") 11037 .setPropertyString("body", "foo") 11038 .setCreationTimestampMillis(1000) 11039 .setPropertyEmbedding("embedding1", new EmbeddingVector( 11040 new float[]{0.1f, 0.2f, 0.3f, 0.4f, 0.5f}, "my_model_v1")) 11041 .setPropertyEmbedding("embedding2", new EmbeddingVector( 11042 new float[]{-0.1f, -0.2f, -0.3f, 0.4f, 0.5f}, 11043 "my_model_v1"), 11044 new EmbeddingVector( 11045 new float[]{0.6f, 0.7f, 0.8f}, "my_model_v2")) 11046 .build(); 11047 GenericDocument doc1 = 11048 new GenericDocument.Builder<>("namespace", "id1", "Email") 11049 .setPropertyString("body", "bar") 11050 .setCreationTimestampMillis(1000) 11051 .setPropertyEmbedding("embedding1", new EmbeddingVector( 11052 new float[]{-0.1f, 0.2f, -0.3f, -0.4f, 0.5f}, "my_model_v1")) 11053 .setPropertyEmbedding("embedding2", new EmbeddingVector( 11054 new float[]{0.6f, 0.7f, -0.8f}, "my_model_v2")) 11055 .build(); 11056 checkIsBatchResultSuccess(mDb1.putAsync( 11057 new PutDocumentsRequest.Builder().addGenericDocuments(doc0, doc1).build())); 11058 11059 // Add an embedding search with dot product semantic scores: 11060 // - document 0: -0.5 (embedding2) 11061 // - document 1: -2.1 (embedding2) 11062 EmbeddingVector searchEmbedding = new EmbeddingVector( 11063 new float[]{-1, -1, 1}, "my_model_v2"); 11064 11065 // Create a hybrid query that matches document 0 because of term-based search 11066 // and document 1 because of embedding-based search. 11067 // 11068 // The matched embeddings for each doc are: 11069 // - document 1: -2.1 (embedding2) 11070 // The scoring expression for each doc will be evaluated as: 11071 // - document 0: sum({}) = 0 11072 // - document 1: sum({-2.1}) = -2.1 11073 SearchSpec searchSpec = new SearchSpec.Builder() 11074 .setDefaultEmbeddingSearchMetricType( 11075 SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT) 11076 .addEmbeddingParameters(searchEmbedding) 11077 .setRankingStrategy("sum(this.matchedSemanticScores(getEmbeddingParameter(0)))") 11078 .setListFilterQueryLanguageEnabled(true) 11079 .build(); 11080 SearchResults searchResults = mDb1.search( 11081 "foo OR semanticSearch(getEmbeddingParameter(0), -10, -1)", searchSpec); 11082 List<SearchResult> results = retrieveAllSearchResults(searchResults); 11083 assertThat(results).hasSize(2); 11084 assertThat(results.get(0).getGenericDocument()).isEqualTo(doc0); 11085 assertThat(results.get(0).getRankingSignal()).isWithin(0.00001).of(0); 11086 assertThat(results.get(1).getGenericDocument()).isEqualTo(doc1); 11087 assertThat(results.get(1).getRankingSignal()).isWithin(0.00001).of(-2.1); 11088 } 11089 11090 @Test 11091 @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG) testEmbeddingSearch_notSupported()11092 public void testEmbeddingSearch_notSupported() throws Exception { 11093 assumeTrue( 11094 mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_QUERY_LANGUAGE)); 11095 assumeFalse( 11096 mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG)); 11097 11098 EmbeddingVector searchEmbedding = new EmbeddingVector( 11099 new float[]{-1, -1, 1}, "my_model_v2"); 11100 SearchSpec searchSpec = new SearchSpec.Builder() 11101 .setListFilterQueryLanguageEnabled(true) 11102 .addEmbeddingParameters(searchEmbedding) 11103 .build(); 11104 UnsupportedOperationException exception = assertThrows( 11105 UnsupportedOperationException.class, 11106 () -> mDb1.search("semanticSearch(getEmbeddingParameter(0), -1, 1)", 11107 searchSpec).getNextPageAsync().get()); 11108 assertThat(exception).hasMessageThat().contains(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG 11109 + " is not available on this AppSearch implementation."); 11110 } 11111 11112 @Test 11113 @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SEARCH_SPEC_SEARCH_STRING_PARAMETERS) testSearchSpecStrings_simple()11114 public void testSearchSpecStrings_simple() throws Exception { 11115 assumeTrue( 11116 mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_QUERY_LANGUAGE)); 11117 assumeTrue( 11118 mDb1.getFeatures().isFeatureSupported( 11119 Features.SEARCH_SPEC_SEARCH_STRING_PARAMETERS)); 11120 11121 // Schema registration 11122 AppSearchSchema schema = new AppSearchSchema.Builder("Email") 11123 .addProperty(new StringPropertyConfig.Builder("body") 11124 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 11125 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 11126 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 11127 .build()) 11128 .build(); 11129 mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get(); 11130 11131 // Index documents 11132 GenericDocument doc0 = 11133 new GenericDocument.Builder<>("namespace", "id0", "Email") 11134 .setPropertyString("body", "foo bar") 11135 .setCreationTimestampMillis(1000) 11136 .build(); 11137 GenericDocument doc1 = 11138 new GenericDocument.Builder<>("namespace", "id1", "Email") 11139 .setPropertyString("body", "bar") 11140 .setCreationTimestampMillis(1000) 11141 .build(); 11142 GenericDocument doc2 = 11143 new GenericDocument.Builder<>("namespace", "id2", "Email") 11144 .setPropertyString("body", "foo") 11145 .setCreationTimestampMillis(1000) 11146 .build(); 11147 checkIsBatchResultSuccess(mDb1.putAsync( 11148 new PutDocumentsRequest.Builder().addGenericDocuments(doc0, doc1, doc2).build())); 11149 11150 SearchSpec searchSpec = new SearchSpec.Builder() 11151 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 11152 .setListFilterQueryLanguageEnabled(true) 11153 .addSearchStringParameters("foo.") 11154 .build(); 11155 SearchResults searchResults = mDb1.search("getSearchStringParameter(0)", searchSpec); 11156 List<GenericDocument> results = convertSearchResultsToDocuments(searchResults); 11157 assertThat(results).containsExactly(doc2, doc0); 11158 11159 searchSpec = new SearchSpec.Builder() 11160 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 11161 .setListFilterQueryLanguageEnabled(true) 11162 .addSearchStringParameters("bar, foo") 11163 .build(); 11164 searchResults = mDb1.search("getSearchStringParameter(0)", searchSpec); 11165 results = convertSearchResultsToDocuments(searchResults); 11166 assertThat(results).containsExactly(doc0); 11167 11168 searchSpec = new SearchSpec.Builder() 11169 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 11170 .setListFilterQueryLanguageEnabled(true) 11171 .addSearchStringParameters("\\\"bar, \\\"foo\\\"") 11172 .build(); 11173 searchResults = mDb1.search("getSearchStringParameter(0)", searchSpec); 11174 results = convertSearchResultsToDocuments(searchResults); 11175 assertThat(results).containsExactly(doc0); 11176 11177 searchSpec = new SearchSpec.Builder() 11178 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 11179 .setListFilterQueryLanguageEnabled(true) 11180 .addSearchStringParameters("bar ) foo") 11181 .build(); 11182 searchResults = mDb1.search("getSearchStringParameter(0)", searchSpec); 11183 results = convertSearchResultsToDocuments(searchResults); 11184 assertThat(results).containsExactly(doc0); 11185 11186 searchSpec = new SearchSpec.Builder() 11187 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 11188 .setListFilterQueryLanguageEnabled(true) 11189 .addSearchStringParameters("bar foo(") 11190 .build(); 11191 searchResults = mDb1.search("getSearchStringParameter(0)", searchSpec); 11192 results = convertSearchResultsToDocuments(searchResults); 11193 assertThat(results).containsExactly(doc0); 11194 } 11195 11196 @Test 11197 @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SEARCH_SPEC_SEARCH_STRING_PARAMETERS) testSearchSpecString_notSupported()11198 public void testSearchSpecString_notSupported() throws Exception { 11199 assumeTrue( 11200 mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_QUERY_LANGUAGE)); 11201 assumeFalse( 11202 mDb1.getFeatures().isFeatureSupported( 11203 Features.SEARCH_SPEC_SEARCH_STRING_PARAMETERS)); 11204 11205 SearchSpec searchSpec = new SearchSpec.Builder() 11206 .setListFilterQueryLanguageEnabled(true) 11207 .addSearchStringParameters("bar foo(") 11208 .build(); 11209 UnsupportedOperationException exception = assertThrows( 11210 UnsupportedOperationException.class, 11211 () -> mDb1.search("getSearchStringParameter(0)", searchSpec)); 11212 assertThat(exception).hasMessageThat().contains( 11213 Features.SEARCH_SPEC_SEARCH_STRING_PARAMETERS 11214 + " is not available on this AppSearch implementation."); 11215 } 11216 11217 11218 @Test 11219 @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SEARCH_SPEC_SEARCH_STRING_PARAMETERS) testSuggestionSearchSpecStringParameters_simple()11220 public void testSuggestionSearchSpecStringParameters_simple() throws Exception { 11221 assumeTrue( 11222 mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_QUERY_LANGUAGE)); 11223 assumeTrue( 11224 mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION)); 11225 assumeTrue( 11226 mDb1.getFeatures().isFeatureSupported( 11227 Features.SEARCH_SPEC_SEARCH_STRING_PARAMETERS)); 11228 11229 // Schema registration 11230 AppSearchSchema schema = new AppSearchSchema.Builder("Email") 11231 .addProperty(new StringPropertyConfig.Builder("body") 11232 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 11233 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 11234 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 11235 .build()) 11236 .build(); 11237 mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get(); 11238 11239 // Index documents 11240 GenericDocument doc0 = 11241 new GenericDocument.Builder<>("namespace", "id0", "Email") 11242 .setPropertyString("body", "foo bar") 11243 .setCreationTimestampMillis(1000) 11244 .build(); 11245 checkIsBatchResultSuccess(mDb1.putAsync( 11246 new PutDocumentsRequest.Builder().addGenericDocuments(doc0).build())); 11247 11248 // Get a suggestion for 'foo b'. This should be expanded to 'foo bar'. Using search string 11249 // parameters to replace a token other than the last one, should work exactly the same as if 11250 // the parameter were written in the string itself. 11251 SearchSuggestionSpec spec = new SearchSuggestionSpec.Builder(/*maximumResultCount=*/1) 11252 .addSearchStringParameters("foo") 11253 .build(); 11254 List<SearchSuggestionResult> suggestions = 11255 mDb1.searchSuggestionAsync("getSearchStringParameter(0) b", spec).get(); 11256 assertThat(suggestions).hasSize(1); 11257 assertThat(suggestions.get(0).getSuggestedResult()) 11258 .isEqualTo("getSearchStringParameter(0) bar"); 11259 } 11260 11261 @Test 11262 @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SEARCH_SPEC_SEARCH_STRING_PARAMETERS) testSearchSuggestionSpecStringParameters_notSupported()11263 public void testSearchSuggestionSpecStringParameters_notSupported() throws Exception { 11264 assumeTrue( 11265 mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION)); 11266 assumeFalse( 11267 mDb1.getFeatures().isFeatureSupported( 11268 Features.SEARCH_SPEC_SEARCH_STRING_PARAMETERS)); 11269 11270 SearchSuggestionSpec spec = new SearchSuggestionSpec.Builder(/*maximumResultCount=*/1) 11271 .addSearchStringParameters("foo") 11272 .build(); 11273 UnsupportedOperationException exception = assertThrows( 11274 UnsupportedOperationException.class, 11275 () -> mDb1.searchSuggestionAsync( 11276 "getSearchStringParameter(0) b", spec).get()); 11277 assertThat(exception).hasMessageThat().contains( 11278 Features.SEARCH_SPEC_SEARCH_STRING_PARAMETERS 11279 + " is not available on this AppSearch implementation."); 11280 } 11281 11282 @Test 11283 @RequiresFlagsEnabled({ 11284 Flags.FLAG_ENABLE_INFORMATIONAL_RANKING_EXPRESSIONS, 11285 Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG}) testInformationalRankingExpressions()11286 public void testInformationalRankingExpressions() throws Exception { 11287 assumeTrue( 11288 mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG)); 11289 assumeTrue(mDb1.getFeatures().isFeatureSupported( 11290 Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS)); 11291 11292 // Schema registration 11293 AppSearchSchema schema = new AppSearchSchema.Builder("Email") 11294 .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding") 11295 .setCardinality(PropertyConfig.CARDINALITY_REPEATED) 11296 .setIndexingType( 11297 AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY) 11298 .build()) 11299 .build(); 11300 mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get(); 11301 11302 // Index documents 11303 final int doc0DocScore = 2; 11304 GenericDocument doc0 = 11305 new GenericDocument.Builder<>("namespace", "id0", "Email") 11306 .setScore(doc0DocScore) 11307 .setCreationTimestampMillis(1000) 11308 .setPropertyEmbedding("embedding", new EmbeddingVector( 11309 new float[]{-0.1f, -0.2f, -0.3f, -0.4f, -0.5f}, "my_model")) 11310 .build(); 11311 final int doc1DocScore = 3; 11312 GenericDocument doc1 = 11313 new GenericDocument.Builder<>("namespace", "id1", "Email") 11314 .setScore(doc1DocScore) 11315 .setCreationTimestampMillis(1000) 11316 .setPropertyEmbedding("embedding", new EmbeddingVector( 11317 new float[]{-0.1f, 0.2f, -0.3f, -0.4f, 0.5f}, "my_model"), 11318 new EmbeddingVector( 11319 new float[]{-0.1f, -0.2f, -0.3f, -0.4f, -0.5f}, "my_model")) 11320 .build(); 11321 checkIsBatchResultSuccess(mDb1.putAsync( 11322 new PutDocumentsRequest.Builder().addGenericDocuments(doc0, doc1).build())); 11323 11324 // Add an embedding search with dot product semantic scores: 11325 // - document 0: 0.5 11326 // - document 1: -0.9, 0.5 11327 EmbeddingVector searchEmbedding = new EmbeddingVector( 11328 new float[]{1, -1, -1, 1, -1}, "my_model"); 11329 11330 // Make an embedding query that matches all documents. 11331 SearchSpec searchSpec = new SearchSpec.Builder() 11332 .setDefaultEmbeddingSearchMetricType( 11333 SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT) 11334 .addEmbeddingParameters(searchEmbedding) 11335 .setRankingStrategy( 11336 "sum(this.matchedSemanticScores(getEmbeddingParameter(0)))") 11337 .addInformationalRankingExpressions( 11338 "len(this.matchedSemanticScores(getEmbeddingParameter(0)))") 11339 .addInformationalRankingExpressions("this.documentScore()") 11340 .setListFilterQueryLanguageEnabled(true) 11341 .build(); 11342 SearchResults searchResults = mDb1.search( 11343 "semanticSearch(getEmbeddingParameter(0))", searchSpec); 11344 List<SearchResult> results = retrieveAllSearchResults(searchResults); 11345 assertThat(results).hasSize(2); 11346 // doc0: 11347 assertThat(results.get(0).getGenericDocument()).isEqualTo(doc0); 11348 assertThat(results.get(0).getRankingSignal()).isWithin(0.00001).of(0.5); 11349 // doc0 has 1 embedding vector and a document score of 2. 11350 assertThat(results.get(0).getInformationalRankingSignals()) 11351 .containsExactly(1.0, (double) doc0DocScore).inOrder(); 11352 11353 // doc1: 11354 assertThat(results.get(1).getGenericDocument()).isEqualTo(doc1); 11355 assertThat(results.get(1).getRankingSignal()).isWithin(0.00001).of(-0.9 + 0.5); 11356 // doc1 has 2 embedding vectors and a document score of 3. 11357 assertThat(results.get(1).getInformationalRankingSignals()) 11358 .containsExactly(2.0, (double) doc1DocScore).inOrder(); 11359 } 11360 11361 @Test 11362 @RequiresFlagsEnabled(Flags.FLAG_ENABLE_INFORMATIONAL_RANKING_EXPRESSIONS) testInformationalRankingExpressions_notSupported()11363 public void testInformationalRankingExpressions_notSupported() throws Exception { 11364 assumeTrue(mDb1.getFeatures().isFeatureSupported( 11365 Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION)); 11366 assumeFalse(mDb1.getFeatures().isFeatureSupported( 11367 Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS)); 11368 11369 SearchSpec searchSpec = new SearchSpec.Builder() 11370 .setRankingStrategy("this.documentScore() + 1") 11371 .addInformationalRankingExpressions("this.documentScore()") 11372 .build(); 11373 UnsupportedOperationException exception = assertThrows( 11374 UnsupportedOperationException.class, 11375 () -> mDb1.search("foo", searchSpec)); 11376 assertThat(exception).hasMessageThat().contains( 11377 Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS 11378 + " are not available on this AppSearch implementation."); 11379 } 11380 11381 @Test testPutDocuments_emptyBytesAndDocuments()11382 public void testPutDocuments_emptyBytesAndDocuments() throws Exception { 11383 // Schema registration 11384 AppSearchSchema schema = new AppSearchSchema.Builder("testSchema") 11385 .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder("bytes") 11386 .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED) 11387 .build()) 11388 .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder( 11389 "document", AppSearchEmail.SCHEMA_TYPE) 11390 .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED) 11391 .setShouldIndexNestedProperties(true) 11392 .build()) 11393 .build(); 11394 mDb1.setSchemaAsync(new SetSchemaRequest.Builder() 11395 .addSchemas(schema, AppSearchEmail.SCHEMA).build()).get(); 11396 11397 // Index a document 11398 GenericDocument document = new GenericDocument.Builder<>("namespace", "id1", "testSchema") 11399 .setPropertyBytes("bytes") 11400 .setPropertyDocument("document") 11401 .build(); 11402 11403 AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb1.putAsync( 11404 new PutDocumentsRequest.Builder().addGenericDocuments(document).build())); 11405 assertThat(result.getSuccesses()).containsExactly("id1", null); 11406 assertThat(result.getFailures()).isEmpty(); 11407 11408 GetByDocumentIdRequest request = new GetByDocumentIdRequest.Builder("namespace") 11409 .addIds("id1") 11410 .build(); 11411 List<GenericDocument> outDocuments = doGet(mDb1, request); 11412 assertThat(outDocuments).hasSize(1); 11413 GenericDocument outDocument = outDocuments.get(0); 11414 assertThat(outDocument.getPropertyBytesArray("bytes")).isEmpty(); 11415 assertThat(outDocument.getPropertyDocumentArray("document")).isEmpty(); 11416 } 11417 11418 @Test 11419 @RequiresFlagsEnabled({Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG, 11420 Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_QUANTIZATION}) testEmbeddingQuantization()11421 public void testEmbeddingQuantization() throws Exception { 11422 assumeTrue( 11423 mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG)); 11424 assumeTrue( 11425 mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_QUANTIZATION)); 11426 11427 // Schema registration 11428 AppSearchSchema schema = new AppSearchSchema.Builder("Email") 11429 .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding") 11430 .setCardinality(PropertyConfig.CARDINALITY_REPEATED) 11431 .setIndexingType( 11432 AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY) 11433 .setQuantizationType( 11434 AppSearchSchema.EmbeddingPropertyConfig.QUANTIZATION_TYPE_8_BIT) 11435 .build()) 11436 .build(); 11437 mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get(); 11438 11439 // Index a document 11440 GenericDocument doc = 11441 new GenericDocument.Builder<>("namespace", "id", "Email") 11442 .setCreationTimestampMillis(1000) 11443 // Since quantization is enabled, this vector will be quantized to 11444 // {0, 1, 255}. 11445 .setPropertyEmbedding("embedding", new EmbeddingVector( 11446 new float[]{0, 1.45f, 255}, "my_model")) 11447 .build(); 11448 checkIsBatchResultSuccess(mDb1.putAsync( 11449 new PutDocumentsRequest.Builder().addGenericDocuments(doc).build())); 11450 11451 // Verify the embedding will be quantized, so that the embedding score would be 11452 // 0 + 1 + 255 = 256, instead of 0 + 1.45 + 255 = 256.45. 11453 SearchSpec searchSpec = new SearchSpec.Builder() 11454 .setDefaultEmbeddingSearchMetricType( 11455 SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT) 11456 .addEmbeddingParameters(new EmbeddingVector(new float[]{1, 1, 1}, "my_model")) 11457 .setRankingStrategy( 11458 "sum(this.matchedSemanticScores(getEmbeddingParameter(0)))") 11459 .setListFilterQueryLanguageEnabled(true) 11460 .build(); 11461 SearchResults searchResults = mDb1.search( 11462 "semanticSearch(getEmbeddingParameter(0), -1000, 1000)", searchSpec); 11463 List<SearchResult> results = retrieveAllSearchResults(searchResults); 11464 assertThat(results).hasSize(1); 11465 assertThat(results.get(0).getGenericDocument()).isEqualTo(doc); 11466 assertThat(results.get(0).getRankingSignal()) 11467 .isWithin(0.0001) 11468 .of(256); 11469 } 11470 @Test 11471 @RequiresFlagsEnabled({Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG, 11472 Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_QUANTIZATION}) testEmbeddingQuantization_changeSchema()11473 public void testEmbeddingQuantization_changeSchema() throws Exception { 11474 assumeTrue( 11475 mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG)); 11476 assumeTrue( 11477 mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_QUANTIZATION)); 11478 11479 // Set Schema with an embedding property for QUANTIZATION_TYPE_NONE 11480 AppSearchSchema schema = new AppSearchSchema.Builder("Email") 11481 .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding") 11482 .setCardinality(PropertyConfig.CARDINALITY_REPEATED) 11483 .setIndexingType( 11484 AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY) 11485 .setQuantizationType( 11486 AppSearchSchema.EmbeddingPropertyConfig.QUANTIZATION_TYPE_NONE) 11487 .build()) 11488 .build(); 11489 mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get(); 11490 11491 // Index a document 11492 GenericDocument doc = 11493 new GenericDocument.Builder<>("namespace", "id", "Email") 11494 .setCreationTimestampMillis(1000) 11495 // Since quantization is enabled, this vector will be quantized to 11496 // {0, 1, 255}. 11497 .setPropertyEmbedding("embedding", new EmbeddingVector( 11498 new float[]{0, 1.45f, 255}, "my_model")) 11499 .build(); 11500 checkIsBatchResultSuccess(mDb1.putAsync( 11501 new PutDocumentsRequest.Builder().addGenericDocuments(doc).build())); 11502 11503 // Update the embedding property to QUANTIZATION_TYPE_8_BIT 11504 schema = new AppSearchSchema.Builder("Email") 11505 .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding") 11506 .setCardinality(PropertyConfig.CARDINALITY_REPEATED) 11507 .setIndexingType( 11508 AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY) 11509 .setQuantizationType( 11510 AppSearchSchema.EmbeddingPropertyConfig.QUANTIZATION_TYPE_8_BIT) 11511 .build()) 11512 .build(); 11513 mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get(); 11514 11515 // Verify the embedding will be quantized, so that the embedding score would be 11516 // 0 + 1 + 255 = 256, instead of 0 + 1.45 + 255 = 256.45. 11517 SearchSpec searchSpec = new SearchSpec.Builder() 11518 .setDefaultEmbeddingSearchMetricType( 11519 SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT) 11520 .addEmbeddingParameters(new EmbeddingVector(new float[]{1, 1, 1}, "my_model")) 11521 .setRankingStrategy( 11522 "sum(this.matchedSemanticScores(getEmbeddingParameter(0)))") 11523 .setListFilterQueryLanguageEnabled(true) 11524 .build(); 11525 SearchResults searchResults = mDb1.search( 11526 "semanticSearch(getEmbeddingParameter(0), -1000, 1000)", searchSpec); 11527 List<SearchResult> results = retrieveAllSearchResults(searchResults); 11528 assertThat(results).hasSize(1); 11529 assertThat(results.get(0).getGenericDocument()).isEqualTo(doc); 11530 assertThat(results.get(0).getRankingSignal()) 11531 .isWithin(0.0001) 11532 .of(256); 11533 11534 // Update the embedding property back to QUANTIZATION_TYPE_NONE 11535 schema = new AppSearchSchema.Builder("Email") 11536 .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding") 11537 .setCardinality(PropertyConfig.CARDINALITY_REPEATED) 11538 .setIndexingType( 11539 AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY) 11540 .setQuantizationType( 11541 AppSearchSchema.EmbeddingPropertyConfig.QUANTIZATION_TYPE_NONE) 11542 .build()) 11543 .build(); 11544 mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get(); 11545 11546 // Verify the embedding will not be quantized, so that the embedding score would be 11547 // 0 + 1.45 + 255 = 256.45. 11548 searchResults = mDb1.search( 11549 "semanticSearch(getEmbeddingParameter(0), -1000, 1000)", searchSpec); 11550 results = retrieveAllSearchResults(searchResults); 11551 assertThat(results).hasSize(1); 11552 assertThat(results.get(0).getGenericDocument()).isEqualTo(doc); 11553 assertThat(results.get(0).getRankingSignal()) 11554 .isWithin(0.0001) 11555 .of(256.45); 11556 } 11557 11558 @Test 11559 @RequiresFlagsEnabled({Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG, 11560 Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_QUANTIZATION}) testEmbeddingQuantization_notSupported()11561 public void testEmbeddingQuantization_notSupported() throws Exception { 11562 assumeTrue( 11563 mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_QUERY_LANGUAGE)); 11564 assumeTrue( 11565 mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG)); 11566 assumeFalse( 11567 mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_QUANTIZATION)); 11568 11569 AppSearchSchema schema = new AppSearchSchema.Builder("Email") 11570 .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding") 11571 .setCardinality(PropertyConfig.CARDINALITY_REPEATED) 11572 .setIndexingType( 11573 AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY) 11574 .setQuantizationType( 11575 AppSearchSchema.EmbeddingPropertyConfig 11576 .QUANTIZATION_TYPE_8_BIT) 11577 .build()) 11578 .build(); 11579 11580 UnsupportedOperationException e = 11581 assertThrows( 11582 UnsupportedOperationException.class, 11583 () -> mDb1.setSchemaAsync( 11584 new SetSchemaRequest.Builder() 11585 .addSchemas(schema).build()).get()); 11586 assertThat(e) 11587 .hasMessageThat() 11588 .contains( 11589 Features.SCHEMA_EMBEDDING_QUANTIZATION 11590 + " is not available on this AppSearch implementation."); 11591 } 11592 11593 @Test 11594 @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG) testRankingFunction_maxMinOrDefault()11595 public void testRankingFunction_maxMinOrDefault() throws Exception { 11596 assumeTrue( 11597 mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG)); 11598 assumeTrue(mDb1.getFeatures().isFeatureSupported( 11599 Features.SEARCH_SPEC_RANKING_FUNCTION_MAX_MIN_OR_DEFAULT)); 11600 11601 // Schema registration 11602 AppSearchSchema schema = new AppSearchSchema.Builder("Email") 11603 .addProperty(new StringPropertyConfig.Builder("body") 11604 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 11605 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 11606 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 11607 .build()) 11608 .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding") 11609 .setCardinality(PropertyConfig.CARDINALITY_REPEATED) 11610 .setIndexingType( 11611 AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY) 11612 .build()) 11613 .build(); 11614 mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get(); 11615 11616 // Index documents 11617 GenericDocument doc0 = 11618 new GenericDocument.Builder<>("namespace", "id0", "Email") 11619 .setPropertyString("body", "foo") 11620 .setCreationTimestampMillis(1000) 11621 .setPropertyEmbedding("embedding", 11622 new EmbeddingVector( 11623 new float[]{0.6f, 0.7f, 0.8f}, "my_model")) 11624 .build(); 11625 GenericDocument doc1 = 11626 new GenericDocument.Builder<>("namespace", "id1", "Email") 11627 .setPropertyString("body", "bar") 11628 .setCreationTimestampMillis(1000) 11629 .setPropertyEmbedding("embedding", new EmbeddingVector( 11630 new float[]{0.6f, 0.7f, -0.8f}, "my_model"), 11631 new EmbeddingVector( 11632 new float[]{0.2f, 0.1f, -1.2f}, "my_model")) 11633 .build(); 11634 checkIsBatchResultSuccess(mDb1.putAsync( 11635 new PutDocumentsRequest.Builder().addGenericDocuments(doc0, doc1).build())); 11636 11637 // Add an embedding search with dot product semantic scores: 11638 // - document 0: -0.5 11639 // - document 1: -2.1, -1.5 11640 EmbeddingVector searchEmbedding = new EmbeddingVector( 11641 new float[]{-1, -1, 1}, "my_model"); 11642 11643 // Create a hybrid query that matches document 0 because of term-based search 11644 // and document 1 because of embedding-based search. 11645 // 11646 // The scoring expression for each doc will be evaluated as: 11647 // - document 0: maxOrDefault({}, -100) = -100 11648 // - document 1: maxOrDefault({-2.1, -1.5}, -100) = -1.5 11649 SearchSpec searchSpec1 = new SearchSpec.Builder() 11650 .setDefaultEmbeddingSearchMetricType( 11651 SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT) 11652 .addEmbeddingParameters(searchEmbedding) 11653 .setRankingStrategy( 11654 "maxOrDefault(this.matchedSemanticScores(getEmbeddingParameter(0)), -100)") 11655 .setListFilterQueryLanguageEnabled(true) 11656 .build(); 11657 SearchResults searchResults = mDb1.search( 11658 "foo OR semanticSearch(getEmbeddingParameter(0), -10, -1)", searchSpec1); 11659 List<SearchResult> results = retrieveAllSearchResults(searchResults); 11660 assertThat(results).hasSize(2); 11661 assertThat(results.get(0).getGenericDocument()).isEqualTo(doc1); 11662 assertThat(results.get(0).getRankingSignal()).isWithin(0.00001).of(-1.5); 11663 assertThat(results.get(1).getGenericDocument()).isEqualTo(doc0); 11664 assertThat(results.get(1).getRankingSignal()).isWithin(0.00001).of(-100); 11665 11666 // Create the same query with minOrDefault. 11667 // 11668 // The scoring expression for each doc will be evaluated as: 11669 // - document 0: minOrDefault({}, -100) = -100 11670 // - document 1: minOrDefault({-2.1, -1.5}, -100) = -2.1 11671 SearchSpec searchSpec2 = new SearchSpec.Builder() 11672 .setDefaultEmbeddingSearchMetricType( 11673 SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT) 11674 .addEmbeddingParameters(searchEmbedding) 11675 .setRankingStrategy( 11676 "minOrDefault(this.matchedSemanticScores(getEmbeddingParameter(0)), -100)") 11677 .setListFilterQueryLanguageEnabled(true) 11678 .build(); 11679 searchResults = mDb1.search( 11680 "foo OR semanticSearch(getEmbeddingParameter(0), -10, -1)", searchSpec2); 11681 results = retrieveAllSearchResults(searchResults); 11682 assertThat(results).hasSize(2); 11683 assertThat(results.get(0).getGenericDocument()).isEqualTo(doc1); 11684 assertThat(results.get(0).getRankingSignal()).isWithin(0.00001).of(-2.1); 11685 assertThat(results.get(1).getGenericDocument()).isEqualTo(doc0); 11686 assertThat(results.get(1).getRankingSignal()).isWithin(0.00001).of(-100); 11687 } 11688 11689 @Test testRankingFunction_maxMinOrDefault_notSupported()11690 public void testRankingFunction_maxMinOrDefault_notSupported() 11691 throws Exception { 11692 assumeTrue(mDb1.getFeatures().isFeatureSupported( 11693 Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION)); 11694 assumeFalse(mDb1.getFeatures().isFeatureSupported( 11695 Features.SEARCH_SPEC_RANKING_FUNCTION_MAX_MIN_OR_DEFAULT)); 11696 11697 // Schema registration 11698 mDb1.setSchemaAsync( 11699 new SetSchemaRequest.Builder() 11700 .addSchemas(AppSearchEmail.SCHEMA) 11701 .build()).get(); 11702 11703 // Index a document 11704 AppSearchEmail inEmail = 11705 new AppSearchEmail.Builder("namespace", "id1") 11706 .setFrom("from@example.com") 11707 .setTo("to1@example.com", "to2@example.com") 11708 .setSubject("testPut example") 11709 .setBody("This is the body of the testPut email") 11710 .build(); 11711 checkIsBatchResultSuccess(mDb1.putAsync( 11712 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build())); 11713 11714 // Query for the document with the unsupported ranking function maxOrDefault. 11715 SearchResults searchResults1 = mDb1.search("foo", 11716 new SearchSpec.Builder() 11717 .setRankingStrategy("maxOrDefault()") 11718 .build()); 11719 ExecutionException executionException = assertThrows(ExecutionException.class, 11720 () -> searchResults1.getNextPageAsync().get()); 11721 assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class); 11722 AppSearchException exception = (AppSearchException) executionException.getCause(); 11723 assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT); 11724 assertThat(exception).hasMessageThat().contains("Unknown function: maxOrDefault"); 11725 11726 // Query for the document with the unsupported ranking function minOrDefault. 11727 SearchResults searchResults2 = mDb1.search("foo", 11728 new SearchSpec.Builder() 11729 .setRankingStrategy("minOrDefault()") 11730 .build()); 11731 executionException = assertThrows(ExecutionException.class, 11732 () -> searchResults2.getNextPageAsync().get()); 11733 assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class); 11734 exception = (AppSearchException) executionException.getCause(); 11735 assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT); 11736 assertThat(exception).hasMessageThat().contains("Unknown function: minOrDefault"); 11737 } 11738 11739 @Test 11740 @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG) testRankingFunction_filterByRange()11741 public void testRankingFunction_filterByRange() throws Exception { 11742 assumeTrue( 11743 mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG)); 11744 assumeTrue(mDb1.getFeatures().isFeatureSupported( 11745 Features.SEARCH_SPEC_RANKING_FUNCTION_FILTER_BY_RANGE)); 11746 11747 // Schema registration 11748 AppSearchSchema schema = new AppSearchSchema.Builder("Email") 11749 .addProperty(new StringPropertyConfig.Builder("body") 11750 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 11751 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 11752 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 11753 .build()) 11754 .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding") 11755 .setCardinality(PropertyConfig.CARDINALITY_REPEATED) 11756 .setIndexingType( 11757 AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY) 11758 .build()) 11759 .build(); 11760 mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get(); 11761 11762 // Index documents 11763 GenericDocument doc = 11764 new GenericDocument.Builder<>("namespace", "id", "Email") 11765 .setPropertyString("body", "bar") 11766 .setCreationTimestampMillis(1000) 11767 .setPropertyEmbedding("embedding", new EmbeddingVector( 11768 new float[]{0.6f, 0.7f, -0.8f}, "my_model"), 11769 new EmbeddingVector( 11770 new float[]{0.2f, 0.1f, -1.2f}, "my_model"), 11771 new EmbeddingVector( 11772 new float[]{0.f, 0.f, 0.1f}, "my_model")) 11773 .build(); 11774 checkIsBatchResultSuccess(mDb1.putAsync( 11775 new PutDocumentsRequest.Builder().addGenericDocuments(doc).build())); 11776 11777 // Add an embedding search with dot product semantic scores: -2.1, -1.5, 0.1 11778 EmbeddingVector searchEmbedding = new EmbeddingVector( 11779 new float[]{-1, -1, 1}, "my_model"); 11780 11781 // Create a query with a ranking signal as the sum of all matched semantic scores within 11782 // [-2, 1], which will be evaluated as -1.5 + 0.1 = -1.4. 11783 SearchSpec searchSpec = new SearchSpec.Builder() 11784 .setDefaultEmbeddingSearchMetricType( 11785 SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT) 11786 .addEmbeddingParameters(searchEmbedding) 11787 .setRankingStrategy( 11788 "sum(filterByRange(this.matchedSemanticScores(" 11789 + "getEmbeddingParameter(0)), -2, 1))") 11790 .setListFilterQueryLanguageEnabled(true) 11791 .build(); 11792 SearchResults searchResults = mDb1.search( 11793 "semanticSearch(getEmbeddingParameter(0))", searchSpec); 11794 List<SearchResult> results = retrieveAllSearchResults(searchResults); 11795 assertThat(results).hasSize(1); 11796 assertThat(results.get(0).getGenericDocument()).isEqualTo(doc); 11797 assertThat(results.get(0).getRankingSignal()).isWithin(0.00001).of(-1.5 + 0.1); 11798 } 11799 11800 @Test testRankingFunction_filterByRange_notSupported()11801 public void testRankingFunction_filterByRange_notSupported() 11802 throws Exception { 11803 assumeTrue(mDb1.getFeatures().isFeatureSupported( 11804 Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION)); 11805 assumeFalse(mDb1.getFeatures().isFeatureSupported( 11806 Features.SEARCH_SPEC_RANKING_FUNCTION_FILTER_BY_RANGE)); 11807 11808 // Schema registration 11809 mDb1.setSchemaAsync( 11810 new SetSchemaRequest.Builder() 11811 .addSchemas(AppSearchEmail.SCHEMA) 11812 .build()).get(); 11813 11814 // Index a document 11815 AppSearchEmail inEmail = 11816 new AppSearchEmail.Builder("namespace", "id1") 11817 .setFrom("from@example.com") 11818 .setTo("to1@example.com", "to2@example.com") 11819 .setSubject("testPut example") 11820 .setBody("This is the body of the testPut email") 11821 .build(); 11822 checkIsBatchResultSuccess(mDb1.putAsync( 11823 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build())); 11824 11825 // Query for the document with the unsupported ranking function filterByRange. 11826 SearchResults searchResults1 = mDb1.search("foo", 11827 new SearchSpec.Builder() 11828 .setRankingStrategy("filterByRange()") 11829 .build()); 11830 ExecutionException executionException = assertThrows(ExecutionException.class, 11831 () -> searchResults1.getNextPageAsync().get()); 11832 assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class); 11833 AppSearchException exception = (AppSearchException) executionException.getCause(); 11834 assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT); 11835 assertThat(exception).hasMessageThat().contains("Unknown function: filterByRange"); 11836 } 11837 11838 @Test 11839 @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCORABLE_PROPERTY) testRankWithScorableProperty_getScorablePropertyFunction_notSupported()11840 public void testRankWithScorableProperty_getScorablePropertyFunction_notSupported() 11841 throws Exception { 11842 assumeTrue(mDb1.getFeatures().isFeatureSupported( 11843 Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION)); 11844 assumeFalse(mDb1.getFeatures().isFeatureSupported( 11845 Features.SCHEMA_SCORABLE_PROPERTY_CONFIG)); 11846 11847 UnsupportedOperationException exception = assertThrows( 11848 UnsupportedOperationException.class, 11849 () -> mDb1.search("body", 11850 new SearchSpec.Builder() 11851 .setScorablePropertyRankingEnabled(true) 11852 .setRankingStrategy( 11853 "sum(getScorableProperty(\"Gmail\", \"invalid\"))") 11854 .build())); 11855 assertThat(exception).hasMessageThat().contains( 11856 Features.SCHEMA_SCORABLE_PROPERTY_CONFIG 11857 + " is not available on this AppSearch implementation."); 11858 } 11859 11860 @Test 11861 @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCORABLE_PROPERTY) testRankWithScorableProperty_setScoringEnabledInSchema_notSupported()11862 public void testRankWithScorableProperty_setScoringEnabledInSchema_notSupported() 11863 throws Exception { 11864 assumeTrue(mDb1.getFeatures().isFeatureSupported( 11865 Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION)); 11866 assumeFalse(mDb1.getFeatures().isFeatureSupported( 11867 Features.SCHEMA_SCORABLE_PROPERTY_CONFIG)); 11868 AppSearchSchema schema = new AppSearchSchema.Builder("Gmail") 11869 .addProperty(new BooleanPropertyConfig.Builder("important") 11870 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 11871 .setScoringEnabled(true) 11872 .build()) 11873 .build(); 11874 UnsupportedOperationException exception = assertThrows( 11875 UnsupportedOperationException.class, 11876 () -> mDb1.setSchemaAsync(new SetSchemaRequest.Builder() 11877 .addSchemas(schema).build()).get() 11878 ); 11879 assertThat(exception).hasMessageThat().contains( 11880 Features.SCHEMA_SCORABLE_PROPERTY_CONFIG 11881 + " is not available on this AppSearch implementation."); 11882 } 11883 11884 @Test 11885 @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCORABLE_PROPERTY) testRankWithScorableProperty_simple()11886 public void testRankWithScorableProperty_simple() throws Exception { 11887 assumeTrue(mDb1.getFeatures().isFeatureSupported( 11888 Features.SCHEMA_SCORABLE_PROPERTY_CONFIG)); 11889 AppSearchSchema schema = new AppSearchSchema.Builder("Gmail") 11890 .addProperty(new StringPropertyConfig.Builder("subject") 11891 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 11892 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 11893 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 11894 .build()) 11895 .addProperty(new BooleanPropertyConfig.Builder("important") 11896 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 11897 .setScoringEnabled(true) 11898 .build()) 11899 .build(); 11900 mDb1.setSchemaAsync(new SetSchemaRequest.Builder() 11901 .addSchemas(schema).build()).get(); 11902 11903 GenericDocument doc1 = 11904 new GenericDocument.Builder<>("namespace", "id1", "Gmail") 11905 .setPropertyString("subject", "bar") 11906 .setPropertyBoolean("important", true) 11907 .setScore(1) 11908 .build(); 11909 int rankingScoreOfDoc1 = 2; 11910 GenericDocument doc2 = 11911 new GenericDocument.Builder<>("namespace", "id2", "Gmail") 11912 .setPropertyString("subject", "bar 2") 11913 .setPropertyBoolean("important", true) 11914 .setScore(2) 11915 .build(); 11916 int rankingScoreOfDoc2 = 3; 11917 checkIsBatchResultSuccess(mDb1.putAsync( 11918 new PutDocumentsRequest.Builder().addGenericDocuments(doc1, doc2).build())); 11919 11920 SearchSpec searchSpec = new SearchSpec.Builder() 11921 .setScorablePropertyRankingEnabled(true) 11922 .setRankingStrategy( 11923 "this.documentScore() + sum(getScorableProperty(\"Gmail\", \"important\"))") 11924 .build(); 11925 11926 SearchResults searchResults = 11927 mDb1.search("", searchSpec); 11928 List<SearchResult> results = retrieveAllSearchResults(searchResults); 11929 assertThat(results).hasSize(2); 11930 assertThat(results.get(0).getGenericDocument()).isEqualTo(doc2); 11931 assertThat(results.get(0).getRankingSignal()) 11932 .isWithin(0.00001).of(rankingScoreOfDoc2); 11933 assertThat(results.get(1).getGenericDocument()).isEqualTo(doc1); 11934 assertThat(results.get(1).getRankingSignal()) 11935 .isWithin(0.00001).of(rankingScoreOfDoc1); 11936 } 11937 11938 @Test 11939 @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCORABLE_PROPERTY) testRankWithScorableProperty_withNestedSchema()11940 public void testRankWithScorableProperty_withNestedSchema() throws Exception { 11941 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_SCORABLE_PROPERTY_CONFIG)); 11942 AppSearchSchema personSchema = new AppSearchSchema.Builder("Person") 11943 .addProperty(new DoublePropertyConfig.Builder("income") 11944 .setCardinality(PropertyConfig.CARDINALITY_REPEATED) 11945 .setScoringEnabled(true) 11946 .build()) 11947 .addProperty(new LongPropertyConfig.Builder("age") 11948 .setCardinality(PropertyConfig.CARDINALITY_REPEATED) 11949 .setScoringEnabled(true) 11950 .build()) 11951 .addProperty(new BooleanPropertyConfig.Builder("isStarred") 11952 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 11953 .setScoringEnabled(true) 11954 .build()) 11955 .build(); 11956 AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email") 11957 .addProperty(new DocumentPropertyConfig 11958 .Builder("sender", "Person") 11959 .setCardinality(PropertyConfig.CARDINALITY_REPEATED) 11960 .build()) 11961 .addProperty(new DocumentPropertyConfig 11962 .Builder("recipient", "Person") 11963 .setCardinality(PropertyConfig.CARDINALITY_REPEATED) 11964 .build()) 11965 .addProperty(new LongPropertyConfig.Builder("viewTimes") 11966 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 11967 .setScoringEnabled(true) 11968 .build()) 11969 .build(); 11970 mDb1.setSchemaAsync( 11971 new SetSchemaRequest.Builder() 11972 .addSchemas(personSchema, emailSchema) 11973 .build()).get(); 11974 11975 GenericDocument sender1 = new GenericDocument.Builder<>("namespace", "person1", "Person") 11976 .setPropertyBoolean("isStarred", true) 11977 .setPropertyDouble("income", 1000, 2000) 11978 .setPropertyLong("age", 30) 11979 .build(); 11980 GenericDocument sender2 = new GenericDocument.Builder<>("namespace", "person2", "Person") 11981 .setPropertyBoolean("isStarred", true) 11982 .setPropertyDouble("income", 5000, 3000) 11983 .setPropertyLong("age", 40) 11984 .build(); 11985 GenericDocument recipient = new GenericDocument.Builder<>("namespace", "person2", "Person") 11986 .setPropertyBoolean("isStarred", true) 11987 .setPropertyDouble("income", 2000, 3000) 11988 .setPropertyLong("age", 50) 11989 .build(); 11990 11991 GenericDocument email = 11992 new GenericDocument.Builder<>("namespace", "email1", "Email") 11993 .setPropertyDocument( 11994 "sender", sender1, sender2) 11995 .setPropertyDocument( 11996 "recipient", recipient) 11997 .setPropertyLong("viewTimes", 10) 11998 .build(); 11999 12000 // Put the email document to AppSearch and verify its success. 12001 AppSearchBatchResult<String, Void> result = 12002 checkIsBatchResultSuccess( 12003 mDb1.putAsync( 12004 new PutDocumentsRequest.Builder() 12005 .addGenericDocuments(email) 12006 .build())); 12007 assertThat(result.getSuccesses()).containsExactly("email1", null); 12008 assertThat(result.getFailures()).isEmpty(); 12009 12010 // Search and ranking with the scorable properties 12011 String rankingStrategy = 12012 "sum(getScorableProperty(\"Email\", \"viewTimes\")) + " + 12013 "max(getScorableProperty(\"Email\", \"recipient.age\")) + " + 12014 "100 * max(getScorableProperty(\"Email\", \"recipient.isStarred\")) + " + 12015 "5 * sum(getScorableProperty(\"Email\", \"sender.income\"))"; 12016 SearchSpec searchSpec = new SearchSpec.Builder() 12017 .setScorablePropertyRankingEnabled(true) 12018 .setRankingStrategy(rankingStrategy) 12019 .build(); 12020 double expectedScore = /*viewTimes=*/10 + /*age=*/50 + 100 * /*isStarred=*/1 + 12021 5 * (1000 + 2000 + 5000 + 3000); 12022 SearchResults searchResults = mDb1.search("", searchSpec); 12023 List<SearchResult> results = retrieveAllSearchResults(searchResults); 12024 assertThat(results).hasSize(1); 12025 assertThat(results.get(0).getGenericDocument()).isEqualTo(email); 12026 assertThat(results.get(0).getRankingSignal()) 12027 .isWithin(0.00001).of(expectedScore); 12028 } 12029 12030 @Test 12031 @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCORABLE_PROPERTY) testRankWithScorableProperty_updateSchemaByAddScorableProperty()12032 public void testRankWithScorableProperty_updateSchemaByAddScorableProperty() throws Exception { 12033 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_SCORABLE_PROPERTY_CONFIG)); 12034 12035 AppSearchSchema schema = new AppSearchSchema.Builder("Gmail") 12036 .addProperty(new StringPropertyConfig.Builder("subject") 12037 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 12038 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 12039 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 12040 .build()) 12041 .build(); 12042 GenericDocument gmailDoc = 12043 new GenericDocument.Builder<>("namespace", "id", "Gmail") 12044 .setPropertyString("subject", "foo") 12045 .build(); 12046 mDb1.setSchemaAsync(new SetSchemaRequest.Builder() 12047 .addSchemas(schema).build()).get(); 12048 12049 // Put the email document to AppSearch and verify its success. 12050 AppSearchBatchResult<String, Void> result = 12051 checkIsBatchResultSuccess( 12052 mDb1.putAsync( 12053 new PutDocumentsRequest.Builder() 12054 .addGenericDocuments(gmailDoc) 12055 .build())); 12056 assertThat(result.getSuccesses()).containsExactly("id", null); 12057 assertThat(result.getFailures()).isEmpty(); 12058 12059 // Update the schema by adding a scorable property. 12060 schema = new AppSearchSchema.Builder("Gmail") 12061 .addProperty(new StringPropertyConfig.Builder("subject") 12062 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 12063 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 12064 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 12065 .build()) 12066 .addProperty(new BooleanPropertyConfig.Builder("important") 12067 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 12068 .setScoringEnabled(true) 12069 .build()) 12070 .build(); 12071 mDb1.setSchemaAsync(new SetSchemaRequest.Builder() 12072 .addSchemas(schema).build()).get(); 12073 12074 // Search and rank over the existing doc. 12075 // The existing document's scorable property has been populated with the default values. 12076 SearchSpec searchSpec = new SearchSpec.Builder() 12077 .setScorablePropertyRankingEnabled(true) 12078 .setRankingStrategy("sum(getScorableProperty(\"Gmail\", \"important\"))") 12079 .build(); 12080 SearchResults searchResults = mDb1.search("", searchSpec); 12081 List<SearchResult> results = retrieveAllSearchResults(searchResults); 12082 assertThat(results).hasSize(1); 12083 assertThat(results.get(0).getRankingSignal()) 12084 .isWithin(0.00001).of(0); 12085 } 12086 12087 @Test 12088 @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCORABLE_PROPERTY) testRankWithScorableProperty_updateScorableTypeInNestedSchema()12089 public void testRankWithScorableProperty_updateScorableTypeInNestedSchema() throws Exception { 12090 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_SCORABLE_PROPERTY_CONFIG)); 12091 12092 AppSearchSchema personSchema = new AppSearchSchema.Builder("Person") 12093 .addProperty(new DoublePropertyConfig.Builder("income") 12094 .setCardinality(PropertyConfig.CARDINALITY_REPEATED) 12095 .setScoringEnabled(false) 12096 .build()) 12097 .build(); 12098 AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email") 12099 .addProperty(new DocumentPropertyConfig 12100 .Builder("sender", "Person") 12101 .setCardinality(PropertyConfig.CARDINALITY_REPEATED) 12102 .build()) 12103 .addProperty(new DocumentPropertyConfig 12104 .Builder("recipient", "Person") 12105 .setCardinality(PropertyConfig.CARDINALITY_REPEATED) 12106 .build()) 12107 .build(); 12108 mDb1.setSchemaAsync( 12109 new SetSchemaRequest.Builder() 12110 .addSchemas(personSchema, emailSchema) 12111 .build()).get(); 12112 12113 GenericDocument sender = new GenericDocument 12114 .Builder<>("namespace", "person1", "Person") 12115 .setPropertyDouble("income", 1000) 12116 .build(); 12117 GenericDocument recipient = new GenericDocument 12118 .Builder<>("namespace", "person2", "Person") 12119 .setPropertyDouble("income", 5000) 12120 .build(); 12121 GenericDocument email = 12122 new GenericDocument.Builder<>("namespace", "email1", "Email") 12123 .setPropertyDocument( 12124 "sender", sender) 12125 .setPropertyDocument( 12126 "recipient", recipient) 12127 .build(); 12128 checkIsBatchResultSuccess( 12129 mDb1.putAsync( 12130 new PutDocumentsRequest.Builder().addGenericDocuments(email).build())); 12131 12132 // Update the 'Person' schema by setting Person.income as scorable. 12133 // It would trigger the re-generation of the scorable property cache for the 12134 // the schema 'Email', as it is a parent schema of 'Person'. 12135 personSchema = new AppSearchSchema.Builder("Person") 12136 .addProperty(new DoublePropertyConfig.Builder("income") 12137 .setCardinality(PropertyConfig.CARDINALITY_REPEATED) 12138 .setScoringEnabled(true) 12139 .build()) 12140 .build(); 12141 mDb1.setSchemaAsync( 12142 new SetSchemaRequest.Builder() 12143 .addSchemas(personSchema, emailSchema) 12144 .build()).get(); 12145 12146 // Search and ranking with the Email.Person.income 12147 String rankingStrategy = 12148 "sum(getScorableProperty(\"Email\", \"sender.income\")) + " + 12149 "max(getScorableProperty(\"Email\", \"recipient.income\"))"; 12150 SearchSpec searchSpec = new SearchSpec.Builder() 12151 .setScorablePropertyRankingEnabled(true) 12152 .setRankingStrategy(rankingStrategy) 12153 .build(); 12154 double expectedScore = 1000 + 5000; 12155 SearchResults searchResults = mDb1.search("", searchSpec); 12156 List<SearchResult> results = retrieveAllSearchResults(searchResults); 12157 assertThat(results).hasSize(1); 12158 assertThat(results.get(0).getGenericDocument()).isEqualTo(email); 12159 assertThat(results.get(0).getRankingSignal()) 12160 .isWithin(0.00001).of(expectedScore); 12161 } 12162 12163 @Test 12164 @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCORABLE_PROPERTY) testRankWithScorableProperty_updateSchemaByFlippingScorableType()12165 public void testRankWithScorableProperty_updateSchemaByFlippingScorableType() throws Exception { 12166 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_SCORABLE_PROPERTY_CONFIG)); 12167 12168 AppSearchSchema schemaWithPropertyScorable = new AppSearchSchema.Builder("Gmail") 12169 .addProperty(new BooleanPropertyConfig.Builder("important") 12170 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 12171 .setScoringEnabled(true) 12172 .build()) 12173 .build(); 12174 GenericDocument doc = 12175 new GenericDocument.Builder<>("namespace", "id", "Gmail") 12176 .setPropertyBoolean("important", true) 12177 .build(); 12178 mDb1.setSchemaAsync(new SetSchemaRequest.Builder() 12179 .addSchemas(schemaWithPropertyScorable).build()).get(); 12180 12181 checkIsBatchResultSuccess(mDb1.putAsync( 12182 new PutDocumentsRequest.Builder().addGenericDocuments(doc).build())); 12183 SearchSpec searchSpec = new SearchSpec.Builder() 12184 .setScorablePropertyRankingEnabled(true) 12185 .setRankingStrategy("sum(getScorableProperty(\"Gmail\", \"important\"))") 12186 .build(); 12187 SearchResults searchResults = 12188 mDb1.search("", searchSpec); 12189 List<SearchResult> results = retrieveAllSearchResults(searchResults); 12190 assertThat(results).hasSize(1); 12191 assertThat(results.get(0).getGenericDocument()).isEqualTo(doc); 12192 assertThat(results.get(0).getRankingSignal()) 12193 .isWithin(0.00001).of(1); 12194 12195 // Update the Schema by updating the property as not scorable 12196 AppSearchSchema schemaWithPropertyNotScorable = new AppSearchSchema 12197 .Builder("Gmail") 12198 .addProperty(new BooleanPropertyConfig.Builder("important") 12199 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 12200 .setScoringEnabled(false) 12201 .build()) 12202 .build(); 12203 mDb1.setSchemaAsync(new SetSchemaRequest.Builder() 12204 .addSchemas(schemaWithPropertyNotScorable).build()).get(); 12205 12206 // Update the schema by updating the property to scorable again. 12207 mDb1.setSchemaAsync(new SetSchemaRequest.Builder() 12208 .addSchemas(schemaWithPropertyScorable).build()).get(); 12209 // Verify that the property can be used for scoring. 12210 searchResults = mDb1.search("", searchSpec); 12211 results = retrieveAllSearchResults(searchResults); 12212 assertThat(results).hasSize(1); 12213 assertThat(results.get(0).getGenericDocument()).isEqualTo(doc); 12214 assertThat(results.get(0).getRankingSignal()) 12215 .isWithin(0.00001).of(1); 12216 } 12217 12218 @Test 12219 @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCORABLE_PROPERTY) testRankWithScorableProperty_reorderSchemaProperties()12220 public void testRankWithScorableProperty_reorderSchemaProperties() throws Exception { 12221 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_SCORABLE_PROPERTY_CONFIG)); 12222 12223 AppSearchSchema personSchema = new AppSearchSchema.Builder("Person") 12224 .addProperty(new DoublePropertyConfig.Builder("income") 12225 .setCardinality(PropertyConfig.CARDINALITY_REPEATED) 12226 .setScoringEnabled(true) 12227 .build()) 12228 .addProperty(new LongPropertyConfig.Builder("age") 12229 .setCardinality(PropertyConfig.CARDINALITY_REPEATED) 12230 .setScoringEnabled(true) 12231 .build()) 12232 .build(); 12233 GenericDocument person = new GenericDocument 12234 .Builder<>("namespace", "person1", "Person") 12235 .setPropertyDouble("income", 1000, 2000) 12236 .setPropertyLong("age", 30) 12237 .build(); 12238 mDb1.setSchemaAsync(new SetSchemaRequest.Builder() 12239 .addSchemas(personSchema).build()).get(); 12240 checkIsBatchResultSuccess(mDb1.putAsync( 12241 new PutDocumentsRequest.Builder().addGenericDocuments(person).build())); 12242 String rankingStrategy = 12243 "sum(getScorableProperty(\"Person\", \"income\")) + " + 12244 "sum(getScorableProperty(\"Person\", \"age\"))"; 12245 SearchSpec searchSpec = new SearchSpec.Builder() 12246 .setScorablePropertyRankingEnabled(true) 12247 .setRankingStrategy(rankingStrategy) 12248 .build(); 12249 double expectedRankingScore = 3030; 12250 12251 SearchResults searchResults = mDb1.search("", searchSpec); 12252 List<SearchResult> results = retrieveAllSearchResults(searchResults); 12253 assertThat(results.get(0).getRankingSignal()) 12254 .isWithin(0.00001).of(expectedRankingScore); 12255 12256 // Update the schema by swapping the order of property 'age' and 'income' 12257 personSchema = new AppSearchSchema.Builder("Person") 12258 .addProperty(new LongPropertyConfig.Builder("age") 12259 .setCardinality(PropertyConfig.CARDINALITY_REPEATED) 12260 .setScoringEnabled(true) 12261 .build()) 12262 .addProperty(new DoublePropertyConfig.Builder("income") 12263 .setCardinality(PropertyConfig.CARDINALITY_REPEATED) 12264 .setScoringEnabled(true) 12265 .build()) 12266 .build(); 12267 mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(personSchema).build()).get(); 12268 12269 // Verify that the ranking is still working as expected. 12270 searchResults = mDb1.search("", searchSpec); 12271 results = retrieveAllSearchResults(searchResults); 12272 assertThat(results.get(0).getRankingSignal()) 12273 .isWithin(0.00001).of(expectedRankingScore); 12274 } 12275 12276 @Test 12277 @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCORABLE_PROPERTY) testRankWithScorableProperty_matchedDocumentHasDifferentSchemaType()12278 public void testRankWithScorableProperty_matchedDocumentHasDifferentSchemaType() 12279 throws Exception { 12280 assumeTrue(mDb1.getFeatures().isFeatureSupported( 12281 Features.SCHEMA_SCORABLE_PROPERTY_CONFIG)); 12282 AppSearchSchema gmailSchema = new AppSearchSchema.Builder("Gmail") 12283 .addProperty(new BooleanPropertyConfig.Builder("important") 12284 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 12285 .setScoringEnabled(true) 12286 .build()) 12287 .build(); 12288 AppSearchSchema personSchema = new AppSearchSchema.Builder("Person") 12289 .addProperty(new LongPropertyConfig.Builder("income") 12290 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 12291 .setScoringEnabled(true) 12292 .build()) 12293 .build(); 12294 mDb1.setSchemaAsync(new SetSchemaRequest.Builder() 12295 .addSchemas(gmailSchema, personSchema).build()).get(); 12296 12297 GenericDocument gmailDoc = 12298 new GenericDocument.Builder<>("namespace", "id1", "Gmail") 12299 .setPropertyBoolean("important", true) 12300 .setScore(1) 12301 .build(); 12302 GenericDocument personDoc = 12303 new GenericDocument.Builder<>("namespace", "id2", "Person") 12304 .setPropertyLong ("income", 100) 12305 .setScore(1) 12306 .build(); 12307 checkIsBatchResultSuccess(mDb1.putAsync( 12308 new PutDocumentsRequest.Builder(). 12309 addGenericDocuments(gmailDoc, personDoc).build())); 12310 12311 SearchSpec searchSpec = new SearchSpec.Builder() 12312 .setScorablePropertyRankingEnabled(true) 12313 .setRankingStrategy( 12314 "this.documentScore() + sum(getScorableProperty(\"Gmail\", \"important\"))") 12315 .build(); 12316 double expectedGmailDocScore = 2; 12317 double expectedPersonDocScore = 1; 12318 12319 SearchResults searchResults = mDb1.search("", searchSpec); 12320 List<SearchResult> results = retrieveAllSearchResults(searchResults); 12321 assertThat(results).hasSize(2); 12322 assertThat(results.get(0).getGenericDocument()).isEqualTo(gmailDoc); 12323 assertThat(results.get(0).getRankingSignal()) 12324 .isWithin(0.00001).of(expectedGmailDocScore); 12325 assertThat(results.get(1).getGenericDocument()).isEqualTo(personDoc); 12326 assertThat(results.get(1).getRankingSignal()) 12327 .isWithin(0.00001).of(expectedPersonDocScore); 12328 } 12329 12330 @Test 12331 @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCORABLE_PROPERTY) testRankWithScorableProperty_matchedDocumentHasNoDataUnderTheRankingProperty()12332 public void testRankWithScorableProperty_matchedDocumentHasNoDataUnderTheRankingProperty() 12333 throws Exception { 12334 assumeTrue(mDb1.getFeatures().isFeatureSupported( 12335 Features.SCHEMA_SCORABLE_PROPERTY_CONFIG)); 12336 AppSearchSchema gmailSchema = new AppSearchSchema.Builder("Gmail") 12337 .addProperty(new LongPropertyConfig.Builder("viewTimes") 12338 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 12339 .setScoringEnabled(true) 12340 .build()) 12341 .build(); 12342 mDb1.setSchemaAsync(new SetSchemaRequest.Builder() 12343 .addSchemas(gmailSchema).build()).get(); 12344 12345 GenericDocument gmailDoc1 = 12346 new GenericDocument.Builder<>("namespace", "id1", "Gmail") 12347 .setPropertyLong("viewTimes", 100) 12348 .setScore(1) 12349 .build(); 12350 GenericDocument gmailDoc2 = 12351 new GenericDocument.Builder<>("namespace", "id2", "Gmail") 12352 .setScore(1) 12353 .build(); 12354 checkIsBatchResultSuccess(mDb1.putAsync( 12355 new PutDocumentsRequest.Builder(). 12356 addGenericDocuments(gmailDoc1, gmailDoc2).build())); 12357 12358 SearchSpec searchSpec = new SearchSpec.Builder() 12359 .setScorablePropertyRankingEnabled(true) 12360 .setRankingStrategy( 12361 "this.documentScore() + sum(getScorableProperty(\"Gmail\", \"viewTimes\"))") 12362 .build(); 12363 double expectedGmailDoc1Score = 101; 12364 double expectedGmailDoc2Score = 1; 12365 12366 SearchResults searchResults = mDb1.search("", searchSpec); 12367 List<SearchResult> results = retrieveAllSearchResults(searchResults); 12368 assertThat(results).hasSize(2); 12369 assertThat(results.get(0).getGenericDocument()).isEqualTo(gmailDoc1); 12370 assertThat(results.get(0).getRankingSignal()) 12371 .isWithin(0.00001).of(expectedGmailDoc1Score); 12372 assertThat(results.get(1).getGenericDocument()).isEqualTo(gmailDoc2); 12373 assertThat(results.get(1).getRankingSignal()) 12374 .isWithin(0.00001).of(expectedGmailDoc2Score); 12375 } 12376 12377 @Test 12378 @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCORABLE_PROPERTY) testRankWithScorableProperty_joinWithChildQuery()12379 public void testRankWithScorableProperty_joinWithChildQuery() throws Exception { 12380 assumeTrue(mDb1.getFeatures().isFeatureSupported( 12381 Features.SCHEMA_SCORABLE_PROPERTY_CONFIG)); 12382 assumeTrue(mDb1.getFeatures().isFeatureSupported( 12383 Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION)); 12384 assumeTrue(mDb1.getFeatures() 12385 .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID)); 12386 12387 AppSearchSchema personSchema = new AppSearchSchema.Builder("Person") 12388 .addProperty(new StringPropertyConfig.Builder("name") 12389 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 12390 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 12391 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 12392 .build()) 12393 .addProperty(new DoublePropertyConfig.Builder("income") 12394 .setCardinality(PropertyConfig.CARDINALITY_REPEATED) 12395 .setScoringEnabled(true) 12396 .build()) 12397 .addProperty(new BooleanPropertyConfig.Builder("isStarred") 12398 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 12399 .setScoringEnabled(true) 12400 .build()) 12401 .build(); 12402 AppSearchSchema callLogSchema = new AppSearchSchema.Builder("CallLog") 12403 .addProperty(new StringPropertyConfig.Builder("personQualifiedId") 12404 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 12405 .setJoinableValueType(StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID) 12406 .build()) 12407 .addProperty(new DoublePropertyConfig.Builder("rfsScore") 12408 .setCardinality(PropertyConfig.CARDINALITY_REPEATED) 12409 .setScoringEnabled(true) 12410 .build()) 12411 .build(); 12412 AppSearchSchema smsLogSchema = new AppSearchSchema.Builder("SmsLog") 12413 .addProperty(new StringPropertyConfig.Builder("personQualifiedId") 12414 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 12415 .setJoinableValueType(StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID) 12416 .build()) 12417 .addProperty(new DoublePropertyConfig.Builder("rfsScore") 12418 .setCardinality(PropertyConfig.CARDINALITY_REPEATED) 12419 .setScoringEnabled(true) 12420 .build()) 12421 .build(); 12422 mDb1.setSchemaAsync( 12423 new SetSchemaRequest.Builder() 12424 .addSchemas(personSchema, callLogSchema, smsLogSchema).build()).get(); 12425 12426 // John will have two CallLog docs to join and one sms log to join. 12427 GenericDocument personJohn = new GenericDocument.Builder<>( 12428 "namespace", "johnId", "Person") 12429 .setPropertyString("name", "John") 12430 .setPropertyBoolean("isStarred", true) 12431 .setPropertyDouble("income", 30) 12432 .setScore(10) 12433 .build(); 12434 // Kevin will have two CallLog docs to join and one sms log to join. 12435 GenericDocument personKevin = new GenericDocument.Builder<>( 12436 "namespace", "kevinId", "Person") 12437 .setPropertyString("name", "Kevin") 12438 .setPropertyBoolean("isStarred", false) 12439 .setPropertyDouble("income", 40) 12440 .setScore(20) 12441 .build(); 12442 // Tim has no CallLog or SmsLog to join. 12443 GenericDocument personTim = new GenericDocument.Builder<>("namespace", "timId", "Person") 12444 .setPropertyString("name", "Tim") 12445 .setPropertyDouble("income", 60) 12446 .setPropertyBoolean("isStarred", true) 12447 .setScore(50) 12448 .build(); 12449 12450 GenericDocument johnCallLog1 = 12451 new GenericDocument.Builder<>( 12452 "namespace", "johnCallLog1", "CallLog") 12453 .setScore(5) 12454 .setPropertyDouble("rfsScore", 100, 200) 12455 .setPropertyString("personQualifiedId", 12456 DocumentIdUtil.createQualifiedId(mContext.getPackageName(), 12457 DB_NAME_1, personJohn)) 12458 .build(); 12459 GenericDocument johnCallLog2 = 12460 new GenericDocument.Builder<>( 12461 "namespace", "johnCallLog2", "CallLog") 12462 .setScore(5) 12463 .setPropertyDouble("rfsScore", 300, 500) 12464 .setPropertyString("personQualifiedId", 12465 DocumentIdUtil.createQualifiedId(mContext.getPackageName(), 12466 DB_NAME_1, personJohn)) 12467 .build(); 12468 GenericDocument kevinCallLog1 = 12469 new GenericDocument.Builder<>( 12470 "namespace", "kevinCallLog1", "CallLog") 12471 .setScore(5) 12472 .setPropertyDouble("rfsScore", 300, 400) 12473 .setPropertyString("personQualifiedId", 12474 DocumentIdUtil.createQualifiedId(mContext.getPackageName(), 12475 DB_NAME_1, personKevin)) 12476 .build(); 12477 GenericDocument kevinCallLog2 = 12478 new GenericDocument.Builder<>( 12479 "namespace", "kevinCallLog2", "CallLog") 12480 .setScore(5) 12481 .setPropertyDouble("rfsScore", 500, 800) 12482 .setPropertyString("personQualifiedId", 12483 DocumentIdUtil.createQualifiedId(mContext.getPackageName(), 12484 DB_NAME_1, personKevin)) 12485 .build(); 12486 GenericDocument johnSmsLog1 = 12487 new GenericDocument.Builder<>( 12488 "namespace", "johnSmsLog1", "SmsLog") 12489 .setScore(5) 12490 .setPropertyDouble("rfsScore", 1000, 2000) 12491 .setPropertyString("personQualifiedId", 12492 DocumentIdUtil.createQualifiedId(mContext.getPackageName(), 12493 DB_NAME_1, personJohn)) 12494 .build(); 12495 GenericDocument kevinSmsLog1 = 12496 new GenericDocument.Builder<>( 12497 "namespace", "kevinSmsLog1", "SmsLog") 12498 .setScore(5) 12499 .setPropertyDouble("rfsScore", 2000, 3000) 12500 .setPropertyString("personQualifiedId", 12501 DocumentIdUtil.createQualifiedId(mContext.getPackageName(), 12502 DB_NAME_1, personKevin)) 12503 .build(); 12504 12505 // Put all documents to AppSearch and verify its success. 12506 AppSearchBatchResult<String, Void> result = 12507 checkIsBatchResultSuccess( 12508 mDb1.putAsync( 12509 new PutDocumentsRequest.Builder() 12510 .addGenericDocuments( 12511 personTim, personJohn, personKevin, 12512 kevinCallLog1, kevinCallLog2, kevinSmsLog1, 12513 johnCallLog1, johnCallLog2, johnSmsLog1) 12514 .build())); 12515 assertThat(result.getSuccesses().size()).isEqualTo(9); 12516 assertThat(result.getFailures()).isEmpty(); 12517 12518 String childRankingStrategy = 12519 "sum(getScorableProperty(\"CallLog\", \"rfsScore\")) + " + 12520 "sum(getScorableProperty(\"SmsLog\", \"rfsScore\"))"; 12521 double johnChildDocScore = 100 + 200 + 300 + 500 + 1000 + 2000; 12522 double kevinChildDocScore = 300 + 400 + 500 + 800 + 2000 + 3000; 12523 double timChildDocScore = 0; 12524 12525 SearchSpec childSearchSpec = new SearchSpec.Builder() 12526 .setScorablePropertyRankingEnabled(true) 12527 .setRankingStrategy(childRankingStrategy) 12528 .build(); 12529 JoinSpec js = new JoinSpec.Builder("personQualifiedId") 12530 .setNestedSearch("", childSearchSpec) 12531 .build(); 12532 String parentRankingStrategy = 12533 "sum(getScorableProperty(\"Person\", \"income\")) + " + 12534 "20 * sum(getScorableProperty(\"Person\", \"isStarred\")) + " + 12535 "sum(this.childrenRankingSignals())"; 12536 SearchSpec parentSearchSpec = new SearchSpec.Builder() 12537 .setScorablePropertyRankingEnabled(true) 12538 .setJoinSpec(js) 12539 .setRankingStrategy(parentRankingStrategy) 12540 .addFilterSchemas("Person") 12541 .build(); 12542 double johnExpectScore = /*income=*/30 + 20 * /*isStarred=*/1 + johnChildDocScore; 12543 double kevinExpectScore = /*income=*/40 + 20 * /*isStarred=*/0 + kevinChildDocScore; 12544 double timExpectScore = /*income=*/60 + 20 * /*isStarred=*/1 + timChildDocScore; 12545 12546 SearchResults searchResults = 12547 mDb1.search("", parentSearchSpec); 12548 List<SearchResult> results = retrieveAllSearchResults(searchResults); 12549 assertThat(results).hasSize(3); 12550 assertThat(results.get(0).getRankingSignal()) 12551 .isWithin(0.00001).of(kevinExpectScore); 12552 assertThat(results.get(1).getRankingSignal()) 12553 .isWithin(0.00001).of(johnExpectScore); 12554 assertThat(results.get(2).getRankingSignal()) 12555 .isWithin(0.00001).of(timExpectScore); 12556 } 12557 12558 @Test 12559 @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SEARCH_RESULT_PARENT_TYPES) testQuery_typeFilterWithPolymorphism()12560 public void testQuery_typeFilterWithPolymorphism() throws Exception { 12561 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE)); 12562 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_RESULT_PARENT_TYPES)); 12563 12564 // Schema registration 12565 AppSearchSchema personSchema = 12566 new AppSearchSchema.Builder("Person") 12567 .addProperty( 12568 new StringPropertyConfig.Builder("name") 12569 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 12570 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 12571 .setIndexingType( 12572 StringPropertyConfig.INDEXING_TYPE_PREFIXES) 12573 .build()) 12574 .build(); 12575 AppSearchSchema artistSchema = 12576 new AppSearchSchema.Builder("Artist") 12577 .addParentType("Person") 12578 .addProperty( 12579 new StringPropertyConfig.Builder("name") 12580 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 12581 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 12582 .setIndexingType( 12583 StringPropertyConfig.INDEXING_TYPE_PREFIXES) 12584 .build()) 12585 .build(); 12586 mDb1.setSchemaAsync( 12587 new SetSchemaRequest.Builder() 12588 .addSchemas(personSchema) 12589 .addSchemas(artistSchema) 12590 .addSchemas(AppSearchEmail.SCHEMA) 12591 .build()) 12592 .get(); 12593 12594 // Index some documents 12595 GenericDocument personDoc = 12596 new GenericDocument.Builder<>("namespace", "id1", "Person") 12597 .setPropertyString("name", "Foo") 12598 .build(); 12599 GenericDocument artistDoc = 12600 new GenericDocument.Builder<>("namespace", "id2", "Artist") 12601 .setPropertyString("name", "Foo") 12602 .build(); 12603 AppSearchEmail emailDoc = 12604 new AppSearchEmail.Builder("namespace", "id3") 12605 .setFrom("from@example.com") 12606 .setTo("to1@example.com", "to2@example.com") 12607 .setSubject("testPut example") 12608 .setBody("Foo") 12609 .build(); 12610 checkIsBatchResultSuccess( 12611 mDb1.putAsync( 12612 new PutDocumentsRequest.Builder() 12613 .addGenericDocuments(personDoc, artistDoc, emailDoc) 12614 .build())); 12615 12616 // Query for the documents 12617 SearchResults searchResults = 12618 mDb1.search( 12619 "Foo", 12620 new SearchSpec.Builder() 12621 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 12622 .build()); 12623 List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults); 12624 assertThat(documents).hasSize(3); 12625 assertThat(documents).containsExactly(personDoc, artistDoc, emailDoc); 12626 12627 // Query with a filter for the "Person" type should also include the "Artist" type. 12628 searchResults = 12629 mDb1.search( 12630 "Foo", 12631 new SearchSpec.Builder() 12632 .addFilterSchemas("Person") 12633 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 12634 .build()); 12635 documents = convertSearchResultsToDocuments(searchResults); 12636 assertThat(documents).hasSize(2); 12637 assertThat(documents).containsExactly(personDoc, artistDoc); 12638 12639 // Query with a filters for the "Artist" type should not include the "Person" type. 12640 searchResults = 12641 mDb1.search( 12642 "Foo", 12643 new SearchSpec.Builder() 12644 .addFilterSchemas("Artist") 12645 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 12646 .build()); 12647 documents = convertSearchResultsToDocuments(searchResults); 12648 assertThat(documents).hasSize(1); 12649 assertThat(documents).containsExactly(artistDoc); 12650 } 12651 12652 @Test 12653 @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SEARCH_RESULT_PARENT_TYPES) testQuery_projectionWithPolymorphism()12654 public void testQuery_projectionWithPolymorphism() throws Exception { 12655 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE)); 12656 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_RESULT_PARENT_TYPES)); 12657 12658 // Schema registration 12659 AppSearchSchema personSchema = 12660 new AppSearchSchema.Builder("Person") 12661 .addProperty( 12662 new StringPropertyConfig.Builder("name") 12663 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 12664 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 12665 .setIndexingType( 12666 StringPropertyConfig.INDEXING_TYPE_PREFIXES) 12667 .build()) 12668 .addProperty( 12669 new StringPropertyConfig.Builder("emailAddress") 12670 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 12671 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 12672 .setIndexingType( 12673 StringPropertyConfig.INDEXING_TYPE_PREFIXES) 12674 .build()) 12675 .build(); 12676 AppSearchSchema artistSchema = 12677 new AppSearchSchema.Builder("Artist") 12678 .addParentType("Person") 12679 .addProperty( 12680 new StringPropertyConfig.Builder("name") 12681 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 12682 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 12683 .setIndexingType( 12684 StringPropertyConfig.INDEXING_TYPE_PREFIXES) 12685 .build()) 12686 .addProperty( 12687 new StringPropertyConfig.Builder("emailAddress") 12688 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 12689 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 12690 .setIndexingType( 12691 StringPropertyConfig.INDEXING_TYPE_PREFIXES) 12692 .build()) 12693 .addProperty( 12694 new StringPropertyConfig.Builder("company") 12695 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 12696 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 12697 .setIndexingType( 12698 StringPropertyConfig.INDEXING_TYPE_PREFIXES) 12699 .build()) 12700 .build(); 12701 mDb1.setSchemaAsync( 12702 new SetSchemaRequest.Builder() 12703 .addSchemas(personSchema) 12704 .addSchemas(artistSchema) 12705 .build()) 12706 .get(); 12707 12708 // Index two documents 12709 GenericDocument personDoc = 12710 new GenericDocument.Builder<>("namespace", "id1", "Person") 12711 .setCreationTimestampMillis(1000) 12712 .setPropertyString("name", "Foo Person") 12713 .setPropertyString("emailAddress", "person@gmail.com") 12714 .build(); 12715 GenericDocument artistDoc = 12716 new GenericDocument.Builder<>("namespace", "id2", "Artist") 12717 .setCreationTimestampMillis(1000) 12718 .setPropertyString("name", "Foo Artist") 12719 .setPropertyString("emailAddress", "artist@gmail.com") 12720 .setPropertyString("company", "Company") 12721 .build(); 12722 checkIsBatchResultSuccess( 12723 mDb1.putAsync( 12724 new PutDocumentsRequest.Builder() 12725 .addGenericDocuments(personDoc, artistDoc) 12726 .build())); 12727 12728 // Query with type property paths {"Person", ["name"]}, {"Artist", ["emailAddress"]} 12729 // This will be expanded to paths {"Person", ["name"]}, {"Artist", ["name", "emailAddress"]} 12730 // via polymorphism. 12731 SearchResults searchResults = 12732 mDb1.search( 12733 "Foo", 12734 new SearchSpec.Builder() 12735 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 12736 .addProjection("Person", ImmutableList.of("name")) 12737 .addProjection("Artist", ImmutableList.of("emailAddress")) 12738 .build()); 12739 List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults); 12740 12741 // The person document should have been returned with only the "name" property. The artist 12742 // document should have been returned with all of its properties. 12743 GenericDocument expectedPerson = 12744 new GenericDocument.Builder<>("namespace", "id1", "Person") 12745 .setCreationTimestampMillis(1000) 12746 .setPropertyString("name", "Foo Person") 12747 .build(); 12748 GenericDocument expectedArtist = 12749 new GenericDocument.Builder<>("namespace", "id2", "Artist") 12750 .setCreationTimestampMillis(1000) 12751 .setPropertyString("name", "Foo Artist") 12752 .setPropertyString("emailAddress", "artist@gmail.com") 12753 .build(); 12754 assertThat(documents).containsExactly(expectedPerson, expectedArtist); 12755 } 12756 12757 @Test 12758 @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SEARCH_RESULT_PARENT_TYPES) testQuery_projectionWithPolymorphismAndSchemaFilter()12759 public void testQuery_projectionWithPolymorphismAndSchemaFilter() throws Exception { 12760 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE)); 12761 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_RESULT_PARENT_TYPES)); 12762 12763 // Schema registration 12764 AppSearchSchema personSchema = 12765 new AppSearchSchema.Builder("Person") 12766 .addProperty( 12767 new StringPropertyConfig.Builder("name") 12768 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 12769 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 12770 .setIndexingType( 12771 StringPropertyConfig.INDEXING_TYPE_PREFIXES) 12772 .build()) 12773 .addProperty( 12774 new StringPropertyConfig.Builder("emailAddress") 12775 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 12776 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 12777 .setIndexingType( 12778 StringPropertyConfig.INDEXING_TYPE_PREFIXES) 12779 .build()) 12780 .build(); 12781 AppSearchSchema artistSchema = 12782 new AppSearchSchema.Builder("Artist") 12783 .addParentType("Person") 12784 .addProperty( 12785 new StringPropertyConfig.Builder("name") 12786 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 12787 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 12788 .setIndexingType( 12789 StringPropertyConfig.INDEXING_TYPE_PREFIXES) 12790 .build()) 12791 .addProperty( 12792 new StringPropertyConfig.Builder("emailAddress") 12793 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 12794 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 12795 .setIndexingType( 12796 StringPropertyConfig.INDEXING_TYPE_PREFIXES) 12797 .build()) 12798 .addProperty( 12799 new StringPropertyConfig.Builder("company") 12800 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) 12801 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 12802 .setIndexingType( 12803 StringPropertyConfig.INDEXING_TYPE_PREFIXES) 12804 .build()) 12805 .build(); 12806 mDb1.setSchemaAsync( 12807 new SetSchemaRequest.Builder() 12808 .addSchemas(personSchema) 12809 .addSchemas(artistSchema) 12810 .build()) 12811 .get(); 12812 12813 // Index two documents 12814 GenericDocument personDoc = 12815 new GenericDocument.Builder<>("namespace", "id1", "Person") 12816 .setCreationTimestampMillis(1000) 12817 .setPropertyString("name", "Foo Person") 12818 .setPropertyString("emailAddress", "person@gmail.com") 12819 .build(); 12820 GenericDocument artistDoc = 12821 new GenericDocument.Builder<>("namespace", "id2", "Artist") 12822 .setCreationTimestampMillis(1000) 12823 .setPropertyString("name", "Foo Artist") 12824 .setPropertyString("emailAddress", "artist@gmail.com") 12825 .setPropertyString("company", "Company") 12826 .build(); 12827 checkIsBatchResultSuccess( 12828 mDb1.putAsync( 12829 new PutDocumentsRequest.Builder() 12830 .addGenericDocuments(personDoc, artistDoc) 12831 .build())); 12832 12833 // Query with type property paths {"Person", ["name"]} and {"Artist", ["emailAddress"]}, and 12834 // a schema filter for the "Person". 12835 // This will be expanded to paths {"Person", ["name"]} and 12836 // {"Artist", ["name", "emailAddress"]}, and filters for both "Person" and "Artist" via 12837 // polymorphism. 12838 SearchResults searchResults = 12839 mDb1.search( 12840 "Foo", 12841 new SearchSpec.Builder() 12842 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 12843 .addFilterSchemas("Person") 12844 .addProjection("Person", ImmutableList.of("name")) 12845 .addProjection("Artist", ImmutableList.of("emailAddress")) 12846 .build()); 12847 List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults); 12848 12849 // The person document should have been returned with only the "name" property. The artist 12850 // document should have been returned with all of its properties. 12851 GenericDocument expectedPerson = 12852 new GenericDocument.Builder<>("namespace", "id1", "Person") 12853 .setCreationTimestampMillis(1000) 12854 .setPropertyString("name", "Foo Person") 12855 .build(); 12856 GenericDocument expectedArtist = 12857 new GenericDocument.Builder<>("namespace", "id2", "Artist") 12858 .setCreationTimestampMillis(1000) 12859 .setPropertyString("name", "Foo Artist") 12860 .setPropertyString("emailAddress", "artist@gmail.com") 12861 .build(); 12862 assertThat(documents).containsExactly(expectedPerson, expectedArtist); 12863 } 12864 12865 @Test 12866 @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SEARCH_RESULT_PARENT_TYPES) testQuery_indexBasedOnParentTypePolymorphism()12867 public void testQuery_indexBasedOnParentTypePolymorphism() throws Exception { 12868 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE)); 12869 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_RESULT_PARENT_TYPES)); 12870 12871 // Schema registration 12872 AppSearchSchema personSchema = 12873 new AppSearchSchema.Builder("Person") 12874 .addProperty( 12875 new StringPropertyConfig.Builder("name") 12876 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 12877 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 12878 .setIndexingType( 12879 StringPropertyConfig.INDEXING_TYPE_PREFIXES) 12880 .build()) 12881 .build(); 12882 AppSearchSchema artistSchema = 12883 new AppSearchSchema.Builder("Artist") 12884 .addParentType("Person") 12885 .addProperty( 12886 new StringPropertyConfig.Builder("name") 12887 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 12888 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 12889 .setIndexingType( 12890 StringPropertyConfig.INDEXING_TYPE_PREFIXES) 12891 .build()) 12892 .addProperty( 12893 new StringPropertyConfig.Builder("company") 12894 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 12895 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 12896 .setIndexingType( 12897 StringPropertyConfig.INDEXING_TYPE_PREFIXES) 12898 .build()) 12899 .build(); 12900 AppSearchSchema messageSchema = 12901 new AppSearchSchema.Builder("Message") 12902 .addProperty( 12903 new AppSearchSchema.DocumentPropertyConfig.Builder( 12904 "sender", "Person") 12905 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 12906 .setShouldIndexNestedProperties(true) 12907 .build()) 12908 .build(); 12909 mDb1.setSchemaAsync( 12910 new SetSchemaRequest.Builder() 12911 .addSchemas(personSchema) 12912 .addSchemas(artistSchema) 12913 .addSchemas(messageSchema) 12914 .build()) 12915 .get(); 12916 12917 // Index some an artistDoc and a messageDoc 12918 GenericDocument artistDoc = 12919 new GenericDocument.Builder<>("namespace", "id1", "Artist") 12920 .setPropertyString("name", "Foo") 12921 .setPropertyString("company", "Bar") 12922 .build(); 12923 GenericDocument messageDoc = 12924 new GenericDocument.Builder<>("namespace", "id2", "Message") 12925 // sender is defined as a Person, which accepts an Artist because Artist <: 12926 // Person. 12927 // However, indexing will be based on what's defined in Person, so the 12928 // "company" 12929 // property in artistDoc cannot be used to search this messageDoc. 12930 .setPropertyDocument("sender", artistDoc) 12931 .build(); 12932 checkIsBatchResultSuccess( 12933 mDb1.putAsync( 12934 new PutDocumentsRequest.Builder() 12935 .addGenericDocuments(artistDoc, messageDoc) 12936 .build())); 12937 12938 // Query for the documents 12939 SearchResults searchResults = 12940 mDb1.search( 12941 "Foo", 12942 new SearchSpec.Builder() 12943 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 12944 .build()); 12945 List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults); 12946 assertThat(documents).hasSize(2); 12947 assertThat(documents).containsExactly(artistDoc, messageDoc); 12948 12949 // The "company" property in artistDoc cannot be used to search messageDoc. 12950 searchResults = 12951 mDb1.search( 12952 "Bar", 12953 new SearchSpec.Builder() 12954 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 12955 .build()); 12956 documents = convertSearchResultsToDocuments(searchResults); 12957 assertThat(documents).hasSize(1); 12958 assertThat(documents).containsExactly(artistDoc); 12959 } 12960 12961 @Test 12962 @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SEARCH_RESULT_PARENT_TYPES) testQuery_parentTypeListIsTopologicalOrder()12963 public void testQuery_parentTypeListIsTopologicalOrder() throws Exception { 12964 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE)); 12965 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_RESULT_PARENT_TYPES)); 12966 // Create the following subtype relation graph, where 12967 // 1. A's direct parents are B and C. 12968 // 2. B's direct parent is D. 12969 // 3. C's direct parent is B and D. 12970 // DFS order from A: [A, B, D, C]. Not acceptable because B and D appear before C. 12971 // BFS order from A: [A, B, C, D]. Not acceptable because B appears before C. 12972 // Topological order (all subtypes appear before supertypes) from A: [A, C, B, D]. 12973 AppSearchSchema schemaA = 12974 new AppSearchSchema.Builder("A") 12975 .addParentType("B") 12976 .addParentType("C") 12977 .build(); 12978 AppSearchSchema schemaB = 12979 new AppSearchSchema.Builder("B") 12980 .addParentType("D") 12981 .build(); 12982 AppSearchSchema schemaC = 12983 new AppSearchSchema.Builder("C") 12984 .addParentType("B") 12985 .addParentType("D") 12986 .build(); 12987 AppSearchSchema schemaD = 12988 new AppSearchSchema.Builder("D") 12989 .build(); 12990 mDb1.setSchemaAsync( 12991 new SetSchemaRequest.Builder() 12992 .addSchemas(schemaA) 12993 .addSchemas(schemaB) 12994 .addSchemas(schemaC) 12995 .addSchemas(schemaD) 12996 .build()) 12997 .get(); 12998 12999 // Index some documents 13000 GenericDocument docA = 13001 new GenericDocument.Builder<>("namespace", "id1", "A") 13002 .build(); 13003 GenericDocument docB = 13004 new GenericDocument.Builder<>("namespace", "id2", "B") 13005 .build(); 13006 GenericDocument docC = 13007 new GenericDocument.Builder<>("namespace", "id3", "C") 13008 .build(); 13009 GenericDocument docD = 13010 new GenericDocument.Builder<>("namespace", "id4", "D") 13011 .build(); 13012 checkIsBatchResultSuccess( 13013 mDb1.putAsync( 13014 new PutDocumentsRequest.Builder() 13015 .addGenericDocuments(docA, docB, docC, docD) 13016 .build())); 13017 13018 Map<String, List<String>> expectedDocAParentTypeMap = 13019 ImmutableMap.of("A", ImmutableList.of("C", "B", "D")); 13020 Map<String, List<String>> expectedDocBParentTypeMap = 13021 ImmutableMap.of("B", ImmutableList.of("D")); 13022 Map<String, List<String>> expectedDocCParentTypeMap = 13023 ImmutableMap.of("C", ImmutableList.of("B", "D")); 13024 Map<String, List<String>> expectedDocDParentTypeMap = Collections.emptyMap(); 13025 // Query for the documents 13026 List<SearchResult> searchResults = retrieveAllSearchResults( 13027 mDb1.search("", new SearchSpec.Builder().build()) 13028 ); 13029 assertThat(searchResults).hasSize(4); 13030 assertThat(searchResults.get(0).getGenericDocument()).isEqualTo(docD); 13031 assertThat(searchResults.get(0).getParentTypeMap()).isEqualTo(expectedDocDParentTypeMap); 13032 13033 assertThat(searchResults.get(1).getGenericDocument()).isEqualTo(docC); 13034 assertThat(searchResults.get(1).getParentTypeMap()).isEqualTo(expectedDocCParentTypeMap); 13035 13036 assertThat(searchResults.get(2).getGenericDocument()).isEqualTo(docB); 13037 assertThat(searchResults.get(2).getParentTypeMap()).isEqualTo(expectedDocBParentTypeMap); 13038 13039 assertThat(searchResults.get(3).getGenericDocument()).isEqualTo(docA); 13040 assertThat(searchResults.get(3).getParentTypeMap()).isEqualTo(expectedDocAParentTypeMap); 13041 } 13042 13043 @Test 13044 @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SEARCH_RESULT_PARENT_TYPES) testQuery_wildcardProjection_polymorphism()13045 public void testQuery_wildcardProjection_polymorphism() throws Exception { 13046 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE)); 13047 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_RESULT_PARENT_TYPES)); 13048 13049 AppSearchSchema messageSchema = new AppSearchSchema.Builder("Message") 13050 .addProperty(new StringPropertyConfig.Builder("sender") 13051 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 13052 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 13053 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 13054 .build()) 13055 .addProperty(new StringPropertyConfig.Builder("content") 13056 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 13057 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 13058 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 13059 .build()) 13060 .build(); 13061 AppSearchSchema textSchema = new AppSearchSchema.Builder("Text") 13062 .addParentType("Message") 13063 .addProperty(new StringPropertyConfig.Builder("sender") 13064 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 13065 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 13066 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 13067 .build()) 13068 .addProperty(new StringPropertyConfig.Builder("content") 13069 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 13070 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 13071 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 13072 .build()) 13073 .build(); 13074 AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email") 13075 .addParentType("Message") 13076 .addProperty(new StringPropertyConfig.Builder("sender") 13077 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 13078 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 13079 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 13080 .build()) 13081 .addProperty(new StringPropertyConfig.Builder("content") 13082 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 13083 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 13084 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 13085 .build()) 13086 .build(); 13087 13088 // Schema registration 13089 mDb1.setSchemaAsync(new SetSchemaRequest.Builder() 13090 .addSchemas(messageSchema, textSchema, emailSchema).build()).get(); 13091 13092 // Index two child documents 13093 GenericDocument text = new GenericDocument.Builder<>("namespace", "id1", "Text") 13094 .setCreationTimestampMillis(1000) 13095 .setPropertyString("sender", "Some sender") 13096 .setPropertyString("content", "Some note") 13097 .build(); 13098 GenericDocument email = new GenericDocument.Builder<>("namespace", "id2", "Email") 13099 .setCreationTimestampMillis(1000) 13100 .setPropertyString("sender", "Some sender") 13101 .setPropertyString("content", "Some note") 13102 .build(); 13103 checkIsBatchResultSuccess(mDb1.putAsync(new PutDocumentsRequest.Builder() 13104 .addGenericDocuments(email, text).build())); 13105 13106 SearchResults searchResults = mDb1.search("Some", new SearchSpec.Builder() 13107 .addFilterSchemas("Message") 13108 .addProjection(SearchSpec.SCHEMA_TYPE_WILDCARD, ImmutableList.of("sender")) 13109 .addFilterProperties(SearchSpec.SCHEMA_TYPE_WILDCARD, ImmutableList.of("content")) 13110 .build()); 13111 List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults); 13112 13113 // We specified the parent document in the filter schemas, but only indexed child documents. 13114 // As we also specified a wildcard schema type projection, it should apply to the child docs 13115 // The content property must not appear. Also emailNoContent should not appear as we are 13116 // filter on the content property 13117 GenericDocument expectedText = new GenericDocument.Builder<>("namespace", "id1", "Text") 13118 .setCreationTimestampMillis(1000) 13119 .setPropertyString("sender", "Some sender") 13120 .build(); 13121 GenericDocument expectedEmail = new GenericDocument.Builder<>("namespace", "id2", "Email") 13122 .setCreationTimestampMillis(1000) 13123 .setPropertyString("sender", "Some sender") 13124 .build(); 13125 assertThat(documents).containsExactly(expectedText, expectedEmail); 13126 } 13127 13128 @Test 13129 @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SEARCH_RESULT_PARENT_TYPES) testQuery_wildcardFilterSchema_polymorphism()13130 public void testQuery_wildcardFilterSchema_polymorphism() throws Exception { 13131 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE)); 13132 assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_RESULT_PARENT_TYPES)); 13133 13134 AppSearchSchema messageSchema = new AppSearchSchema.Builder("Message") 13135 .addProperty(new StringPropertyConfig.Builder("content") 13136 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 13137 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 13138 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 13139 .build()) 13140 .build(); 13141 AppSearchSchema textSchema = new AppSearchSchema.Builder("Text") 13142 .addParentType("Message") 13143 .addProperty(new StringPropertyConfig.Builder("content") 13144 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 13145 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 13146 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 13147 .build()) 13148 .addProperty(new StringPropertyConfig.Builder("carrier") 13149 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 13150 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 13151 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 13152 .build()) 13153 .build(); 13154 AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email") 13155 .addParentType("Message") 13156 .addProperty(new StringPropertyConfig.Builder("content") 13157 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 13158 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 13159 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 13160 .build()) 13161 .addProperty(new StringPropertyConfig.Builder("attachment") 13162 .setCardinality(PropertyConfig.CARDINALITY_REQUIRED) 13163 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES) 13164 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN) 13165 .build()) 13166 .build(); 13167 13168 // Schema registration 13169 mDb1.setSchemaAsync(new SetSchemaRequest.Builder() 13170 .addSchemas(messageSchema, textSchema, emailSchema).build()).get(); 13171 13172 // Index two child documents 13173 GenericDocument text = new GenericDocument.Builder<>("namespace", "id1", "Text") 13174 .setCreationTimestampMillis(1000) 13175 .setPropertyString("content", "Some note") 13176 .setPropertyString("carrier", "Network Inc") 13177 .build(); 13178 GenericDocument email = new GenericDocument.Builder<>("namespace", "id2", "Email") 13179 .setCreationTimestampMillis(1000) 13180 .setPropertyString("content", "Some note") 13181 .setPropertyString("attachment", "Network report") 13182 .build(); 13183 13184 checkIsBatchResultSuccess(mDb1.putAsync(new PutDocumentsRequest.Builder() 13185 .addGenericDocuments(email, text).build())); 13186 13187 // Both email and text would match for "Network", but only text should match as it is in the 13188 // right property 13189 SearchResults searchResults = mDb1.search("Network", new SearchSpec.Builder() 13190 .addFilterSchemas("Message") 13191 .addFilterProperties(SearchSpec.SCHEMA_TYPE_WILDCARD, ImmutableList.of("carrier")) 13192 .build()); 13193 List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults); 13194 13195 // We specified the parent document in the filter schemas, but only indexed child documents. 13196 // As we also specified a wildcard schema type projection, it should apply to the child docs 13197 // The content property must not appear. Also emailNoContent should not appear as we are 13198 // filter on the content property 13199 GenericDocument expectedText = new GenericDocument.Builder<>("namespace", "id1", "Text") 13200 .setCreationTimestampMillis(1000) 13201 .setPropertyString("content", "Some note") 13202 .setPropertyString("carrier", "Network Inc") 13203 .build(); 13204 assertThat(documents).containsExactly(expectedText); 13205 } 13206 13207 @Test 13208 @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCORABLE_PROPERTY) testRankWithNonScorableProperty()13209 public void testRankWithNonScorableProperty() throws Exception { 13210 // TODO(b/379923400): Implement this test. 13211 } 13212 13213 @Test 13214 @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCORABLE_PROPERTY) testRankWithInvalidPropertyName()13215 public void testRankWithInvalidPropertyName() throws Exception { 13216 // TODO(b/379923400): Implement this test. 13217 } 13218 } 13219