Repository: weaviate/weaviate
Pull request #6045 (2024-10-17 13:18:23)

Improve UX of named vectors with old-style vectors
### What's being changed:

Fixes: https://github.com/weaviate/weaviate/issues/5947

Adds checks and returns error when:
- collection is configured with more than two named vectors but object contains vector
- collection is configured without named vectors but object contains named vectors

Fixes up the input:
- collection is configured with one named vector but object contains vector => single vector is used

### Review checklist

- [ ] Documentation has been updated, if necessary. Link to changed documentation:
- [ ] Chaos pipeline run or not necessary. Link to pipeline:
- [ ] All new code is covered by tests where it is reasonable.
- [ ] Performance tests have been run or not necessary.


Patch:
diff --git a/adapters/repos/db/index_queue.go b/adapters/repos/db/index_queue.go
index a19dd5db9b..e3436f6e3a 100644
--- a/adapters/repos/db/index_queue.go
+++ b/adapters/repos/db/index_queue.go
@@ -134,7 +134,7 @@ type upgradableIndexer interface {
 }
 
 type shardStatusUpdater interface {
-	compareAndSwapStatusIndexingAndReady(old, new string) (storagestate.Status, error)
+	compareAndSwapStatus(old, new string) (storagestate.Status, error)
 	Name() string
 }
 
@@ -415,12 +415,12 @@ func (q *IndexQueue) indexer() {
 			}
 
 			if q.Size() == 0 {
-				_, _ = q.Shard.compareAndSwapStatusIndexingAndReady(storagestate.StatusIndexing.String(), storagestate.StatusReady.String())
+				_, _ = q.Shard.compareAndSwapStatus(storagestate.StatusIndexing.String(), storagestate.StatusReady.String())
 				q.indexLock.RUnlock()
 				continue
 			}
 
-			status, err := q.Shard.compareAndSwapStatusIndexingAndReady(storagestate.StatusReady.String(), storagestate.StatusIndexing.String())
+			status, err := q.Shard.compareAndSwapStatus(storagestate.StatusReady.String(), storagestate.StatusIndexing.String())
 			if status != storagestate.StatusIndexing || err != nil {
 				q.Logger.WithField("status", status).WithError(err).Warn("failed to set shard status to 'indexing', trying again in " + q.IndexInterval.String())
 				q.indexLock.RUnlock()
diff --git a/adapters/repos/db/replication.go b/adapters/repos/db/replication.go
index 5f2bbb8bee..eefb3f5a12 100644
--- a/adapters/repos/db/replication.go
+++ b/adapters/repos/db/replication.go
@@ -161,7 +161,7 @@ func (i *Index) writableShard(name string) (ShardLike, func(), *replica.SimpleRe
 			{Code: replica.StatusShardNotFound, Msg: name},
 		}}
 	}
