github.com/weaviate/weaviate@v1.24.6/adapters/repos/db/crud_references_integration_test.go (about) 1 // _ _ 2 // __ _____ __ ___ ___ __ _| |_ ___ 3 // \ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \ 4 // \ V V / __/ (_| |\ V /| | (_| | || __/ 5 // \_/\_/ \___|\__,_| \_/ |_|\__,_|\__\___| 6 // 7 // Copyright © 2016 - 2024 Weaviate B.V. All rights reserved. 8 // 9 // CONTACT: hello@weaviate.io 10 // 11 12 //go:build integrationTest 13 // +build integrationTest 14 15 package db 16 17 import ( 18 "context" 19 "fmt" 20 "log" 21 "testing" 22 23 "github.com/go-openapi/strfmt" 24 "github.com/sirupsen/logrus" 25 "github.com/stretchr/testify/assert" 26 "github.com/stretchr/testify/require" 27 "github.com/weaviate/weaviate/entities/additional" 28 "github.com/weaviate/weaviate/entities/models" 29 "github.com/weaviate/weaviate/entities/schema" 30 "github.com/weaviate/weaviate/entities/schema/crossref" 31 "github.com/weaviate/weaviate/entities/search" 32 enthnsw "github.com/weaviate/weaviate/entities/vectorindex/hnsw" 33 ) 34 35 func TestNestedReferences(t *testing.T) { 36 dirName := t.TempDir() 37 38 refSchema := schema.Schema{ 39 Objects: &models.Schema{ 40 Classes: []*models.Class{ 41 { 42 Class: "Planet", 43 VectorIndexConfig: enthnsw.NewDefaultUserConfig(), 44 InvertedIndexConfig: invertedConfig(), 45 Properties: []*models.Property{ 46 { 47 Name: "name", 48 DataType: schema.DataTypeText.PropString(), 49 Tokenization: models.PropertyTokenizationWhitespace, 50 }, 51 }, 52 }, 53 { 54 Class: "Continent", 55 VectorIndexConfig: enthnsw.NewDefaultUserConfig(), 56 InvertedIndexConfig: invertedConfig(), 57 Properties: []*models.Property{ 58 { 59 Name: "name", 60 DataType: schema.DataTypeText.PropString(), 61 Tokenization: models.PropertyTokenizationWhitespace, 62 }, 63 { 64 Name: "onPlanet", 65 DataType: []string{"Planet"}, 66 }, 67 }, 68 }, 69 { 70 Class: "Country", 71 VectorIndexConfig: enthnsw.NewDefaultUserConfig(), 72 InvertedIndexConfig: invertedConfig(), 73 Properties: []*models.Property{ 74 { 75 Name: "name", 76 DataType: schema.DataTypeText.PropString(), 77 Tokenization: models.PropertyTokenizationWhitespace, 78 }, 79 { 80 Name: "onContinent", 81 DataType: []string{"Continent"}, 82 }, 83 }, 84 }, 85 { 86 Class: "City", 87 VectorIndexConfig: enthnsw.NewDefaultUserConfig(), 88 InvertedIndexConfig: invertedConfig(), 89 Properties: []*models.Property{ 90 { 91 Name: "name", 92 DataType: schema.DataTypeText.PropString(), 93 Tokenization: models.PropertyTokenizationWhitespace, 94 }, 95 { 96 Name: "inCountry", 97 DataType: []string{"Country"}, 98 }, 99 }, 100 }, 101 { 102 Class: "Place", 103 VectorIndexConfig: enthnsw.NewDefaultUserConfig(), 104 InvertedIndexConfig: invertedConfig(), 105 Properties: []*models.Property{ 106 { 107 Name: "name", 108 DataType: schema.DataTypeText.PropString(), 109 Tokenization: models.PropertyTokenizationWhitespace, 110 }, 111 { 112 Name: "inCity", 113 DataType: []string{"City"}, 114 }, 115 }, 116 }, 117 }, 118 }, 119 } 120 logger := logrus.New() 121 schemaGetter := &fakeSchemaGetter{ 122 schema: schema.Schema{Objects: &models.Schema{Classes: nil}}, 123 shardState: singleShardState(), 124 } 125 repo, err := New(logger, Config{ 126 MemtablesFlushDirtyAfter: 60, 127 RootPath: dirName, 128 MaxImportGoroutinesFactor: 1, 129 }, &fakeRemoteClient{}, &fakeNodeResolver{}, &fakeRemoteNodeClient{}, &fakeReplicationClient{}, nil) 130 require.Nil(t, err) 131 repo.SetSchemaGetter(schemaGetter) 132 require.Nil(t, repo.WaitForStartup(testCtx())) 133 defer repo.Shutdown(context.Background()) 134 migrator := NewMigrator(repo, logger) 135 136 t.Run("adding all classes to the schema", func(t *testing.T) { 137 for _, class := range refSchema.Objects.Classes { 138 t.Run(fmt.Sprintf("add %s", class.Class), func(t *testing.T) { 139 err := migrator.AddClass(context.Background(), class, schemaGetter.shardState) 140 require.Nil(t, err) 141 }) 142 } 143 }) 144 145 // update schema getter so it's in sync with class 146 schemaGetter.schema = refSchema 147 148 t.Run("importing some thing objects with references", func(t *testing.T) { 149 objects := []models.Object{ 150 { 151 Class: "Planet", 152 Properties: map[string]interface{}{ 153 "name": "Earth", 154 }, 155 ID: "32c69af9-cbbe-4ec9-bf6c-365cd6c22fdf", 156 CreationTimeUnix: 1566464889, 157 }, 158 { 159 Class: "Continent", 160 Properties: map[string]interface{}{ 161 "name": "North America", 162 "onPlanet": models.MultipleRef{ 163 &models.SingleRef{ 164 Beacon: "weaviate://localhost/32c69af9-cbbe-4ec9-bf6c-365cd6c22fdf", 165 }, 166 }, 167 }, 168 ID: "4aad8154-e7f3-45b8-81a6-725171419e55", 169 CreationTimeUnix: 1566464892, 170 }, 171 { 172 Class: "Country", 173 Properties: map[string]interface{}{ 174 "name": "USA", 175 "onContinent": models.MultipleRef{ 176 &models.SingleRef{ 177 Beacon: "weaviate://localhost/4aad8154-e7f3-45b8-81a6-725171419e55", 178 }, 179 }, 180 }, 181 ID: "18c80a16-346a-477d-849d-9d92e5040ac9", 182 CreationTimeUnix: 1566464896, 183 }, 184 { 185 Class: "City", 186 Properties: map[string]interface{}{ 187 "name": "San Francisco", 188 "inCountry": models.MultipleRef{ 189 &models.SingleRef{ 190 Beacon: "weaviate://localhost/18c80a16-346a-477d-849d-9d92e5040ac9", 191 }, 192 }, 193 }, 194 ID: "2297e094-6218-43d4-85b1-3d20af752f23", 195 CreationTimeUnix: 1566464899, 196 }, 197 { 198 Class: "Place", 199 Properties: map[string]interface{}{ 200 "name": "Tim Apple's Fruit Bar", 201 "inCity": models.MultipleRef{ 202 &models.SingleRef{ 203 Beacon: "weaviate://localhost/2297e094-6218-43d4-85b1-3d20af752f23", 204 }, 205 }, 206 }, 207 ID: "4ef47fb0-3cf5-44fc-b378-9e217dff13ac", 208 CreationTimeUnix: 1566464904, 209 }, 210 } 211 212 for _, thing := range objects { 213 t.Run(fmt.Sprintf("add %s", thing.ID), func(t *testing.T) { 214 err := repo.PutObject(context.Background(), &thing, []float32{1, 2, 3, 4, 5, 6, 7}, nil, nil) 215 require.Nil(t, err) 216 }) 217 } 218 }) 219 220 t.Run("fully resolving the place", func(t *testing.T) { 221 expectedSchema := map[string]interface{}{ 222 "inCity": []interface{}{ 223 search.LocalRef{ 224 Class: "City", 225 Fields: map[string]interface{}{ 226 "inCountry": []interface{}{ 227 search.LocalRef{ 228 Class: "Country", 229 Fields: map[string]interface{}{ 230 "onContinent": []interface{}{ 231 search.LocalRef{ 232 Class: "Continent", 233 Fields: map[string]interface{}{ 234 "onPlanet": []interface{}{ 235 search.LocalRef{ 236 Class: "Planet", 237 Fields: map[string]interface{}{ 238 "name": "Earth", 239 "id": strfmt.UUID("32c69af9-cbbe-4ec9-bf6c-365cd6c22fdf"), 240 }, 241 }, 242 }, 243 "name": "North America", 244 "id": strfmt.UUID("4aad8154-e7f3-45b8-81a6-725171419e55"), 245 }, 246 }, 247 }, 248 "name": "USA", 249 "id": strfmt.UUID("18c80a16-346a-477d-849d-9d92e5040ac9"), 250 }, 251 }, 252 }, 253 "name": "San Francisco", 254 "id": strfmt.UUID("2297e094-6218-43d4-85b1-3d20af752f23"), 255 }, 256 }, 257 }, 258 "name": "Tim Apple's Fruit Bar", 259 "id": strfmt.UUID("4ef47fb0-3cf5-44fc-b378-9e217dff13ac"), 260 } 261 262 res, err := repo.ObjectByID(context.Background(), "4ef47fb0-3cf5-44fc-b378-9e217dff13ac", fullyNestedSelectProperties(), additional.Properties{}, "") 263 require.Nil(t, err) 264 assert.Equal(t, expectedSchema, res.Schema) 265 }) 266 267 t.Run("fully resolving the place with vectors", func(t *testing.T) { 268 expectedSchema := map[string]interface{}{ 269 "inCity": []interface{}{ 270 search.LocalRef{ 271 Class: "City", 272 Fields: map[string]interface{}{ 273 "inCountry": []interface{}{ 274 search.LocalRef{ 275 Class: "Country", 276 Fields: map[string]interface{}{ 277 "onContinent": []interface{}{ 278 search.LocalRef{ 279 Class: "Continent", 280 Fields: map[string]interface{}{ 281 "onPlanet": []interface{}{ 282 search.LocalRef{ 283 Class: "Planet", 284 Fields: map[string]interface{}{ 285 "name": "Earth", 286 "id": strfmt.UUID("32c69af9-cbbe-4ec9-bf6c-365cd6c22fdf"), 287 "vector": []float32{1, 2, 3, 4, 5, 6, 7}, 288 }, 289 }, 290 }, 291 "name": "North America", 292 "id": strfmt.UUID("4aad8154-e7f3-45b8-81a6-725171419e55"), 293 "vector": []float32{1, 2, 3, 4, 5, 6, 7}, 294 }, 295 }, 296 }, 297 "name": "USA", 298 "id": strfmt.UUID("18c80a16-346a-477d-849d-9d92e5040ac9"), 299 "vector": []float32{1, 2, 3, 4, 5, 6, 7}, 300 }, 301 }, 302 }, 303 "name": "San Francisco", 304 "id": strfmt.UUID("2297e094-6218-43d4-85b1-3d20af752f23"), 305 "vector": []float32{1, 2, 3, 4, 5, 6, 7}, 306 }, 307 }, 308 }, 309 "name": "Tim Apple's Fruit Bar", 310 "id": strfmt.UUID("4ef47fb0-3cf5-44fc-b378-9e217dff13ac"), 311 } 312 313 res, err := repo.ObjectByID(context.Background(), "4ef47fb0-3cf5-44fc-b378-9e217dff13ac", fullyNestedSelectPropertiesWithVector(), additional.Properties{}, "") 314 require.Nil(t, err) 315 assert.Equal(t, expectedSchema, res.Schema) 316 }) 317 318 t.Run("partially resolving the place", func(t *testing.T) { 319 expectedSchema := map[string]interface{}{ 320 "inCity": []interface{}{ 321 search.LocalRef{ 322 Class: "City", 323 Fields: map[string]interface{}{ 324 "name": "San Francisco", 325 "id": strfmt.UUID("2297e094-6218-43d4-85b1-3d20af752f23"), 326 // why is inCountry present here? We didn't specify it our select 327 // properties. Note it is "inCountry" with a lowercase letter 328 // (meaning unresolved) whereas "inCountry" would mean it was 329 // resolved. In GraphQL this property would simply be hidden (as 330 // the GQL is unaware of unresolved properties) 331 // However, for caching and other queries it is helpful that this 332 // info is still present, the important thing is that we're 333 // avoiding the costly resolving of it, if we don't need it. 334 "inCountry": models.MultipleRef{ 335 &models.SingleRef{ 336 Beacon: "weaviate://localhost/18c80a16-346a-477d-849d-9d92e5040ac9", 337 }, 338 }, 339 }, 340 }, 341 }, 342 "name": "Tim Apple's Fruit Bar", 343 "id": strfmt.UUID("4ef47fb0-3cf5-44fc-b378-9e217dff13ac"), 344 } 345 346 res, err := repo.ObjectByID(context.Background(), "4ef47fb0-3cf5-44fc-b378-9e217dff13ac", partiallyNestedSelectProperties(), additional.Properties{}, "") 347 require.Nil(t, err) 348 assert.Equal(t, expectedSchema, res.Schema) 349 }) 350 351 t.Run("resolving without any refs", func(t *testing.T) { 352 res, err := repo.ObjectByID(context.Background(), "4ef47fb0-3cf5-44fc-b378-9e217dff13ac", search.SelectProperties{}, additional.Properties{}, "") 353 354 expectedSchema := map[string]interface{}{ 355 "id": strfmt.UUID("4ef47fb0-3cf5-44fc-b378-9e217dff13ac"), 356 "inCity": models.MultipleRef{ 357 &models.SingleRef{ 358 Beacon: "weaviate://localhost/2297e094-6218-43d4-85b1-3d20af752f23", 359 }, 360 }, 361 "name": "Tim Apple's Fruit Bar", 362 } 363 364 require.Nil(t, err) 365 366 assert.Equal(t, expectedSchema, res.Schema, "does not contain any resolved refs") 367 }) 368 369 t.Run("adding a new place to verify idnexing is constantly happening in the background", func(t *testing.T) { 370 newPlace := models.Object{ 371 Class: "Place", 372 Properties: map[string]interface{}{ 373 "name": "John Oliver's Avocados", 374 "inCity": models.MultipleRef{ 375 &models.SingleRef{ 376 Beacon: "weaviate://localhost/2297e094-6218-43d4-85b1-3d20af752f23", 377 }, 378 }, 379 }, 380 ID: "0f02d525-902d-4dc0-8052-647cb420c1a6", 381 CreationTimeUnix: 1566464912, 382 } 383 384 err := repo.PutObject(context.Background(), &newPlace, []float32{1, 2, 3, 4, 5, 6, 7}, nil, nil) 385 require.Nil(t, err) 386 }) 387 } 388 389 func fullyNestedSelectProperties() search.SelectProperties { 390 return search.SelectProperties{ 391 search.SelectProperty{ 392 Name: "inCity", 393 IsPrimitive: false, 394 Refs: []search.SelectClass{ 395 { 396 ClassName: "City", 397 RefProperties: search.SelectProperties{ 398 search.SelectProperty{ 399 Name: "inCountry", 400 IsPrimitive: false, 401 Refs: []search.SelectClass{ 402 { 403 ClassName: "Country", 404 RefProperties: search.SelectProperties{ 405 search.SelectProperty{ 406 Name: "onContinent", 407 IsPrimitive: false, 408 Refs: []search.SelectClass{ 409 { 410 ClassName: "Continent", 411 RefProperties: search.SelectProperties{ 412 search.SelectProperty{ 413 Name: "onPlanet", 414 IsPrimitive: false, 415 Refs: []search.SelectClass{ 416 { 417 ClassName: "Planet", 418 RefProperties: nil, 419 }, 420 }, 421 }, 422 }, 423 }, 424 }, 425 }, 426 }, 427 }, 428 }, 429 }, 430 }, 431 }, 432 }, 433 }, 434 } 435 } 436 437 func fullyNestedSelectPropertiesWithVector() search.SelectProperties { 438 return search.SelectProperties{ 439 search.SelectProperty{ 440 Name: "inCity", 441 IsPrimitive: false, 442 Refs: []search.SelectClass{ 443 { 444 ClassName: "City", 445 RefProperties: search.SelectProperties{ 446 search.SelectProperty{ 447 Name: "inCountry", 448 IsPrimitive: false, 449 Refs: []search.SelectClass{ 450 { 451 ClassName: "Country", 452 RefProperties: search.SelectProperties{ 453 search.SelectProperty{ 454 Name: "onContinent", 455 IsPrimitive: false, 456 Refs: []search.SelectClass{ 457 { 458 ClassName: "Continent", 459 RefProperties: search.SelectProperties{ 460 search.SelectProperty{ 461 Name: "onPlanet", 462 IsPrimitive: false, 463 Refs: []search.SelectClass{ 464 { 465 ClassName: "Planet", 466 RefProperties: nil, 467 AdditionalProperties: additional.Properties{ 468 Vector: true, 469 }, 470 }, 471 }, 472 }, 473 }, 474 AdditionalProperties: additional.Properties{ 475 Vector: true, 476 }, 477 }, 478 }, 479 }, 480 }, 481 AdditionalProperties: additional.Properties{ 482 Vector: true, 483 }, 484 }, 485 }, 486 }, 487 }, 488 AdditionalProperties: additional.Properties{ 489 Vector: true, 490 }, 491 }, 492 }, 493 }, 494 } 495 } 496 497 func partiallyNestedSelectProperties() search.SelectProperties { 498 return search.SelectProperties{ 499 search.SelectProperty{ 500 Name: "inCity", 501 IsPrimitive: false, 502 Refs: []search.SelectClass{ 503 { 504 ClassName: "City", 505 RefProperties: search.SelectProperties{}, 506 }, 507 }, 508 }, 509 } 510 } 511 512 func GetDimensionsFromRepo(repo *DB, className string) int { 513 if !repo.config.TrackVectorDimensions { 514 log.Printf("Vector dimensions tracking is disabled, returning 0") 515 return 0 516 } 517 index := repo.GetIndex(schema.ClassName(className)) 518 sum := 0 519 index.ForEachShard(func(name string, shard ShardLike) error { 520 sum += shard.Dimensions() 521 return nil 522 }) 523 return sum 524 } 525 526 func GetQuantizedDimensionsFromRepo(repo *DB, className string, segments int) int { 527 if !repo.config.TrackVectorDimensions { 528 log.Printf("Vector dimensions tracking is disabled, returning 0") 529 return 0 530 } 531 index := repo.GetIndex(schema.ClassName(className)) 532 sum := 0 533 index.ForEachShard(func(name string, shard ShardLike) error { 534 sum += shard.QuantizedDimensions(segments) 535 return nil 536 }) 537 return sum 538 } 539 540 func Test_AddingReferenceOneByOne(t *testing.T) { 541 dirName := t.TempDir() 542 543 sch := schema.Schema{ 544 Objects: &models.Schema{ 545 Classes: []*models.Class{ 546 { 547 Class: "AddingReferencesTestTarget", 548 VectorIndexConfig: enthnsw.NewDefaultUserConfig(), 549 InvertedIndexConfig: invertedConfig(), 550 Properties: []*models.Property{ 551 { 552 Name: "name", 553 DataType: schema.DataTypeText.PropString(), 554 Tokenization: models.PropertyTokenizationWhitespace, 555 }, 556 }, 557 }, 558 { 559 Class: "AddingReferencesTestSource", 560 VectorIndexConfig: enthnsw.NewDefaultUserConfig(), 561 InvertedIndexConfig: invertedConfig(), 562 Properties: []*models.Property{ 563 { 564 Name: "name", 565 DataType: schema.DataTypeText.PropString(), 566 Tokenization: models.PropertyTokenizationWhitespace, 567 }, 568 { 569 Name: "toTarget", 570 DataType: []string{"AddingReferencesTestTarget"}, 571 }, 572 }, 573 }, 574 }, 575 }, 576 } 577 logger := logrus.New() 578 schemaGetter := &fakeSchemaGetter{ 579 schema: schema.Schema{Objects: &models.Schema{Classes: nil}}, 580 shardState: singleShardState(), 581 } 582 repo, err := New(logger, Config{ 583 MemtablesFlushDirtyAfter: 60, 584 RootPath: dirName, 585 MaxImportGoroutinesFactor: 1, 586 TrackVectorDimensions: true, 587 }, &fakeRemoteClient{}, &fakeNodeResolver{}, &fakeRemoteNodeClient{}, &fakeReplicationClient{}, nil) 588 require.Nil(t, err) 589 repo.SetSchemaGetter(schemaGetter) 590 require.Nil(t, repo.WaitForStartup(testCtx())) 591 defer repo.Shutdown(context.Background()) 592 migrator := NewMigrator(repo, logger) 593 594 t.Run("add required classes", func(t *testing.T) { 595 for _, class := range sch.Objects.Classes { 596 t.Run(fmt.Sprintf("add %s", class.Class), func(t *testing.T) { 597 err := migrator.AddClass(context.Background(), class, schemaGetter.shardState) 598 require.Nil(t, err) 599 }) 600 } 601 }) 602 603 schemaGetter.schema = sch 604 targetID := strfmt.UUID("a4a92239-e748-4e55-bbbd-f606926619a7") 605 target2ID := strfmt.UUID("325084e7-4faa-43a5-b2b1-56e207be169a") 606 sourceID := strfmt.UUID("0826c61b-85c1-44ac-aebb-cfd07ace6a57") 607 608 t.Run("add objects", func(t *testing.T) { 609 err := repo.PutObject(context.Background(), &models.Object{ 610 ID: sourceID, 611 Class: "AddingReferencesTestSource", 612 Properties: map[string]interface{}{ 613 "name": "source item", 614 }, 615 }, []float32{0.5}, nil, nil) 616 require.Nil(t, err) 617 618 err = repo.PutObject(context.Background(), &models.Object{ 619 ID: targetID, 620 Class: "AddingReferencesTestTarget", 621 Properties: map[string]interface{}{ 622 "name": "target item", 623 }, 624 }, []float32{0.5}, nil, nil) 625 require.Nil(t, err) 626 627 err = repo.PutObject(context.Background(), &models.Object{ 628 ID: target2ID, 629 Class: "AddingReferencesTestTarget", 630 Properties: map[string]interface{}{ 631 "name": "another target item", 632 }, 633 }, []float32{0.5}, nil, nil) 634 require.Nil(t, err) 635 }) 636 637 t.Run("add reference between them", func(t *testing.T) { 638 // Get dimensions before adding reference 639 sourceShardDimension := GetDimensionsFromRepo(repo, "AddingReferencesTestSource") 640 targetShardDimension := GetDimensionsFromRepo(repo, "AddingReferencesTestTarget") 641 642 source := crossref.NewSource("AddingReferencesTestSource", "toTarget", sourceID) 643 target := crossref.New("localhost", "", targetID) 644 645 err := repo.AddReference(context.Background(), source, target, nil, "") 646 assert.Nil(t, err) 647 648 // Check dimensions after adding reference 649 sourceDimensionAfter := GetDimensionsFromRepo(repo, "AddingReferencesTestSource") 650 targetDimensionAfter := GetDimensionsFromRepo(repo, "AddingReferencesTestTarget") 651 652 require.Equalf(t, sourceShardDimension, sourceDimensionAfter, "dimensions of source should not change") 653 require.Equalf(t, targetShardDimension, targetDimensionAfter, "dimensions of target should not change") 654 }) 655 656 t.Run("check reference was added", func(t *testing.T) { 657 source, err := repo.ObjectByID(context.Background(), sourceID, nil, additional.Properties{}, "") 658 require.Nil(t, err) 659 require.NotNil(t, source) 660 require.NotNil(t, source.Object()) 661 require.NotNil(t, source.Object().Properties) 662 663 refs := source.Object().Properties.(map[string]interface{})["toTarget"] 664 refsSlice, ok := refs.(models.MultipleRef) 665 require.True(t, ok, 666 fmt.Sprintf("toTarget must be models.MultipleRef, but got %#v", refs)) 667 668 foundBeacons := []string{} 669 for _, ref := range refsSlice { 670 foundBeacons = append(foundBeacons, ref.Beacon.String()) 671 } 672 expectedBeacons := []string{ 673 fmt.Sprintf("weaviate://localhost/%s", targetID), 674 } 675 676 assert.ElementsMatch(t, foundBeacons, expectedBeacons) 677 }) 678 679 t.Run("reference a second target", func(t *testing.T) { 680 source := crossref.NewSource("AddingReferencesTestSource", "toTarget", sourceID) 681 target := crossref.New("localhost", "", target2ID) 682 683 err := repo.AddReference(context.Background(), source, target, nil, "") 684 assert.Nil(t, err) 685 }) 686 687 t.Run("check both references are now present", func(t *testing.T) { 688 source, err := repo.ObjectByID(context.Background(), sourceID, nil, additional.Properties{}, "") 689 require.Nil(t, err) 690 require.NotNil(t, source) 691 require.NotNil(t, source.Object()) 692 require.NotNil(t, source.Object().Properties) 693 694 refs := source.Object().Properties.(map[string]interface{})["toTarget"] 695 refsSlice, ok := refs.(models.MultipleRef) 696 require.True(t, ok, 697 fmt.Sprintf("toTarget must be models.MultipleRef, but got %#v", refs)) 698 699 foundBeacons := []string{} 700 for _, ref := range refsSlice { 701 foundBeacons = append(foundBeacons, ref.Beacon.String()) 702 } 703 expectedBeacons := []string{ 704 fmt.Sprintf("weaviate://localhost/%s", targetID), 705 fmt.Sprintf("weaviate://localhost/%s", target2ID), 706 } 707 708 assert.ElementsMatch(t, foundBeacons, expectedBeacons) 709 }) 710 }