Spaces:
Running
Running
// _ _ | |
// __ _____ __ ___ ___ __ _| |_ ___ | |
// \ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \ | |
// \ V V / __/ (_| |\ V /| | (_| | || __/ | |
// \_/\_/ \___|\__,_| \_/ |_|\__,_|\__\___| | |
// | |
// Copyright © 2016 - 2024 Weaviate B.V. All rights reserved. | |
// | |
// CONTACT: [email protected] | |
// | |
package schema | |
import ( | |
"context" | |
"encoding/json" | |
"testing" | |
"github.com/pkg/errors" | |
"github.com/stretchr/testify/assert" | |
"github.com/stretchr/testify/require" | |
"github.com/weaviate/weaviate/entities/models" | |
"github.com/weaviate/weaviate/entities/schema" | |
"github.com/weaviate/weaviate/usecases/config" | |
) | |
// As of now, most class settings are immutable, but we need to allow some | |
// specific updates, such as the vector index config | |
func TestClassUpdates(t *testing.T) { | |
t.Run("a class which doesn't exist", func(t *testing.T) { | |
err := newSchemaManager().UpdateClass(context.Background(), | |
nil, "WrongClass", &models.Class{}) | |
require.NotNil(t, err) | |
assert.Equal(t, ErrNotFound, err) | |
}) | |
t.Run("various immutable and mutable fields", func(t *testing.T) { | |
type test struct { | |
name string | |
initial *models.Class | |
update *models.Class | |
expectedError error | |
} | |
tests := []test{ | |
{ | |
name: "attempting a name change", | |
initial: &models.Class{Class: "InitialName"}, | |
update: &models.Class{Class: "UpdatedName"}, | |
expectedError: errors.Errorf( | |
"class name is immutable: " + | |
"attempted change from \"InitialName\" to \"UpdatedName\""), | |
}, | |
{ | |
name: "attempting to modify the vectorizer", | |
initial: &models.Class{Class: "InitialName", Vectorizer: "model1"}, | |
update: &models.Class{Class: "InitialName", Vectorizer: "model2"}, | |
expectedError: errors.Errorf( | |
"vectorizer is immutable: " + | |
"attempted change from \"model1\" to \"model2\""), | |
}, | |
{ | |
name: "attempting to modify the vector index type", | |
initial: &models.Class{Class: "InitialName", VectorIndexType: "hnsw"}, | |
update: &models.Class{Class: "InitialName", VectorIndexType: "lsh"}, | |
expectedError: errors.Errorf( | |
"vector index type is immutable: " + | |
"attempted change from \"hnsw\" to \"lsh\""), | |
}, | |
{ | |
name: "attempting to add a property", | |
initial: &models.Class{Class: "InitialName"}, | |
update: &models.Class{ | |
Class: "InitialName", | |
Properties: []*models.Property{ | |
{ | |
Name: "newProp", | |
}, | |
}, | |
}, | |
expectedError: errors.Errorf( | |
"properties cannot be updated through updating the class. Use the add " + | |
"property feature (e.g. \"POST /v1/schema/{className}/properties\") " + | |
"to add additional properties"), | |
}, | |
{ | |
name: "leaving properties unchanged", | |
initial: &models.Class{ | |
Class: "InitialName", | |
Properties: []*models.Property{ | |
{ | |
Name: "aProp", | |
DataType: schema.DataTypeText.PropString(), | |
}, | |
}, | |
}, | |
update: &models.Class{ | |
Class: "InitialName", | |
Properties: []*models.Property{ | |
{ | |
Name: "aProp", | |
DataType: schema.DataTypeText.PropString(), | |
}, | |
}, | |
}, | |
expectedError: nil, | |
}, | |
{ | |
name: "attempting to rename a property", | |
initial: &models.Class{ | |
Class: "InitialName", | |
Properties: []*models.Property{ | |
{ | |
Name: "aProp", | |
DataType: schema.DataTypeText.PropString(), | |
}, | |
}, | |
}, | |
update: &models.Class{ | |
Class: "InitialName", | |
Properties: []*models.Property{ | |
{ | |
Name: "changedProp", | |
DataType: schema.DataTypeText.PropString(), | |
}, | |
}, | |
}, | |
expectedError: errors.Errorf( | |
"properties cannot be updated through updating the class. Use the add " + | |
"property feature (e.g. \"POST /v1/schema/{className}/properties\") " + | |
"to add additional properties"), | |
}, | |
{ | |
name: "attempting to update the inverted index cleanup interval", | |
initial: &models.Class{ | |
Class: "InitialName", | |
InvertedIndexConfig: &models.InvertedIndexConfig{ | |
CleanupIntervalSeconds: 17, | |
}, | |
}, | |
update: &models.Class{ | |
Class: "InitialName", | |
InvertedIndexConfig: &models.InvertedIndexConfig{ | |
CleanupIntervalSeconds: 18, | |
Bm25: &models.BM25Config{ | |
K1: config.DefaultBM25k1, | |
B: config.DefaultBM25b, | |
}, | |
}, | |
}, | |
}, | |
{ | |
name: "attempting to update the inverted index BM25 config", | |
initial: &models.Class{ | |
Class: "InitialName", | |
InvertedIndexConfig: &models.InvertedIndexConfig{ | |
CleanupIntervalSeconds: 18, | |
Bm25: &models.BM25Config{ | |
K1: 1.012, | |
B: 0.125, | |
}, | |
}, | |
}, | |
update: &models.Class{ | |
Class: "InitialName", | |
InvertedIndexConfig: &models.InvertedIndexConfig{ | |
CleanupIntervalSeconds: 18, | |
Bm25: &models.BM25Config{ | |
K1: 1.012, | |
B: 0.125, | |
}, | |
}, | |
}, | |
}, | |
{ | |
name: "attempting to update the inverted index Stopwords config", | |
initial: &models.Class{ | |
Class: "InitialName", | |
InvertedIndexConfig: &models.InvertedIndexConfig{ | |
CleanupIntervalSeconds: 18, | |
Stopwords: &models.StopwordConfig{ | |
Preset: "en", | |
}, | |
}, | |
}, | |
update: &models.Class{ | |
Class: "InitialName", | |
InvertedIndexConfig: &models.InvertedIndexConfig{ | |
CleanupIntervalSeconds: 18, | |
Stopwords: &models.StopwordConfig{ | |
Preset: "none", | |
Additions: []string{"banana", "passionfruit", "kiwi"}, | |
Removals: []string{"a", "the"}, | |
}, | |
}, | |
}, | |
}, | |
{ | |
name: "attempting to update module config", | |
initial: &models.Class{ | |
Class: "InitialName", | |
ModuleConfig: map[string]interface{}{ | |
"my-module1": map[string]interface{}{ | |
"my-setting": "some-value", | |
}, | |
}, | |
}, | |
update: &models.Class{ | |
Class: "InitialName", | |
ModuleConfig: map[string]interface{}{ | |
"my-module1": map[string]interface{}{ | |
"my-setting": "updated-value", | |
}, | |
}, | |
}, | |
expectedError: errors.Errorf("module config is immutable"), | |
}, | |
{ | |
name: "updating vector index config", | |
initial: &models.Class{ | |
Class: "InitialName", | |
VectorIndexConfig: map[string]interface{}{ | |
"some-setting": "old-value", | |
}, | |
}, | |
update: &models.Class{ | |
Class: "InitialName", | |
VectorIndexConfig: map[string]interface{}{ | |
"some-setting": "new-value", | |
}, | |
}, | |
expectedError: nil, | |
}, | |
} | |
for _, test := range tests { | |
t.Run(test.name, func(t *testing.T) { | |
sm := newSchemaManager() | |
assert.Nil(t, sm.AddClass(context.Background(), nil, test.initial)) | |
err := sm.UpdateClass(context.Background(), nil, test.initial.Class, test.update) | |
if test.expectedError == nil { | |
assert.Nil(t, err) | |
} else { | |
require.NotNil(t, err, "update must error") | |
assert.Equal(t, test.expectedError.Error(), err.Error()) | |
} | |
}) | |
} | |
}) | |
t.Run("update vector index config", func(t *testing.T) { | |
t.Run("with a validation error", func(t *testing.T) { | |
sm := newSchemaManager() | |
migrator := &configMigrator{ | |
vectorConfigValidationError: errors.Errorf("don't think so!"), | |
} | |
sm.migrator = migrator | |
t.Run("create an initial class", func(t *testing.T) { | |
err := sm.AddClass(context.Background(), nil, &models.Class{ | |
Class: "ClassWithVectorIndexConfig", | |
VectorIndexConfig: map[string]interface{}{ | |
"setting-1": "value-1", | |
}, | |
}) | |
assert.Nil(t, err) | |
}) | |
t.Run("attempt an update of the vector index config", func(t *testing.T) { | |
err := sm.UpdateClass(context.Background(), nil, | |
"ClassWithVectorIndexConfig", &models.Class{ | |
Class: "ClassWithVectorIndexConfig", | |
VectorIndexConfig: map[string]interface{}{ | |
"setting-1": "updated-value", | |
}, | |
}) | |
expectedErrMsg := "vector index config: don't think so!" | |
expectedValidateCalledWith := fakeVectorConfig{ | |
raw: map[string]interface{}{ | |
"distance": "cosine", | |
"setting-1": "updated-value", | |
}, | |
} | |
expectedUpdateCalled := false | |
require.NotNil(t, err) | |
assert.Equal(t, expectedErrMsg, err.Error()) | |
assert.Equal(t, expectedValidateCalledWith, migrator.vectorConfigValidateCalledWith) | |
assert.Equal(t, expectedUpdateCalled, migrator.vectorConfigUpdateCalled) | |
}) | |
}) | |
t.Run("with a valid update", func(t *testing.T) { | |
sm := newSchemaManager() | |
migrator := &configMigrator{} | |
sm.migrator = migrator | |
t.Run("create an initial class", func(t *testing.T) { | |
err := sm.AddClass(context.Background(), nil, &models.Class{ | |
Class: "ClassWithVectorIndexConfig", | |
VectorIndexConfig: map[string]interface{}{ | |
"setting-1": "value-1", | |
}, | |
}) | |
assert.Nil(t, err) | |
}) | |
t.Run("update the vector index config", func(t *testing.T) { | |
err := sm.UpdateClass(context.Background(), nil, | |
"ClassWithVectorIndexConfig", &models.Class{ | |
Class: "ClassWithVectorIndexConfig", | |
VectorIndexConfig: map[string]interface{}{ | |
"setting-1": "updated-value", | |
}, | |
}) | |
expectedValidateCalledWith := fakeVectorConfig{ | |
raw: map[string]interface{}{ | |
"distance": "cosine", | |
"setting-1": "updated-value", | |
}, | |
} | |
expectedUpdateCalledWith := fakeVectorConfig{ | |
raw: map[string]interface{}{ | |
"distance": "cosine", | |
"setting-1": "updated-value", | |
}, | |
} | |
expectedUpdateCalled := true | |
require.Nil(t, err) | |
assert.Equal(t, expectedValidateCalledWith, migrator.vectorConfigValidateCalledWith) | |
assert.Equal(t, expectedUpdateCalledWith, migrator.vectorConfigUpdateCalledWith) | |
assert.Equal(t, expectedUpdateCalled, migrator.vectorConfigUpdateCalled) | |
}) | |
t.Run("the update is reflected", func(t *testing.T) { | |
class := sm.getClassByName("ClassWithVectorIndexConfig") | |
require.NotNil(t, class) | |
expectedVectorIndexConfig := fakeVectorConfig{ | |
raw: map[string]interface{}{ | |
"distance": "cosine", | |
"setting-1": "updated-value", | |
}, | |
} | |
assert.Equal(t, expectedVectorIndexConfig, class.VectorIndexConfig) | |
}) | |
}) | |
}) | |
t.Run("update sharding config", func(t *testing.T) { | |
t.Run("with a validation error (immutable field)", func(t *testing.T) { | |
sm := newSchemaManager() | |
migrator := &NilMigrator{} | |
sm.migrator = migrator | |
t.Run("create an initial class", func(t *testing.T) { | |
err := sm.AddClass(context.Background(), nil, &models.Class{ | |
Class: "ClassWithShardingConfig", | |
}) | |
assert.Nil(t, err) | |
}) | |
t.Run("attempt an update of the vector index config", func(t *testing.T) { | |
err := sm.UpdateClass(context.Background(), nil, | |
"ClassWithShardingConfig", &models.Class{ | |
Class: "ClassWithShardingConfig", | |
ShardingConfig: map[string]interface{}{ | |
"desiredCount": json.Number("7"), | |
}, | |
}) | |
expectedErrMsg := "re-sharding not supported yet: shard count is immutable: attempted change from \"1\" to \"7\"" | |
require.NotNil(t, err) | |
assert.Contains(t, err.Error(), expectedErrMsg) | |
}) | |
}) | |
}) | |
} | |
type configMigrator struct { | |
NilMigrator | |
vectorConfigValidationError error | |
vectorConfigValidateCalledWith schema.VectorIndexConfig | |
vectorConfigUpdateCalled bool | |
vectorConfigUpdateCalledWith schema.VectorIndexConfig | |
} | |
func (m *configMigrator) ValidateVectorIndexConfigUpdate(ctx context.Context, | |
old, updated schema.VectorIndexConfig, | |
) error { | |
m.vectorConfigValidateCalledWith = updated | |
return m.vectorConfigValidationError | |
} | |
func (m *configMigrator) UpdateVectorIndexConfig(ctx context.Context, | |
className string, updated schema.VectorIndexConfig, | |
) error { | |
m.vectorConfigUpdateCalledWith = updated | |
m.vectorConfigUpdateCalled = true | |
return nil | |
} | |