-	if localShard.isReadOnly() != nil {
+	if localShard.isReadOnly() {
 		release()
 
 		return nil, func() {}, &replica.SimpleResponse{Errors: []replica.Error{{
diff --git a/adapters/repos/db/resource_use.go b/adapters/repos/db/resource_use.go
index 3fbd7a6821..767c27286e 100644
--- a/adapters/repos/db/resource_use.go
+++ b/adapters/repos/db/resource_use.go
@@ -18,6 +18,7 @@ import (
 	enterrors "github.com/weaviate/weaviate/entities/errors"
 
 	"github.com/weaviate/weaviate/entities/interval"
+	"github.com/weaviate/weaviate/entities/storagestate"
 	"github.com/weaviate/weaviate/usecases/memwatch"
 )
 
@@ -129,7 +130,7 @@ func (db *DB) diskUseReadonly(du diskUse) {
 	diskROPercent := db.config.ResourceUsage.DiskUse.ReadOnlyPercentage
 	if diskROPercent > 0 {
 		if pu := du.percentUsed(); pu > float64(diskROPercent) {
-			db.setShardsReadOnly(fmt.Sprintf("disk usage too high. Set to read-only at %.2f%%, threshold set to %.2f%%\"", pu, float64(diskROPercent)))
+			db.setShardsReadOnly()
 			db.logger.WithField("action", "set_shard_read_only").
 				WithField("path", db.config.RootPath).
 				Warnf("Set READONLY, disk usage currently at %.2f%%, threshold set to %.2f%%",
@@ -142,7 +143,7 @@ func (db *DB) memUseReadonly(mon *memwatch.Monitor) {
 	memROPercent := db.config.ResourceUsage.MemUse.ReadOnlyPercentage
 	if memROPercent > 0 {
 		if pu := mon.Ratio() * 100; pu > float64(memROPercent) {
-			db.setShardsReadOnly(fmt.Sprintf("memory usage too high. Set to read-only at %.2f%%, threshold set to %.2f%%\"", pu, float64(memROPercent)))
+			db.setShardsReadOnly()
 			db.logger.WithField("action", "set_shard_read_only").
 				WithField("path", db.config.RootPath).
 				Warnf("Set READONLY, memory usage currently at %.2f%%, threshold set to %.2f%%",
@@ -151,11 +152,11 @@ func (db *DB) memUseReadonly(mon *memwatch.Monitor) {
 	}
 }
 
-func (db *DB) setShardsReadOnly(reason string) {
+func (db *DB) setShardsReadOnly() {
 	db.indexLock.Lock()
 	for _, index := range db.indices {
 		index.ForEachShard(func(name string, shard ShardLike) error {
-			err := shard.SetStatusReadonly(reason)
+			err := shard.UpdateStatus(storagestate.StatusReadOnly.String())
 			if err != nil {
 				db.logger.WithField("action", "set_shard_read_only").
 					WithField("path", db.config.RootPath).
diff --git a/adapters/repos/db/shard.go b/adapters/repos/db/shard.go
index 0450da3f4d..45ad6cf24d 100644
--- a/adapters/repos/db/shard.go
+++ b/adapters/repos/db/shard.go
@@ -61,7 +61,6 @@ type ShardLike interface {
 	GetStatus() storagestate.Status // Return the shard status
 	GetStatusNoLoad() storagestate.Status
 	UpdateStatus(status string) error                                                   // Set shard status
-	SetStatusReadonly(reason string) error                                              // Set shard status to readonly with reason
 	FindUUIDs(ctx context.Context, filters *filters.LocalFilter) ([]strfmt.UUID, error) // Search and return document ids
 
 	Counter() *indexcounter.Counter
@@ -109,7 +108,7 @@ type ShardLike interface {
 	// TODO tests only
 	Versioner() *shardVersioner // Get the shard versioner
 
-	isReadOnly() error
+	isReadOnly() bool
 
 	preparePutObject(context.Context, string, *storobj.Object) replica.SimpleResponse
 	preparePutObjects(context.Context, string, []*storobj.Object) replica.SimpleResponse
@@ -178,7 +177,7 @@ type Shard struct {
 	propLenTracker    *inverted.JsonShardMetaData
 	versioner         *shardVersioner
 
-	status              ShardStatus
+	status              storagestate.Status
 	statusLock          sync.Mutex
 	propertyIndicesLock sync.RWMutex
 
@@ -267,11 +266,11 @@ func (s *Shard) segmentCleanupConfig() lsmkv.BucketOption {
 }
 
 func (s *Shard) UpdateVectorIndexConfig(ctx context.Context, updated schemaConfig.VectorIndexConfig) error {
-	if err := s.isReadOnly(); err != nil {
-		return err
+	if s.isReadOnly() {
+		return storagestate.ErrStatusReadOnly
 	}
 
-	err := s.SetStatusReadonly("UpdateVectorIndexConfig")
+	err := s.UpdateStatus(storagestate.StatusReadOnly.String())
 	if err != nil {
 		return fmt.Errorf("attempt to mark read-only: %w", err)
 	}
@@ -282,10 +281,10 @@ func (s *Shard) UpdateVectorIndexConfig(ctx context.Context, updated schemaConfi
 }
 
 func (s *Shard) UpdateVectorIndexConfigs(ctx context.Context, updated map[string]schemaConfig.VectorIndexConfig) error {
-	if err := s.isReadOnly(); err != nil {
-		return err
+	if s.isReadOnly() {
+		return storagestate.ErrStatusReadOnly
 	}
-	if err := s.SetStatusReadonly("UpdateVectorIndexConfig"); err != nil {
+	if err := s.UpdateStatus(storagestate.StatusReadOnly.String()); err != nil {
 		return fmt.Errorf("attempt to mark read-only: %w", err)
 	}
 
diff --git a/adapters/repos/db/shard_geo_props.go b/adapters/repos/db/shard_geo_props.go
index 2d1878132f..4d59a674bf 100644
--- a/adapters/repos/db/shard_geo_props.go
+++ b/adapters/repos/db/shard_geo_props.go
@@ -20,6 +20,7 @@ import (
 	"github.com/weaviate/weaviate/adapters/repos/db/vector/geo"
 	"github.com/weaviate/weaviate/entities/models"
 	"github.com/weaviate/weaviate/entities/schema"
+	"github.com/weaviate/weaviate/entities/storagestate"
 	"github.com/weaviate/weaviate/entities/storobj"
 )
 
@@ -93,10 +94,9 @@ func geoPropID(propName string) string {
 func (s *Shard) updatePropertySpecificIndices(object *storobj.Object,
 	status objectInsertStatus,
 ) error {
-	if err := s.isReadOnly(); err != nil {
-		return err
+	if s.isReadOnly() {
+		return storagestate.ErrStatusReadOnly
 	}
-
 	s.propertyIndicesLock.RLock()
 	defer s.propertyIndicesLock.RUnlock()
 
@@ -125,8 +125,8 @@ func (s *Shard) updatePropertySpecificIndex(propName string,
 func (s *Shard) updateGeoIndex(propName string, index propertyspecific.Index,
 	obj *storobj.Object, status objectInsertStatus,
 ) error {
-	if err := s.isReadOnly(); err != nil {
-		return err
+	if s.isReadOnly() {
+		return storagestate.ErrStatusReadOnly
 	}
 
 	// geo props were not changed
@@ -146,8 +146,8 @@ func (s *Shard) updateGeoIndex(propName string, index propertyspecific.Index,
 func (s *Shard) addToGeoIndex(propName string, index propertyspecific.Index,
 	obj *storobj.Object, status objectInsertStatus,
 ) error {
-	if err := s.isReadOnly(); err != nil {
-		return err
+	if s.isReadOnly() {
+		return storagestate.ErrStatusReadOnly
 	}
 
 	if obj.Properties() == nil {
@@ -177,8 +177,8 @@ func (s *Shard) addToGeoIndex(propName string, index propertyspecific.Index,
 func (s *Shard) deleteFromGeoIndex(index propertyspecific.Index,
 	docID uint64,
 ) error {
-	if err := s.isReadOnly(); err != nil {
-		return err
+	if s.isReadOnly() {
+		return storagestate.ErrStatusReadOnly
 	}
 
 	if err := index.GeoIndex.Delete(docID); err != nil {
diff --git a/adapters/repos/db/shard_init.go b/adapters/repos/db/shard_init.go
index 0dc971c236..08d0037455 100644
--- a/adapters/repos/db/shard_init.go
+++ b/adapters/repos/db/shard_init.go
@@ -26,6 +26,7 @@ import (
 	enterrors "github.com/weaviate/weaviate/entities/errors"
 	"github.com/weaviate/weaviate/entities/models"
 	entsentry "github.com/weaviate/weaviate/entities/sentry"
+	"github.com/weaviate/weaviate/entities/storagestate"
 	"github.com/weaviate/weaviate/usecases/monitoring"
 )
 
@@ -56,7 +57,7 @@ func NewShard(ctx context.Context, promMetrics *monitoring.PrometheusMetrics,
 		shut:         false,
 		shutdownLock: new(sync.RWMutex),
 
-		status: NewShardStatus(),
+		status: storagestate.StatusLoading,
 	}
 
 	defer func() {
diff --git a/adapters/repos/db/shard_init_properties.go b/adapters/repos/db/shard_init_properties.go
index bb69353e29..e79104265e 100644
--- a/adapters/repos/db/shard_init_properties.go
+++ b/adapters/repos/db/shard_init_properties.go
@@ -15,14 +15,16 @@ import (
 	"context"
 	"fmt"
 
+	enterrors "github.com/weaviate/weaviate/entities/errors"
+	"github.com/weaviate/weaviate/entities/filters"
+	"github.com/weaviate/weaviate/entities/schema"
+	"github.com/weaviate/weaviate/entities/storagestate"
+
 	"github.com/weaviate/weaviate/adapters/repos/db/helpers"
 	"github.com/weaviate/weaviate/adapters/repos/db/inverted"
 	"github.com/weaviate/weaviate/adapters/repos/db/lsmkv"
 	"github.com/weaviate/weaviate/adapters/repos/db/propertyspecific"
-	enterrors "github.com/weaviate/weaviate/entities/errors"
-	"github.com/weaviate/weaviate/entities/filters"
 	"github.com/weaviate/weaviate/entities/models"
-	"github.com/weaviate/weaviate/entities/schema"
 )
 
 func (s *Shard) initProperties(eg *enterrors.ErrorGroupWrapper, class *models.Class) {
@@ -86,8 +88,8 @@ func (s *Shard) initPropertyBuckets(ctx context.Context, eg *enterrors.ErrorGrou
 }
 
 func (s *Shard) createPropertyValueIndex(ctx context.Context, prop *models.Property) error {
-	if err := s.isReadOnly(); err != nil {
-		return err
+	if s.isReadOnly() {
+		return storagestate.ErrStatusReadOnly
 	}
 
 	bucketOpts := []lsmkv.BucketOption{
@@ -140,8 +142,8 @@ func (s *Shard) createPropertyValueIndex(ctx context.Context, prop *models.Prope
 }
 
 func (s *Shard) createPropertyLengthIndex(ctx context.Context, prop *models.Property) error {
-	if err := s.isReadOnly(); err != nil {
-		return err
+	if s.isReadOnly() {
+		return storagestate.ErrStatusReadOnly
 	}
 
 	// some datatypes are not added to the inverted index, so we can skip them here
@@ -163,8 +165,8 @@ func (s *Shard) createPropertyLengthIndex(ctx context.Context, prop *models.Prop
 }
 
 func (s *Shard) createPropertyNullIndex(ctx context.Context, prop *models.Property) error {
-	if err := s.isReadOnly(); err != nil {
-		return err
+	if s.isReadOnly() {
+		return storagestate.ErrStatusReadOnly
 	}
 
 	return s.store.CreateOrLoadBucket(ctx,
@@ -178,8 +180,8 @@ func (s *Shard) createPropertyNullIndex(ctx context.Context, prop *models.Proper
 }
 
 func (s *Shard) addIDProperty(ctx context.Context) error {
-	if err := s.isReadOnly(); err != nil {
-		return err
+	if s.isReadOnly() {
+		return storagestate.ErrStatusReadOnly
 	}
 
 	err := s.store.CreateOrLoadBucket(ctx,
@@ -198,8 +200,8 @@ func (s *Shard) addIDProperty(ctx context.Context) error {
 }
 
 func (s *Shard) addDimensionsProperty(ctx context.Context) error {
-	if err := s.isReadOnly(); err != nil {
-		return err
+	if s.isReadOnly() {
+		return storagestate.ErrStatusReadOnly
 	}
 
 	// Note: this data would fit the "Set" type better, but since the "Map" type
@@ -220,8 +222,8 @@ func (s *Shard) addDimensionsProperty(ctx context.Context) error {
 }
 
 func (s *Shard) addTimestampProperties(ctx context.Context) error {
-	if err := s.isReadOnly(); err != nil {
-		return err
+	if s.isReadOnly() {
+		return storagestate.ErrStatusReadOnly
 	}
 
 	if err := s.addCreationTimeUnixProperty(ctx); err != nil {
diff --git a/adapters/repos/db/shard_lazyloader.go b/adapters/repos/db/shard_lazyloader.go
index 75e1a13b39..1e09016821 100644
--- a/adapters/repos/db/shard_lazyloader.go
+++ b/adapters/repos/db/shard_lazyloader.go
@@ -162,11 +162,6 @@ func (l *LazyLoadShard) UpdateStatus(status string) error {
 	return l.shard.UpdateStatus(status)
 }
 
-func (l *LazyLoadShard) SetStatusReadonly(reason string) error {
-	l.mustLoad()
-	return l.shard.SetStatusReadonly(reason)
-}
-
 func (l *LazyLoadShard) FindUUIDs(ctx context.Context, filters *filters.LocalFilter) ([]strfmt.UUID, error) {
 	if err := l.Load(ctx); err != nil {
 		return []strfmt.UUID{}, err
@@ -477,7 +472,7 @@ func (l *LazyLoadShard) Versioner() *shardVersioner {
 	return l.shard.Versioner()
 }
 
-func (l *LazyLoadShard) isReadOnly() error {
+func (l *LazyLoadShard) isReadOnly() bool {
 	l.mustLoad()
 	return l.shard.isReadOnly()
 }
diff --git a/adapters/repos/db/shard_status.go b/adapters/repos/db/shard_status.go
index 747c7908e5..feeb2816b2 100644
--- a/adapters/repos/db/shard_status.go
+++ b/adapters/repos/db/shard_status.go
@@ -12,39 +12,24 @@
 package db
 
 import (
-	"fmt"
 	"strings"
 
 	"github.com/pkg/errors"
 	"github.com/weaviate/weaviate/entities/storagestate"
 )
 
-type ShardStatus struct {
-	Status storagestate.Status
-	Reason string
-}
-
-func NewShardStatus() ShardStatus {
-	return ShardStatus{Status: storagestate.StatusLoading}
-}
-
-func (s *ShardStatus) Init() {
-	s.Status = storagestate.StatusReady
-	s.Reason = ""
-}
-
 func (s *Shard) initStatus() {
 	s.statusLock.Lock()
 	defer s.statusLock.Unlock()
 
-	s.status.Init()
+	s.status = storagestate.StatusReady
 }
 
 func (s *Shard) GetStatus() storagestate.Status {
 	s.statusLock.Lock()
 	defer s.statusLock.Unlock()
 
-	return s.status.Status
+	return s.status
 }
 
 // Same implem for for a regular shard, this only differ in lazy loaded shards
@@ -52,59 +37,37 @@ func (s *Shard) GetStatusNoLoad() storagestate.Status {
 	return s.GetStatus()
 }
 
-// isReadOnly returns an error if shard is readOnly and nil otherwise
-func (s *Shard) isReadOnly() error {
-	if s.status.Status == storagestate.StatusReadOnly {
-		return storagestate.ErrStatusReadOnlyWithReason(s.status.Reason)
-	}
-	return nil
+func (s *Shard) isReadOnly() bool {
+	return s.GetStatus() == storagestate.StatusReadOnly
 }
 
-func (s *Shard) compareAndSwapStatusIndexingAndReady(old, new string) (storagestate.Status, error) {
+func (s *Shard) compareAndSwapStatus(old, new string) (storagestate.Status, error) {
 	s.statusLock.Lock()
 	defer s.statusLock.Unlock()
 
-	if (old != storagestate.StatusIndexing.String() && old != storagestate.StatusReady.String()) ||
-		(new != storagestate.StatusIndexing.String() && new != storagestate.StatusReady.String()) {
-		return s.status.Status, fmt.Errorf("can only swap between indexing and ready, got %v and %v", old, new)
+	if s.status.String() != old {
+		return s.status, nil
 	}
 
-	if s.status.Status.String() != old {
-		return s.status.Status, nil
-	}
-
-	return s.status.Status, s.updateStatusUnlocked(new, "")
-}
-
-func (s *Shard) SetStatusReadonly(reason string) error {
-	s.statusLock.Lock()
-	defer s.statusLock.Unlock()
-
-	return s.updateStatusUnlocked(storagestate.StatusReadOnly.String(), reason)
+	return s.status, s.updateStatusUnlocked(new)
 }
 
 func (s *Shard) UpdateStatus(in string) error {
 	s.statusLock.Lock()
 	defer s.statusLock.Unlock()
 
-	reason := ""
-	if in == storagestate.StatusReadOnly.String() {
-		reason = "manually set by user"
-	}
-
-	return s.updateStatusUnlocked(in, reason)
+	return s.updateStatusUnlocked(in)
 }
 
 // updateStatusUnlocked updates the status without locking the statusLock.
 // Warning: Use UpdateStatus instead.
-func (s *Shard) updateStatusUnlocked(in, reason string) error {
+func (s *Shard) updateStatusUnlocked(in string) error {
 	targetStatus, err := storagestate.ValidateStatus(strings.ToUpper(in))
 	if err != nil {
 		return errors.Wrap(err, in)
 	}
 
-	s.status.Status = targetStatus
-	s.status.Reason = reason
+	s.status = targetStatus
 
 	err = s.updateStoreStatus(targetStatus)
 	if err != nil {
@@ -115,8 +78,7 @@ func (s *Shard) updateStatusUnlocked(in, reason string) error {
 		WithField("action", "update shard status").
 		WithField("class", s.index.Config.ClassName).
 		WithField("shard", s.name).
-		WithField("status", in).
-		WithField("readOnlyReason", reason)
+		WithField("status", in)
 
 	return nil
 }
diff --git a/adapters/repos/db/shard_write_batch_delete.go b/adapters/repos/db/shard_write_batch_delete.go
index a5a9a5f1c0..9f8e779e61 100644
--- a/adapters/repos/db/shard_write_batch_delete.go
+++ b/adapters/repos/db/shard_write_batch_delete.go
@@ -24,15 +24,16 @@ import (
 	"github.com/weaviate/weaviate/adapters/repos/db/inverted"
 	"github.com/weaviate/weaviate/entities/additional"
 	"github.com/weaviate/weaviate/entities/filters"
+	"github.com/weaviate/weaviate/entities/storagestate"
 	"github.com/weaviate/weaviate/usecases/objects"
 )
 
 // return value map[int]error gives the error for the index as it received it
 func (s *Shard) DeleteObjectBatch(ctx context.Context, uuids []strfmt.UUID, dryRun bool) objects.BatchSimpleObjects {
 	s.activityTracker.Add(1)
-	if err := s.isReadOnly(); err != nil {
+	if s.isReadOnly() {
 		return objects.BatchSimpleObjects{
-			objects.BatchSimpleObject{Err: err},
+			objects.BatchSimpleObject{Err: storagestate.ErrStatusReadOnly},
 		}
 	}
 	return newDeleteObjectsBatcher(s).Delete(ctx, uuids, dryRun)
diff --git a/adapters/repos/db/shard_write_batch_objects.go b/adapters/repos/db/shard_write_batch_objects.go
index 783c810232..4639dd5fd7 100644
--- a/adapters/repos/db/shard_write_batch_objects.go
+++ b/adapters/repos/db/shard_write_batch_objects.go
@@ -26,6 +26,7 @@ import (
 	"github.com/go-openapi/strfmt"
 	"github.com/google/uuid"
 	"github.com/pkg/errors"
+	"github.com/weaviate/weaviate/entities/storagestate"
 	"github.com/weaviate/weaviate/entities/storobj"
 )
 
@@ -34,8 +35,8 @@ func (s *Shard) PutObjectBatch(ctx context.Context,
 	objects []*storobj.Object,
 ) []error {
 	s.activityTracker.Add(1)
-	if err := s.isReadOnly(); err != nil {
-		return []error{err}
+	if s.isReadOnly() {
+		return []error{storagestate.ErrStatusReadOnly}
 	}
 
 	return s.putBatch(ctx, objects)
diff --git a/adapters/repos/db/shard_write_batch_references.go b/adapters/repos/db/shard_write_batch_references.go
index c626f7a8bf..5438d3f928 100644
--- a/adapters/repos/db/shard_write_batch_references.go
+++ b/adapters/repos/db/shard_write_batch_references.go
@@ -29,8 +29,8 @@ import (
 // return value map[int]error gives the error for the index as it received it
 func (s *Shard) AddReferencesBatch(ctx context.Context, refs objects.BatchReferences) []error {
 	s.activityTracker.Add(1)
-	if err := s.isReadOnly(); err != nil {
-		return []error{err}
+	if s.isReadOnly() {
+		return []error{errors.Errorf("shard is read-only")}
 	}
 
 	return newReferencesBatcher(s).References(ctx, refs)
diff --git a/adapters/repos/db/shard_write_delete.go b/adapters/repos/db/shard_write_delete.go
index dc9f8444e6..89d62f9f6f 100644
--- a/adapters/repos/db/shard_write_delete.go
+++ b/adapters/repos/db/shard_write_delete.go
@@ -19,12 +19,13 @@ import (
 	"github.com/google/uuid"
 	"github.com/weaviate/weaviate/adapters/repos/db/helpers"
 	"github.com/weaviate/weaviate/adapters/repos/db/lsmkv"
+	"github.com/weaviate/weaviate/entities/storagestate"
 	"github.com/weaviate/weaviate/entities/storobj"
 )
 
 func (s *Shard) DeleteObject(ctx context.Context, id strfmt.UUID) error {
-	if err := s.isReadOnly(); err != nil {
-		return err
+	if s.isReadOnly() {
+		return storagestate.ErrStatusReadOnly
 	}
 
 	idBytes, err := uuid.MustParse(id.String()).MarshalBinary()
diff --git a/adapters/repos/db/shard_write_merge.go b/adapters/repos/db/shard_write_merge.go
index cdea18fc40..f583605857 100644
--- a/adapters/repos/db/shard_write_merge.go
+++ b/adapters/repos/db/shard_write_merge.go
@@ -19,6 +19,7 @@ import (
 	"github.com/pkg/errors"
 	"github.com/weaviate/weaviate/adapters/repos/db/helpers"
 	"github.com/weaviate/weaviate/entities/models"
+	"github.com/weaviate/weaviate/entities/storagestate"
 	"github.com/weaviate/weaviate/entities/storobj"
 	"github.com/weaviate/weaviate/usecases/objects"
 )
@@ -27,8 +28,8 @@ var errObjectNotFound = errors.New("object not found")
 
 func (s *Shard) MergeObject(ctx context.Context, merge objects.MergeDocument) error {
 	s.activityTracker.Add(1)
-	if err := s.isReadOnly(); err != nil {
-		return err
+	if s.isReadOnly() {
+		return storagestate.ErrStatusReadOnly
 	}
 
 	if s.hasTargetVectors() {
diff --git a/adapters/repos/db/shard_write_put.go b/adapters/repos/db/shard_write_put.go
index b02fc691ff..401fccf03a 100644
--- a/adapters/repos/db/shard_write_put.go
+++ b/adapters/repos/db/shard_write_put.go
@@ -26,19 +26,20 @@ import (
 	"github.com/weaviate/weaviate/adapters/repos/db/lsmkv"
 	"github.com/weaviate/weaviate/adapters/repos/db/vector/common"
 	"github.com/weaviate/weaviate/entities/models"
+	"github.com/weaviate/weaviate/entities/storagestate"
 	"github.com/weaviate/weaviate/entities/storobj"
 )
 
 func (s *Shard) PutObject(ctx context.Context, object *storobj.Object) error {
 	s.activityTracker.Add(1)
-	if err := s.isReadOnly(); err != nil {
-		return err
+	if s.isReadOnly() {
+		return storagestate.ErrStatusReadOnly
 	}
-	uid, err := uuid.MustParse(object.ID().String()).MarshalBinary()
+	uuid, err := uuid.MustParse(object.ID().String()).MarshalBinary()
 	if err != nil {
 		return err
 	}
-	return s.putOne(ctx, uid, object)
+	return s.putOne(ctx, uuid, object)
 }
 
 func (s *Shard) putOne(ctx context.Context, uuid []byte, object *storobj.Object) error {
diff --git a/entities/storagestate/status.go b/entities/storagestate/status.go
index 57cedb4cca..d97e6c7c93 100644
--- a/entities/storagestate/status.go
+++ b/entities/storagestate/status.go
@@ -11,10 +11,7 @@
 
 package storagestate
 
-import (
-	"errors"
-	"fmt"
-)
+import "errors"
 
 const (
 	StatusReadOnly Status = "READONLY"
@@ -23,10 +20,6 @@ const (
 	StatusReady    Status = "READY"
 )
 
-var ErrStatusReadOnlyWithReason = func(reason string) error {
-	return fmt.Errorf("store is read-only due to: %v", reason)
-}
-
 var (
 	ErrStatusReadOnly = errors.New("store is read-only")
 	ErrInvalidStatus  = errors.New("invalid storage status")
diff --git a/usecases/objects/validation/model_validation.go b/usecases/objects/validation/model_validation.go
index ba63810320..396389e7a7 100644
--- a/usecases/objects/validation/model_validation.go
+++ b/usecases/objects/validation/model_validation.go
@@ -85,6 +85,10 @@ func (v *Validator) Object(ctx context.Context, class *models.Class,
 		return fmt.Errorf(ErrorMissingClass)
 	}
 
+	if err := v.vector(ctx, class, incoming); err != nil {
+		return err
+	}
+
 	return v.properties(ctx, class, incoming, existing)
 }
 
diff --git a/usecases/objects/validation/vector_validation.go b/usecases/objects/validation/vector_validation.go
new file mode 100644
index 0000000000..e940b8e003
--- /dev/null
+++ b/usecases/objects/validation/vector_validation.go
@@ -0,0 +1,43 @@
+//                           _       _
+// __      _____  __ ___   ___  __ _| |_ ___
+// \ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \
+//  \ V  V /  __/ (_| |\ V /| | (_| | ||  __/
+//   \_/\_/ \___|\__,_| \_/ |_|\__,_|\__\___|
+//
+//  Copyright © 2016 - 2024 Weaviate B.V. All rights reserved.
+//
+//  CONTACT: hello@weaviate.io
+//
+
+package validation
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/weaviate/weaviate/entities/models"
+)
+
+func (v *Validator) vector(ctx context.Context, class *models.Class,
+	incomingObject *models.Object,
+) error {
+	if len(class.VectorConfig) > 1 && len(incomingObject.Vector) > 0 {
+		return fmt.Errorf("collection %v is configured with multiple named vectors %v, but received a single vector", class.Class, class.VectorConfig)
+	}
+
+	if class.VectorIndexConfig != nil && len(incomingObject.Vectors) > 0 {
+		return fmt.Errorf("collection %v is configured without multiple named vectors, but received named vectors: %v", class.Class, incomingObject.Vectors)
+	}
+
+	// if there is only one named vector we can assume that the single vector
+	if len(class.VectorConfig) == 1 && len(incomingObject.Vector) > 0 {
+		namedVectorName := ""
+		for key := range class.VectorConfig {
+			namedVectorName = key
+		}
+		incomingObject.Vectors = map[string]models.Vector{namedVectorName: models.Vector(incomingObject.Vector)}
+		incomingObject.Vector = nil
+	}
+
+	return nil
+}


Test patch:
diff --git a/adapters/repos/db/index_queue_test.go b/adapters/repos/db/index_queue_test.go
index bae5c3a5ce..d5c478bfd0 100644
--- a/adapters/repos/db/index_queue_test.go
+++ b/adapters/repos/db/index_queue_test.go
@@ -990,7 +990,7 @@ type mockShard struct {
 	compareAndSwapStatusFn func(old, new string) (storagestate.Status, error)
 }
 
-func (m *mockShard) compareAndSwapStatusIndexingAndReady(old, new string) (storagestate.Status, error) {
+func (m *mockShard) compareAndSwapStatus(old, new string) (storagestate.Status, error) {
 	if m.compareAndSwapStatusFn == nil {
 		return storagestate.Status(new), nil
 	}
diff --git a/adapters/repos/db/shard_test.go b/adapters/repos/db/shard_test.go
index 7ed5312f68..095c4459d3 100644
--- a/adapters/repos/db/shard_test.go
+++ b/adapters/repos/db/shard_test.go
@@ -68,12 +68,11 @@ func TestShard_UpdateStatus(t *testing.T) {
 	})
 
 	t.Run("mark shard readonly and fail to insert", func(t *testing.T) {
-		err := shd.SetStatusReadonly("testing")
+		err := shd.UpdateStatus(storagestate.StatusReadOnly.String())
 		require.Nil(t, err)
 
 		err = shd.PutObject(ctx, testObject(className))
-		require.Contains(t, err.Error(), storagestate.ErrStatusReadOnly.Error())
-		require.Contains(t, err.Error(), "testing")
+		require.EqualError(t, err, storagestate.ErrStatusReadOnly.Error())
 	})
 
 	t.Run("mark shard ready and insert successfully", func(t *testing.T) {
diff --git a/test/acceptance_with_python/test_named_vectors.py b/test/acceptance_with_python/test_named_vectors.py
index 45ac67973a..b50a01fd94 100644
--- a/test/acceptance_with_python/test_named_vectors.py
+++ b/test/acceptance_with_python/test_named_vectors.py
@@ -1,4 +1,6 @@
+import pytest
 import weaviate.classes as wvc
+from weaviate.exceptions import UnexpectedStatusCodeError, WeaviateInsertManyAllFailedError
 
 from .conftest import CollectionFactory
 
@@ -29,3 +31,52 @@ def test_create_named_vectors_with_and_without_vectorizer(
     obj = collection.query.fetch_object_by_id(uuid, include_vector=True)
     assert obj.vector["AllExplicit"] is not None
     assert obj.vector["bringYourOwn"] is not None
+
+
+def test_single_named_vectors_without_names(collection_factory: CollectionFactory) -> None:
+    collection = collection_factory(
+        properties=[wvc.config.Property(name="title", data_type=wvc.config.DataType.TEXT)],
+        vectorizer_config=[wvc.config.Configure.NamedVectors.none("title")],
+    )
+
+    # insert object with single non-named vector.
+    uuid1 = collection.data.insert(properties={"title": "Hello"}, vector=[1, 2, 3])
+    obj = collection.query.fetch_object_by_id(uuid1, include_vector=True)
+    assert "title" in obj.vector
+
+    ret = collection.data.insert_many(
+        [wvc.data.DataObject(properties={"title": "Hello"}, vector=[1, 2, 3])]
+    )
+    obj_batch = collection.query.fetch_object_by_id(ret.uuids[0], include_vector=True)
+    assert "title" in obj_batch.vector
+
+
+def test_named_vectors_without_names(collection_factory: CollectionFactory) -> None:
+    collection = collection_factory(
+        properties=[wvc.config.Property(name="title", data_type=wvc.config.DataType.TEXT)],
+        vectorizer_config=[
+            wvc.config.Configure.NamedVectors.none("first"),
+            wvc.config.Configure.NamedVectors.none("second"),
+        ],
+    )
+    with pytest.raises(UnexpectedStatusCodeError):
+        collection.data.insert(properties={"title": "Hello"}, vector=[1, 2, 3])
+
+    with pytest.raises(WeaviateInsertManyAllFailedError):
+        collection.data.insert_many(
+            [wvc.data.DataObject(properties={"title": "Hello"}, vector=[1, 2, 3])]
+        )
+
+
+def test_single_vectorizer_with_named_vectors(collection_factory: CollectionFactory) -> None:
+    collection = collection_factory(
+        properties=[wvc.config.Property(name="title", data_type=wvc.config.DataType.TEXT)],
+        vectorizer_config=wvc.config.Configure.Vectorizer.none(),
+    )
+    with pytest.raises(UnexpectedStatusCodeError):
+        collection.data.insert(properties={"title": "Hello"}, vector={"something": [1, 2, 3]})
+
+    with pytest.raises(WeaviateInsertManyAllFailedError):
+        collection.data.insert_many(
+            [wvc.data.DataObject(properties={"title": "Hello"}, vector={"something": [1, 2, 3]})]
+        )
diff --git a/usecases/objects/validation/vector_validation_test.go b/usecases/objects/validation/vector_validation_test.go
new file mode 100644
index 0000000000..c9db6932f3
--- /dev/null
+++ b/usecases/objects/validation/vector_validation_test.go
@@ -0,0 +1,85 @@
+//                           _       _
+// __      _____  __ ___   ___  __ _| |_ ___
+// \ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \
+//  \ V  V /  __/ (_| |\ V /| | (_| | ||  __/
+//   \_/\_/ \___|\__,_| \_/ |_|\__,_|\__\___|
+//
+//  Copyright © 2016 - 2024 Weaviate B.V. All rights reserved.
+//
+//  CONTACT: hello@weaviate.io
+//
+
+package validation
+
+import (
+	"context"
+	"testing"
+
+	"github.com/go-openapi/strfmt"
+	"github.com/stretchr/testify/require"
+	"github.com/weaviate/weaviate/entities/additional"
+	"github.com/weaviate/weaviate/entities/models"
+)
+
+func TestVectors(t *testing.T) {
+	specs := map[string]struct {
+		class  *models.Class
+		obj    *models.Object
+		objNew *models.Object
+		expErr bool
+	}{
+		"multiple named vectors with 'old' vector": {
+			class: &models.Class{
+				VectorConfig: map[string]models.VectorConfig{"first": {}, "second": {}, "third": {}}, // content does not matter
+			},
+			obj:    &models.Object{Vector: []float32{1, 2, 3}},
+			expErr: true,
+		},
+		"multiple named vectors with vectors": {
+			class: &models.Class{
+				VectorConfig: map[string]models.VectorConfig{"first": {}, "second": {}, "third": {}}, // content does not matter
+			},
+			obj: &models.Object{
+				Vectors: models.Vectors{"first": []float32{1, 2, 3}},
+			},
+			expErr: false,
+		},
+		"single named vectors with vector": {
+			class: &models.Class{
+				VectorConfig: map[string]models.VectorConfig{"first": {}}, // content does not matter
+			},
+			obj: &models.Object{Vector: []float32{1, 2, 3}},
+			objNew: &models.Object{
+				Vectors: models.Vectors{"first": []float32{1, 2, 3}},
+			},
+			expErr: false,
+		},
+		"old vector with named vectors": {
+			class: &models.Class{
+				VectorIndexConfig: models.VectorConfig{}, // content does not matter
+			},
+			obj: &models.Object{
+				Vectors: models.Vectors{"first": []float32{1, 2, 3}},
+			},
+			expErr: true,
+		},
+	}
+	for name, spec := range specs {
+		t.Run(name, func(t *testing.T) {
+			validator := &Validator{exists: func(_ context.Context, class string, _ strfmt.UUID, _ *additional.ReplicationProperties, _ string) (bool, error) {
+				return true, nil
+			}}
+			gotErr := validator.vector(context.Background(), spec.class, spec.obj)
+
+			if spec.objNew != nil {
+				require.Equal(t, spec.objNew, spec.obj)
+			}
+
+			if spec.expErr {
+				require.Error(t, gotErr)
+				return
+			}
+			require.NoError(t, gotErr)
+		})
+	}
+}