github.com/weaviate/weaviate@v1.24.6/usecases/schema/update_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 package schema 13 14 import ( 15 "context" 16 "encoding/json" 17 "testing" 18 19 "github.com/pkg/errors" 20 "github.com/stretchr/testify/assert" 21 "github.com/stretchr/testify/require" 22 "github.com/weaviate/weaviate/entities/models" 23 "github.com/weaviate/weaviate/entities/schema" 24 "github.com/weaviate/weaviate/usecases/config" 25 ) 26 27 // As of now, most class settings are immutable, but we need to allow some 28 // specific updates, such as the vector index config 29 func TestClassUpdates(t *testing.T) { 30 t.Run("a class which doesn't exist", func(t *testing.T) { 31 err := newSchemaManager().UpdateClass(context.Background(), 32 nil, "WrongClass", &models.Class{}) 33 require.NotNil(t, err) 34 assert.Equal(t, ErrNotFound, err) 35 }) 36 37 t.Run("various immutable and mutable fields", func(t *testing.T) { 38 type test struct { 39 name string 40 initial *models.Class 41 update *models.Class 42 expectedError error 43 } 44 45 tests := []test{ 46 { 47 name: "attempting a name change", 48 initial: &models.Class{Class: "InitialName"}, 49 update: &models.Class{Class: "UpdatedName"}, 50 expectedError: errors.Errorf( 51 "class name is immutable: " + 52 "attempted change from \"InitialName\" to \"UpdatedName\""), 53 }, 54 { 55 name: "attempting to modify the vectorizer", 56 initial: &models.Class{Class: "InitialName", Vectorizer: "model1"}, 57 update: &models.Class{Class: "InitialName", Vectorizer: "model2"}, 58 expectedError: errors.Errorf( 59 "vectorizer is immutable: " + 60 "attempted change from \"model1\" to \"model2\""), 61 }, 62 { 63 name: "attempting to modify the vector index type", 64 initial: &models.Class{Class: "InitialName", VectorIndexType: "hnsw"}, 65 update: &models.Class{Class: "InitialName", VectorIndexType: "lsh"}, 66 expectedError: errors.Errorf( 67 "vector index type is immutable: " + 68 "attempted change from \"hnsw\" to \"lsh\""), 69 }, 70 { 71 name: "attempting to add a property", 72 initial: &models.Class{Class: "InitialName"}, 73 update: &models.Class{ 74 Class: "InitialName", 75 Properties: []*models.Property{ 76 { 77 Name: "newProp", 78 }, 79 }, 80 }, 81 expectedError: errors.Errorf( 82 "properties cannot be updated through updating the class. Use the add " + 83 "property feature (e.g. \"POST /v1/schema/{className}/properties\") " + 84 "to add additional properties"), 85 }, 86 { 87 name: "leaving properties unchanged", 88 initial: &models.Class{ 89 Class: "InitialName", 90 Properties: []*models.Property{ 91 { 92 Name: "aProp", 93 DataType: schema.DataTypeText.PropString(), 94 }, 95 }, 96 }, 97 update: &models.Class{ 98 Class: "InitialName", 99 Properties: []*models.Property{ 100 { 101 Name: "aProp", 102 DataType: schema.DataTypeText.PropString(), 103 }, 104 }, 105 }, 106 expectedError: nil, 107 }, 108 { 109 name: "attempting to rename a property", 110 initial: &models.Class{ 111 Class: "InitialName", 112 Properties: []*models.Property{ 113 { 114 Name: "aProp", 115 DataType: schema.DataTypeText.PropString(), 116 }, 117 }, 118 }, 119 update: &models.Class{ 120 Class: "InitialName", 121 Properties: []*models.Property{ 122 { 123 Name: "changedProp", 124 DataType: schema.DataTypeText.PropString(), 125 }, 126 }, 127 }, 128 expectedError: errors.Errorf( 129 "properties cannot be updated through updating the class. Use the add " + 130 "property feature (e.g. \"POST /v1/schema/{className}/properties\") " + 131 "to add additional properties"), 132 }, 133 { 134 name: "attempting to update the inverted index cleanup interval", 135 initial: &models.Class{ 136 Class: "InitialName", 137 InvertedIndexConfig: &models.InvertedIndexConfig{ 138 CleanupIntervalSeconds: 17, 139 }, 140 }, 141 update: &models.Class{ 142 Class: "InitialName", 143 InvertedIndexConfig: &models.InvertedIndexConfig{ 144 CleanupIntervalSeconds: 18, 145 Bm25: &models.BM25Config{ 146 K1: config.DefaultBM25k1, 147 B: config.DefaultBM25b, 148 }, 149 }, 150 }, 151 }, 152 { 153 name: "attempting to update the inverted index BM25 config", 154 initial: &models.Class{ 155 Class: "InitialName", 156 InvertedIndexConfig: &models.InvertedIndexConfig{ 157 CleanupIntervalSeconds: 18, 158 Bm25: &models.BM25Config{ 159 K1: 1.012, 160 B: 0.125, 161 }, 162 }, 163 }, 164 update: &models.Class{ 165 Class: "InitialName", 166 InvertedIndexConfig: &models.InvertedIndexConfig{ 167 CleanupIntervalSeconds: 18, 168 Bm25: &models.BM25Config{ 169 K1: 1.012, 170 B: 0.125, 171 }, 172 }, 173 }, 174 }, 175 { 176 name: "attempting to update the inverted index Stopwords config", 177 initial: &models.Class{ 178 Class: "InitialName", 179 InvertedIndexConfig: &models.InvertedIndexConfig{ 180 CleanupIntervalSeconds: 18, 181 Stopwords: &models.StopwordConfig{ 182 Preset: "en", 183 }, 184 }, 185 }, 186 update: &models.Class{ 187 Class: "InitialName", 188 InvertedIndexConfig: &models.InvertedIndexConfig{ 189 CleanupIntervalSeconds: 18, 190 Stopwords: &models.StopwordConfig{ 191 Preset: "none", 192 Additions: []string{"banana", "passionfruit", "kiwi"}, 193 Removals: []string{"a", "the"}, 194 }, 195 }, 196 }, 197 }, 198 { 199 name: "attempting to update module config", 200 initial: &models.Class{ 201 Class: "InitialName", 202 ModuleConfig: map[string]interface{}{ 203 "my-module1": map[string]interface{}{ 204 "my-setting": "some-value", 205 }, 206 }, 207 }, 208 update: &models.Class{ 209 Class: "InitialName", 210 ModuleConfig: map[string]interface{}{ 211 "my-module1": map[string]interface{}{ 212 "my-setting": "updated-value", 213 }, 214 }, 215 }, 216 expectedError: errors.Errorf("module config is immutable"), 217 }, 218 { 219 name: "updating vector index config", 220 initial: &models.Class{ 221 Class: "InitialName", 222 VectorIndexConfig: map[string]interface{}{ 223 "some-setting": "old-value", 224 }, 225 }, 226 update: &models.Class{ 227 Class: "InitialName", 228 VectorIndexConfig: map[string]interface{}{ 229 "some-setting": "new-value", 230 }, 231 }, 232 expectedError: nil, 233 }, 234 } 235 236 for _, test := range tests { 237 t.Run(test.name, func(t *testing.T) { 238 sm := newSchemaManager() 239 assert.Nil(t, sm.AddClass(context.Background(), nil, test.initial)) 240 err := sm.UpdateClass(context.Background(), nil, test.initial.Class, test.update) 241 if test.expectedError == nil { 242 assert.Nil(t, err) 243 } else { 244 require.NotNil(t, err, "update must error") 245 assert.Equal(t, test.expectedError.Error(), err.Error()) 246 } 247 }) 248 } 249 }) 250 251 t.Run("update vector index config", func(t *testing.T) { 252 t.Run("with a validation error", func(t *testing.T) { 253 sm := newSchemaManager() 254 migrator := &configMigrator{ 255 vectorConfigValidationError: errors.Errorf("don't think so!"), 256 } 257 sm.migrator = migrator 258 259 t.Run("create an initial class", func(t *testing.T) { 260 err := sm.AddClass(context.Background(), nil, &models.Class{ 261 Class: "ClassWithVectorIndexConfig", 262 VectorIndexConfig: map[string]interface{}{ 263 "setting-1": "value-1", 264 }, 265 }) 266 267 assert.Nil(t, err) 268 }) 269 270 t.Run("attempt an update of the vector index config", func(t *testing.T) { 271 err := sm.UpdateClass(context.Background(), nil, 272 "ClassWithVectorIndexConfig", &models.Class{ 273 Class: "ClassWithVectorIndexConfig", 274 VectorIndexConfig: map[string]interface{}{ 275 "setting-1": "updated-value", 276 }, 277 }) 278 expectedErrMsg := "vector index config: don't think so!" 279 expectedValidateCalledWith := fakeVectorConfig{ 280 raw: map[string]interface{}{ 281 "distance": "cosine", 282 "setting-1": "updated-value", 283 }, 284 } 285 expectedUpdateCalled := false 286 287 require.NotNil(t, err) 288 assert.Equal(t, expectedErrMsg, err.Error()) 289 assert.Equal(t, expectedValidateCalledWith, migrator.vectorConfigValidateCalledWith) 290 assert.Equal(t, expectedUpdateCalled, migrator.vectorConfigUpdateCalled) 291 }) 292 }) 293 294 t.Run("with a valid update", func(t *testing.T) { 295 sm := newSchemaManager() 296 migrator := &configMigrator{} 297 sm.migrator = migrator 298 299 t.Run("create an initial class", func(t *testing.T) { 300 err := sm.AddClass(context.Background(), nil, &models.Class{ 301 Class: "ClassWithVectorIndexConfig", 302 VectorIndexConfig: map[string]interface{}{ 303 "setting-1": "value-1", 304 }, 305 }) 306 307 assert.Nil(t, err) 308 }) 309 310 t.Run("update the vector index config", func(t *testing.T) { 311 err := sm.UpdateClass(context.Background(), nil, 312 "ClassWithVectorIndexConfig", &models.Class{ 313 Class: "ClassWithVectorIndexConfig", 314 VectorIndexConfig: map[string]interface{}{ 315 "setting-1": "updated-value", 316 }, 317 }) 318 expectedValidateCalledWith := fakeVectorConfig{ 319 raw: map[string]interface{}{ 320 "distance": "cosine", 321 "setting-1": "updated-value", 322 }, 323 } 324 expectedUpdateCalledWith := fakeVectorConfig{ 325 raw: map[string]interface{}{ 326 "distance": "cosine", 327 "setting-1": "updated-value", 328 }, 329 } 330 expectedUpdateCalled := true 331 332 require.Nil(t, err) 333 assert.Equal(t, expectedValidateCalledWith, migrator.vectorConfigValidateCalledWith) 334 assert.Equal(t, expectedUpdateCalledWith, migrator.vectorConfigUpdateCalledWith) 335 assert.Equal(t, expectedUpdateCalled, migrator.vectorConfigUpdateCalled) 336 }) 337 338 t.Run("the update is reflected", func(t *testing.T) { 339 class := sm.getClassByName("ClassWithVectorIndexConfig") 340 require.NotNil(t, class) 341 expectedVectorIndexConfig := fakeVectorConfig{ 342 raw: map[string]interface{}{ 343 "distance": "cosine", 344 "setting-1": "updated-value", 345 }, 346 } 347 348 assert.Equal(t, expectedVectorIndexConfig, class.VectorIndexConfig) 349 }) 350 }) 351 }) 352 353 t.Run("update sharding config", func(t *testing.T) { 354 t.Run("with a validation error (immutable field)", func(t *testing.T) { 355 sm := newSchemaManager() 356 migrator := &NilMigrator{} 357 sm.migrator = migrator 358 359 t.Run("create an initial class", func(t *testing.T) { 360 err := sm.AddClass(context.Background(), nil, &models.Class{ 361 Class: "ClassWithShardingConfig", 362 }) 363 364 assert.Nil(t, err) 365 }) 366 367 t.Run("attempt an update of the vector index config", func(t *testing.T) { 368 err := sm.UpdateClass(context.Background(), nil, 369 "ClassWithShardingConfig", &models.Class{ 370 Class: "ClassWithShardingConfig", 371 ShardingConfig: map[string]interface{}{ 372 "desiredCount": json.Number("7"), 373 }, 374 }) 375 expectedErrMsg := "re-sharding not supported yet: shard count is immutable: attempted change from \"1\" to \"7\"" 376 require.NotNil(t, err) 377 assert.Contains(t, err.Error(), expectedErrMsg) 378 }) 379 }) 380 }) 381 } 382 383 func TestClassUpdate_ValidateVectorIndexConfigs(t *testing.T) { 384 type testCase struct { 385 name string 386 initial *models.Class 387 updated *models.Class 388 expectedErrMsg string 389 } 390 391 createClass := func(cfg map[string]models.VectorConfig) *models.Class { 392 return &models.Class{ 393 Class: "TargetVectors", 394 VectorConfig: cfg, 395 } 396 } 397 398 vcFlatContextionary := models.VectorConfig{ 399 VectorIndexType: "flat", 400 Vectorizer: map[string]interface{}{ 401 "text2vec-contextionary": "some-settings", 402 }, 403 VectorIndexConfig: map[string]interface{}{ 404 "setting-flat": "value-flat", 405 }, 406 } 407 vcHnswContextionary := models.VectorConfig{ 408 VectorIndexType: "hnsw", 409 Vectorizer: map[string]interface{}{ 410 "text2vec-contextionary": "some-settings", 411 }, 412 VectorIndexConfig: map[string]interface{}{ 413 "setting-hnsw": "value-hnsw", 414 }, 415 } 416 417 _ = vcFlatContextionary 418 _ = vcHnswContextionary 419 420 testCases := []testCase{ 421 { 422 name: "same settings with nil vectors config", 423 initial: createClass(nil), 424 updated: createClass(nil), 425 expectedErrMsg: "", 426 }, 427 { 428 name: "same settings with nil+empty vectors config", 429 initial: createClass(nil), 430 updated: createClass(map[string]models.VectorConfig{}), 431 expectedErrMsg: "", 432 }, 433 { 434 name: "same settings with empty+nil vectors config", 435 initial: createClass(map[string]models.VectorConfig{}), 436 updated: createClass(nil), 437 expectedErrMsg: "", 438 }, 439 { 440 name: "same settings with empty vectors config", 441 initial: createClass(map[string]models.VectorConfig{}), 442 updated: createClass(map[string]models.VectorConfig{}), 443 expectedErrMsg: "", 444 }, 445 { 446 name: "same settings with single vector", 447 initial: createClass(map[string]models.VectorConfig{ 448 "vector": vcFlatContextionary, 449 }), 450 updated: createClass(map[string]models.VectorConfig{ 451 "vector": vcFlatContextionary, 452 }), 453 expectedErrMsg: "", 454 }, 455 { 456 name: "same settings with multi vectors", 457 initial: createClass(map[string]models.VectorConfig{ 458 "vector1": vcFlatContextionary, 459 "vector2": vcHnswContextionary, 460 }), 461 updated: createClass(map[string]models.VectorConfig{ 462 "vector2": vcHnswContextionary, 463 "vector1": vcFlatContextionary, 464 }), 465 expectedErrMsg: "", 466 }, 467 { 468 name: "no initial vectors", 469 initial: createClass(nil), 470 updated: createClass(map[string]models.VectorConfig{ 471 "vector2": vcHnswContextionary, 472 "vector1": vcFlatContextionary, 473 }), 474 expectedErrMsg: "additional configs for vectors", 475 }, 476 { 477 name: "no updated vectors", 478 initial: createClass(map[string]models.VectorConfig{ 479 "vector1": vcFlatContextionary, 480 "vector2": vcHnswContextionary, 481 }), 482 updated: createClass(nil), 483 expectedErrMsg: "missing configs for vectors", 484 }, 485 { 486 name: "more updated vectors", 487 initial: createClass(map[string]models.VectorConfig{ 488 "vector1": vcFlatContextionary, 489 }), 490 updated: createClass(map[string]models.VectorConfig{ 491 "vector2": vcHnswContextionary, 492 "vector1": vcFlatContextionary, 493 }), 494 expectedErrMsg: "additional config for vector \"vector2\"", 495 }, 496 { 497 name: "more initial vectors", 498 initial: createClass(map[string]models.VectorConfig{ 499 "vector1": vcFlatContextionary, 500 "vector2": vcHnswContextionary, 501 }), 502 updated: createClass(map[string]models.VectorConfig{ 503 "vector1": vcFlatContextionary, 504 }), 505 expectedErrMsg: "missing config for vector \"vector2\"", 506 }, 507 { 508 name: "index type changed", 509 initial: createClass(map[string]models.VectorConfig{ 510 "vector1": vcFlatContextionary, 511 "vector2": vcHnswContextionary, 512 }), 513 updated: createClass(map[string]models.VectorConfig{ 514 "vector1": vcFlatContextionary, 515 "vector2": vcFlatContextionary, 516 }), 517 expectedErrMsg: "vector index type of vector \"vector2\" is immutable: attempted change from \"hnsw\" to \"flat\"", 518 }, 519 { 520 name: "vectorizer changed", 521 initial: createClass(map[string]models.VectorConfig{ 522 "vector1": vcFlatContextionary, 523 }), 524 updated: createClass(map[string]models.VectorConfig{ 525 "vector1": { 526 VectorIndexType: "flat", 527 Vectorizer: map[string]interface{}{ 528 "not-contextionary": "some-settings", 529 }, 530 }, 531 }), 532 expectedErrMsg: "vectorizer of vector \"vector1\" is immutable: attempted change from \"text2vec-contextionary\" to \"not-contextionary\"", 533 }, 534 { 535 name: "vectorizer config not map", 536 initial: createClass(map[string]models.VectorConfig{ 537 "vector1": vcFlatContextionary, 538 }), 539 updated: createClass(map[string]models.VectorConfig{ 540 "vector1": { 541 VectorIndexType: "flat", 542 Vectorizer: "not-map", 543 }, 544 }), 545 expectedErrMsg: "invalid vectorizer config for vector \"vector1\"", 546 }, 547 { 548 name: "vectorizer config multiple keys", 549 initial: createClass(map[string]models.VectorConfig{ 550 "vector1": vcFlatContextionary, 551 }), 552 updated: createClass(map[string]models.VectorConfig{ 553 "vector1": { 554 VectorIndexType: "flat", 555 Vectorizer: map[string]interface{}{ 556 "text2vec-contextionary": "some-settings", 557 "additional-key": "value", 558 }, 559 }, 560 }), 561 expectedErrMsg: "invalid vectorizer config for vector \"vector1\"", 562 }, 563 } 564 565 t.Run("validation only", func(t *testing.T) { 566 for _, tc := range testCases { 567 t.Run(tc.name, func(t *testing.T) { 568 err := validateVectorConfigsParityAndImmutables(tc.initial, tc.updated) 569 570 if tc.expectedErrMsg == "" { 571 assert.NoError(t, err) 572 } else { 573 assert.ErrorContains(t, err, tc.expectedErrMsg) 574 } 575 }) 576 } 577 }) 578 579 t.Run("full update", func(t *testing.T) { 580 ctx := context.Background() 581 582 for _, tc := range testCases { 583 t.Run(tc.name, func(t *testing.T) { 584 sm := newSchemaManager() 585 m := &configMigrator{} 586 sm.migrator = m 587 588 err := sm.AddClass(ctx, nil, tc.initial) 589 require.NoError(t, err) 590 591 err = sm.UpdateClass(ctx, nil, tc.updated.Class, tc.updated) 592 593 if tc.expectedErrMsg == "" { 594 assert.NoError(t, err) 595 } else { 596 assert.ErrorContains(t, err, tc.expectedErrMsg) 597 } 598 599 // migrator's validation and update are called only for configured target vectors 600 if tc.expectedErrMsg == "" && len(tc.updated.VectorConfig) > 0 { 601 cfgs := map[string]schema.VectorIndexConfig{} 602 for vecName, vecCfg := range tc.updated.VectorConfig { 603 cfgs[vecName] = vecCfg.VectorIndexConfig.(schema.VectorIndexConfig) 604 } 605 606 assert.True(t, m.vectorConfigsUpdateCalled) 607 assert.Equal(t, cfgs, m.vectorConfigsValidateCalledWith) 608 assert.Equal(t, cfgs, m.vectorConfigsUpdateCalledWith) 609 } else { 610 assert.False(t, m.vectorConfigsUpdateCalled) 611 assert.Nil(t, m.vectorConfigsValidateCalledWith) 612 assert.Nil(t, m.vectorConfigsUpdateCalledWith) 613 } 614 }) 615 } 616 }) 617 } 618 619 type configMigrator struct { 620 NilMigrator 621 vectorConfigValidationError error 622 vectorConfigValidateCalledWith schema.VectorIndexConfig 623 vectorConfigUpdateCalled bool 624 vectorConfigUpdateCalledWith schema.VectorIndexConfig 625 vectorConfigsValidationError error 626 vectorConfigsValidateCalledWith map[string]schema.VectorIndexConfig 627 vectorConfigsUpdateCalled bool 628 vectorConfigsUpdateCalledWith map[string]schema.VectorIndexConfig 629 } 630 631 func (m *configMigrator) ValidateVectorIndexConfigUpdate(ctx context.Context, 632 old, updated schema.VectorIndexConfig, 633 ) error { 634 m.vectorConfigValidateCalledWith = updated 635 return m.vectorConfigValidationError 636 } 637 638 func (m *configMigrator) UpdateVectorIndexConfig(ctx context.Context, 639 className string, updated schema.VectorIndexConfig, 640 ) error { 641 m.vectorConfigUpdateCalledWith = updated 642 m.vectorConfigUpdateCalled = true 643 return nil 644 } 645 646 func (m *configMigrator) ValidateVectorIndexConfigsUpdate(ctx context.Context, 647 old, updated map[string]schema.VectorIndexConfig, 648 ) error { 649 m.vectorConfigsValidateCalledWith = updated 650 return m.vectorConfigsValidationError 651 } 652 653 func (m *configMigrator) UpdateVectorIndexConfigs(ctx context.Context, 654 className string, updated map[string]schema.VectorIndexConfig, 655 ) error { 656 m.vectorConfigsUpdateCalledWith = updated 657 m.vectorConfigsUpdateCalled = true 658 return nil 659 }