Repository: weaviate/weaviate
Pull request #6074 (2024-10-22 09:09:21)

Merge stable/v1.25 into stable/v1.26
### What's being changed:

Merge stable/v1.25 into stable/v1.26

### 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.

<!-- Uncomment the following section if this PR requires changes in related projects (e.g., documentation, client libraries).

GitHub actions will automatically create an issue in the corresponding repository for each checked box below. (See `.github/workflows/create-cross-functional-issues.yml`)

### Cross-functional impact

- [ ] This change requires public documentation (weaviate-io) to be updated. Check the box to automatically create a corresponding issue.
- Does it require a change in the client libraries? If yes, please check the boxes for the affected client libraries.
    - [ ] Python (weaviate-python-client)
    - [ ] JavaScript/TypeScript (typescript-client)
    - [ ] Go (weaviate-go-client)
    - [ ] Java (java-client)

-->



Patch:
diff --git a/.github/workflows/pull_requests.yaml b/.github/workflows/pull_requests.yaml
index 4b33d65a42..4dec0fa811 100644
--- a/.github/workflows/pull_requests.yaml
+++ b/.github/workflows/pull_requests.yaml
@@ -77,7 +77,7 @@ jobs:
         run: |
           echo "${{ env.SKIPPED_VULN }}" | tr ' ' '\n' > .trivyignore
       - name: Run Trivy vulnerability scanner for the built image
-        uses: aquasecurity/trivy-action@0.27.0
+        uses: aquasecurity/trivy-action@0.28.0
         with:
           image-ref: 'weaviate:${{ github.sha }}'
           exit-code: '1'
@@ -146,6 +146,7 @@ jobs:
           go-version: '1.22'
           cache: true
       - name: Login to Docker Hub
+        if: ${{ !github.event.pull_request.head.repo.fork && github.triggering_actor != 'dependabot[bot]' }}
         uses: docker/login-action@v3
         with:
           username: ${{secrets.DOCKER_USERNAME}}
@@ -179,6 +180,7 @@ jobs:
           cache: true
       - name: Login to Docker Hub
         uses: docker/login-action@v3
+        if: ${{ !github.event.pull_request.head.repo.fork && github.triggering_actor != 'dependabot[bot]' }}
         with:
           username: ${{secrets.DOCKER_USERNAME}}
           password: ${{secrets.DOCKER_PASSWORD}}
@@ -281,6 +283,7 @@ jobs:
           cache: true
       - name: Login to Docker Hub
         uses: docker/login-action@v3
+        if: ${{ !github.event.pull_request.head.repo.fork && github.triggering_actor != 'dependabot[bot]' }}
         with:
           username: ${{secrets.DOCKER_USERNAME}}
           password: ${{secrets.DOCKER_PASSWORD}}
@@ -305,11 +308,27 @@ jobs:
           cache: true
       - name: Login to Docker Hub
         uses: docker/login-action@v3
+        if: ${{ !github.event.pull_request.head.repo.fork && github.triggering_actor != 'dependabot[bot]' }}
         with:
           username: ${{secrets.DOCKER_USERNAME}}
           password: ${{secrets.DOCKER_PASSWORD}}
+      - name: Determine retry max_attempts value
+        env:
+          MATRIX_TEST: ${{ matrix.test }}
+        run: |
+          if [[ "$MATRIX_TEST" == *"lsmkv"* ]]; then
+            echo "retry_max_attempts=2" >> $GITHUB_ENV
+          else
+            echo "retry_max_attempts=1" >> $GITHUB_ENV
+          fi
       - name: Acceptance tests Large
-        run: ./test/run.sh ${{ matrix.test }}
+        uses: nick-fields/retry@v3
+        with:
+          # 15 Minute is a large enough timeout for most of our tests
+          timeout_minutes: 15
+          max_attempts: ${{ env.retry_max_attempts }}
+          command: ./test/run.sh ${{ matrix.test }}
+          on_retry_command: ./test/run.sh --cleanup
   Codecov:
     needs: [Unit-Tests, Integration-Tests]
     name: codecov
diff --git a/adapters/handlers/graphql/local/get/class_builder_fields.go b/adapters/handlers/graphql/local/get/class_builder_fields.go
index c25a66d479..89bd063a2a 100644
--- a/adapters/handlers/graphql/local/get/class_builder_fields.go
+++ b/adapters/handlers/graphql/local/get/class_builder_fields.go
@@ -353,7 +353,7 @@ func (r *resolver) resolveGet(p graphql.ResolveParams, className string) (interf
 
 	selectionsOfClass := p.Info.FieldASTs[0].SelectionSet
 
-	properties, addlProps, err := extractProperties(
+	properties, addlProps, groupByProperties, err := extractProperties(
 		className, selectionsOfClass, p.Info.Fragments, r.modulesProvider)
 	if err != nil {
 		return nil, err
@@ -441,6 +441,7 @@ func (r *resolver) resolveGet(p graphql.ResolveParams, className string) (interf
 	var groupByParams *searchparams.GroupBy
 	if groupBy, ok := p.Args["groupBy"]; ok {
 		p := common_filters.ExtractGroupBy(groupBy.(map[string]interface{}))
+		p.Properties = groupByProperties
 		groupByParams = &p
 	}
 
@@ -616,8 +617,9 @@ func fieldNameIsOfObjectButNonReferenceType(field string) bool {
 func extractProperties(className string, selections *ast.SelectionSet,
 	fragments map[string]ast.Definition,
 	modulesProvider ModulesProvider,
-) ([]search.SelectProperty, additional.Properties, error) {
+) ([]search.SelectProperty, additional.Properties, []search.SelectProperty, error) {
 	var properties []search.SelectProperty
+	var additionalGroupHitProperties []search.SelectProperty
 	var additionalProps additional.Properties
 	additionalCheck := &additionalCheck{modulesProvider}
 
@@ -692,11 +694,11 @@ func extractProperties(className string, selections *ast.SelectionSet,
 						}
 						if additionalProperty == "group" {
 							additionalProps.Group = true
-							additionalGroupHitProperties, err := extractGroupHitProperties(className, additionalProps, subSelection, fragments, modulesProvider)
+							var err error
+							additionalGroupHitProperties, err = extractGroupHitProperties(className, additionalProps, subSelection, fragments, modulesProvider)
 							if err != nil {
-								return nil, additionalProps, err
+								return nil, additionalProps, nil, err
 							}
-							properties = append(properties, additionalGroupHitProperties...)
 							continue
 						}
 						if modulesProvider != nil {
@@ -714,7 +716,7 @@ func extractProperties(className string, selections *ast.SelectionSet,
 				case *ast.FragmentSpread:
 					ref, err := extractFragmentSpread(className, s, fragments, modulesProvider)
 					if err != nil {
-						return nil, additionalProps, err
+						return nil, additionalProps, nil, err
 					}
 
 					property.Refs = append(property.Refs, ref)
@@ -722,13 +724,13 @@ func extractProperties(className string, selections *ast.SelectionSet,
 				case *ast.InlineFragment:
 					ref, err := extractInlineFragment(className, s, fragments, modulesProvider)
 					if err != nil {
-						return nil, additionalProps, err
+						return nil, additionalProps, nil, err
 					}
 
 					property.Refs = append(property.Refs, ref)
 
 				default:
-					return nil, additionalProps, fmt.Errorf("unrecoginzed type in subs-selection: %T", subSelection)
+					return nil, additionalProps, nil, fmt.Errorf("unrecoginzed type in subs-selection: %T", subSelection)
 				}
 			}
 		}
@@ -740,7 +742,7 @@ func extractProperties(className string, selections *ast.SelectionSet,
 		properties = append(properties, property)
 	}
 
-	return properties, additionalProps, nil
+	return properties, additionalProps, additionalGroupHitProperties, nil
 }
 
 func extractGroupHitProperties(
@@ -767,7 +769,7 @@ func extractGroupHitProperties(
 													return nil, err
 												}
 
-												additionalGroupHitProp := search.SelectProperty{Name: fmt.Sprintf("_additional:group:hits:%v", hf.Name.Value)}
+												additionalGroupHitProp := search.SelectProperty{Name: hf.Name.Value}
 												additionalGroupHitProp.Refs = append(additionalGroupHitProp.Refs, ref)
 												additionalGroupProperties = append(additionalGroupProperties, additionalGroupHitProp)
 											}
@@ -814,7 +816,7 @@ func extractInlineFragment(class string, fragment *ast.InlineFragment,
 		return result, fmt.Errorf("retrieving cross-refs by beacon is not supported yet - coming soon!")
 	}
 
-	subProperties, additionalProperties, err := extractProperties(class, fragment.SelectionSet, fragments, modulesProvider)
+	subProperties, additionalProperties, _, err := extractProperties(class, fragment.SelectionSet, fragments, modulesProvider)
 	if err != nil {
 		return result, err
 	}
@@ -842,7 +844,7 @@ func extractFragmentSpread(class string, spread *ast.FragmentSpread,
 		return result, err
 	}
 
-	subProperties, additionalProperties, err := extractProperties(class, def.GetSelectionSet(), fragments, modulesProvider)
+	subProperties, additionalProperties, _, err := extractProperties(class, def.GetSelectionSet(), fragments, modulesProvider)
 	if err != nil {
 		return result, err
 	}
diff --git a/adapters/handlers/grpc/v1/parse_search_request.go b/adapters/handlers/grpc/v1/parse_search_request.go
index d8a2010b59..d27d4c18e7 100644
--- a/adapters/handlers/grpc/v1/parse_search_request.go
+++ b/adapters/handlers/grpc/v1/parse_search_request.go
@@ -355,10 +355,21 @@ func extractGroupBy(groupIn *pb.GroupBy, out *dto.GetParams) (*searchparams.Grou
 		return nil, fmt.Errorf("groupby path can only have one entry, received %v", groupIn.Path)
 	}
 
+	var additionalGroupProperties []search.SelectProperty
+	for _, prop := range out.Properties {
+		additionalGroupHitProp := search.SelectProperty{Name: prop.Name}
+		additionalGroupHitProp.Refs = append(additionalGroupHitProp.Refs, prop.Refs...)
+		additionalGroupHitProp.IsPrimitive = prop.IsPrimitive
+		additionalGroupHitProp.IsObject = prop.IsObject
+		additionalGroupProperties = append(additionalGroupProperties, additionalGroupHitProp)
+
+	}
+
 	groupOut := &searchparams.GroupBy{
 		Property:        groupIn.Path[0],
 		ObjectsPerGroup: int(groupIn.ObjectsPerGroup),
 		Groups:          int(groupIn.NumberOfGroups),
+		Properties:      additionalGroupProperties,
 	}
 
 	// add the property in case it was not requested as return prop - otherwise it is not resolved
diff --git a/adapters/repos/db/index_queue.go b/adapters/repos/db/index_queue.go
index e3436f6e3a..a19dd5db9b 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 {
-	compareAndSwapStatus(old, new string) (storagestate.Status, error)
+	compareAndSwapStatusIndexingAndReady(old, new string) (storagestate.Status, error)
 	Name() string
 }
 
@@ -415,12 +415,12 @@ func (q *IndexQueue) indexer() {
 			}
 
 			if q.Size() == 0 {
-				_, _ = q.Shard.compareAndSwapStatus(storagestate.StatusIndexing.String(), storagestate.StatusReady.String())
+				_, _ = q.Shard.compareAndSwapStatusIndexingAndReady(storagestate.StatusIndexing.String(), storagestate.StatusReady.String())
 				q.indexLock.RUnlock()
 				continue
 			}
 
-			status, err := q.Shard.compareAndSwapStatus(storagestate.StatusReady.String(), storagestate.StatusIndexing.String())
+			status, err := q.Shard.compareAndSwapStatusIndexingAndReady(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/refcache/cacher.go b/adapters/repos/db/refcache/cacher.go
index ebe275304b..951adead92 100644
--- a/adapters/repos/db/refcache/cacher.go
+++ b/adapters/repos/db/refcache/cacher.go
@@ -31,24 +31,11 @@ type repo interface {
 }
 
 func NewCacher(repo repo, logger logrus.FieldLogger, tenant string) *Cacher {
-	return &Cacher{
-		logger:    logger,
-		repo:      repo,
-		store:     map[multi.Identifier]search.Result{},
-		withGroup: false,
-		tenant:    tenant,
-	}
-}
-
-func NewCacherWithGroup(repo repo, logger logrus.FieldLogger, tenant string) *Cacher {
 	return &Cacher{
 		logger: logger,
 		repo:   repo,
 		store:  map[multi.Identifier]search.Result{},
-		// for groupBy feature
-		withGroup:                true,
-		getGroupSelectProperties: getGroupSelectProperties,
-		tenant:                   tenant,
+		tenant: tenant,
 	}
 }
 
@@ -60,15 +47,13 @@ type cacherJob struct {
 
 type Cacher struct {
 	sync.Mutex
-	jobs       []cacherJob
-	logger     logrus.FieldLogger
-	repo       repo
-	store      map[multi.Identifier]search.Result
-	additional additional.Properties // meta is immutable for the lifetime of the request cacher, so we can safely store it
-	// for groupBy feature
-	withGroup                bool
-	getGroupSelectProperties func(properties search.SelectProperties) search.SelectProperties
-	tenant                   string
+	jobs         []cacherJob
+	logger       logrus.FieldLogger
+	repo         repo
+	store        map[multi.Identifier]search.Result
+	additional   additional.Properties // meta is immutable for the lifetime of the request cacher, so we can safely store it
+	tenant       string
+	groupByProps search.SelectProperties
 }
 
 func (c *Cacher) Get(si multi.Identifier) (search.Result, bool) {
@@ -91,9 +76,10 @@ func (c *Cacher) Get(si multi.Identifier) (search.Result, bool) {
 //
 // This keeps request times to a minimum even on deeply nested requests.
 func (c *Cacher) Build(ctx context.Context, objects []search.Result,
-	properties search.SelectProperties, additional additional.Properties,
+	properties search.SelectProperties, additional additional.Properties, groupByProperties search.SelectProperties,
 ) error {
 	c.additional = additional
+	c.groupByProps = groupByProperties
 	err := c.findJobsFromResponse(objects, properties)
 	if err != nil {
 		return fmt.Errorf("build request cache: %v", err)
@@ -145,8 +131,8 @@ func (c *Cacher) findJobsFromResponse(objects []search.Result, properties search
 			return err
 		}
 
-		if c.withGroup {
-			if err := c.parseAdditionalGroup(obj, properties); err != nil {
+		if c.groupByProps != nil {
+			if err := c.parseAdditionalGroup(obj); err != nil {
 				return err
 			}
 		}
@@ -155,11 +141,11 @@ func (c *Cacher) findJobsFromResponse(objects []search.Result, properties search
 	return nil
 }
 
-func (c *Cacher) parseAdditionalGroup(obj search.Result, properties search.SelectProperties) error {
+func (c *Cacher) parseAdditionalGroup(obj search.Result) error {
 	if obj.AdditionalProperties != nil && obj.AdditionalProperties["group"] != nil {
 		if group, ok := obj.AdditionalProperties["group"].(*additional.Group); ok {
 			for _, hitMap := range group.Hits {
-				if err := c.parseSchemaMap(hitMap, c.getGroupSelectProperties(properties)); err != nil {
+				if err := c.parseSchemaMap(hitMap, c.groupByProps); err != nil {
 					return err
 				}
 			}
@@ -367,7 +353,7 @@ func (c *Cacher) parseAndStore(ctx context.Context, res []search.Result) error {
 	// iteration which will eventually come to this place again
 	c.markAllJobsAsDone()
 
-	err := c.Build(ctx, removeEmptyResults(res), nil, c.additional)
+	err := c.Build(ctx, removeEmptyResults(res), nil, c.additional, nil)
 	if err != nil {
 		return errors.Wrap(err, "build nested cache")
 	}
diff --git a/adapters/repos/db/refcache/group_properties_util.go b/adapters/repos/db/refcache/group_properties_util.go
deleted file mode 100644
index 6f61020671..0000000000
--- a/adapters/repos/db/refcache/group_properties_util.go
+++ /dev/null
@@ -1,33 +0,0 @@
-//                           _       _
-// __      _____  __ ___   ___  __ _| |_ ___
-// \ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \
-//  \ V  V /  __/ (_| |\ V /| | (_| | ||  __/
-//   \_/\_/ \___|\__,_| \_/ |_|\__,_|\__\___|
-//
-//  Copyright © 2016 - 2024 Weaviate B.V. All rights reserved.
-//
-//  CONTACT: hello@weaviate.io
-//
-
-package refcache
-
-import (
-	"strings"
-
-	"github.com/weaviate/weaviate/entities/search"
-)
-
-func getGroupSelectProperties(properties search.SelectProperties) search.SelectProperties {
-	var selectGroupHitsProperties search.SelectProperties
-	for _, prop := range properties {
-		if strings.HasPrefix(prop.Name, "_additional:group:hits") {
-			selectGroupHitsProperties = append(selectGroupHitsProperties, search.SelectProperty{
-				Name:            strings.Replace(prop.Name, "_additional:group:hits:", "", 1),
-				IsPrimitive:     prop.IsPrimitive,
-				IncludeTypeName: prop.IncludeTypeName,
-				Refs:            prop.Refs,
-			})
-		}
-	}
-	return selectGroupHitsProperties
-}
diff --git a/adapters/repos/db/refcache/resolver.go b/adapters/repos/db/refcache/resolver.go
index cfe3929698..ef57eff783 100644
--- a/adapters/repos/db/refcache/resolver.go
+++ b/adapters/repos/db/refcache/resolver.go
@@ -26,12 +26,12 @@ import (
 type Resolver struct {
 	cacher cacher
 	// for groupBy feature
-	withGroup                bool
-	getGroupSelectProperties func(properties search.SelectProperties) search.SelectProperties
+	withGroup    bool
+	groupByProps search.SelectProperties
 }
 
 type cacher interface {
-	Build(ctx context.Context, objects []search.Result, properties search.SelectProperties, additional additional.Properties) error
+	Build(ctx context.Context, objects []search.Result, properties search.SelectProperties, additional additional.Properties, groupByProperties search.SelectProperties) error
 	Get(si multi.Identifier) (search.Result, bool)
 }
 
@@ -39,19 +39,24 @@ func NewResolver(cacher cacher) *Resolver {
 	return &Resolver{cacher: cacher}
 }
 
-func NewResolverWithGroup(cacher cacher) *Resolver {
+func NewResolverWithGroup(cacher cacher, groupByProps search.SelectProperties) *Resolver {
 	return &Resolver{
 		cacher: cacher,
 		// for groupBy feature
-		withGroup:                true,
-		getGroupSelectProperties: getGroupSelectProperties,
+		withGroup:    true,
+		groupByProps: groupByProps,
 	}
 }
 
 func (r *Resolver) Do(ctx context.Context, objects []search.Result,
 	properties search.SelectProperties, additional additional.Properties,
 ) ([]search.Result, error) {
-	if err := r.cacher.Build(ctx, objects, properties, additional); err != nil {
+	cacherProps := properties
+	if !r.withGroup {
+		cacherProps = append(properties, r.groupByProps...)
+	}
+
+	if err := r.cacher.Build(ctx, objects, cacherProps, additional, r.groupByProps); err != nil {
 		return nil, errors.Wrap(err, "build reference cache")
 	}
 
@@ -109,7 +114,7 @@ func (r *Resolver) parseAdditionalGroup(
 	if additionalProperties != nil && additionalProperties["group"] != nil {
 		if group, ok := additionalProperties["group"].(*additional.Group); ok {
 			for j, hit := range group.Hits {
-				schema, err := r.parseSchema(hit, r.getGroupSelectProperties(properties))
+				schema, err := r.parseSchema(hit, r.groupByProps)
 				if err != nil {
 					return additionalProperties, fmt.Errorf("resolve group hit: %w", err)
 				}
diff --git a/adapters/repos/db/replication.go b/adapters/repos/db/replication.go
index 40e2792d8f..53383c87b6 100644
--- a/adapters/repos/db/replication.go
+++ b/adapters/repos/db/replication.go
@@ -162,7 +162,7 @@ func (i *Index) writableShard(name string) (ShardLike, func(), *replica.SimpleRe
 			{Code: replica.StatusShardNotFound, Msg: name},
 		}}
 	}
-	if localShard.isReadOnly() {
+	if localShard.isReadOnly() != nil {
 		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 767c27286e..3fbd7a6821 100644
--- a/adapters/repos/db/resource_use.go
+++ b/adapters/repos/db/resource_use.go
@@ -18,7 +18,6 @@ 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"
 )
 
@@ -130,7 +129,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()
+			db.setShardsReadOnly(fmt.Sprintf("disk usage too high. Set to read-only at %.2f%%, threshold set to %.2f%%\"", pu, float64(diskROPercent)))
 			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%%",
@@ -143,7 +142,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()
+			db.setShardsReadOnly(fmt.Sprintf("memory usage too high. Set to read-only at %.2f%%, threshold set to %.2f%%\"", pu, float64(memROPercent)))
 			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%%",
@@ -152,11 +151,11 @@ func (db *DB) memUseReadonly(mon *memwatch.Monitor) {
 	}
 }
 
-func (db *DB) setShardsReadOnly() {
+func (db *DB) setShardsReadOnly(reason string) {
 	db.indexLock.Lock()
 	for _, index := range db.indices {
 		index.ForEachShard(func(name string, shard ShardLike) error {
-			err := shard.UpdateStatus(storagestate.StatusReadOnly.String())
+			err := shard.SetStatusReadonly(reason)
 			if err != nil {
 				db.logger.WithField("action", "set_shard_read_only").
 					WithField("path", db.config.RootPath).
diff --git a/adapters/repos/db/search.go b/adapters/repos/db/search.go
index 2052019053..3ce16bf4cc 100644
--- a/adapters/repos/db/search.go
+++ b/adapters/repos/db/search.go
@@ -317,7 +317,7 @@ func (db *DB) ResolveReferences(ctx context.Context, objs search.Results,
 	}
 
 	if groupBy != nil {
-		res, err := refcache.NewResolverWithGroup(refcache.NewCacherWithGroup(db, db.logger, tenant)).
+		res, err := refcache.NewResolverWithGroup(refcache.NewCacher(db, db.logger, tenant), groupBy.Properties).
 			Do(ctx, objs, props, addl)
 		if err != nil {
 			return nil, fmt.Errorf("resolve cross-refs: %w", err)
diff --git a/adapters/repos/db/shard.go b/adapters/repos/db/shard.go
index 3fc44a2367..5c90412988 100644
--- a/adapters/repos/db/shard.go
+++ b/adapters/repos/db/shard.go
@@ -69,6 +69,7 @@ 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
@@ -119,7 +120,7 @@ type ShardLike interface {
 	// TODO tests only
 	Versioner() *shardVersioner // Get the shard versioner
 
-	isReadOnly() bool
+	isReadOnly() error
 
 	preparePutObject(context.Context, string, *storobj.Object) replica.SimpleResponse
 	preparePutObjects(context.Context, string, []*storobj.Object) replica.SimpleResponse
@@ -204,7 +205,7 @@ type Shard struct {
 	lastComparedHosts    []string
 	lastComparedHostsMux sync.RWMutex
 
-	status              storagestate.Status
+	status              ShardStatus
 	statusLock          sync.Mutex
 	propertyIndicesLock sync.RWMutex
 
@@ -625,11 +626,11 @@ func (s *Shard) segmentCleanupConfig() lsmkv.BucketOption {
 }
 
 func (s *Shard) UpdateVectorIndexConfig(ctx context.Context, updated schemaConfig.VectorIndexConfig) error {
-	if s.isReadOnly() {
-		return storagestate.ErrStatusReadOnly
+	if err := s.isReadOnly(); err != nil {
+		return err
 	}
 
-	err := s.UpdateStatus(storagestate.StatusReadOnly.String())
+	err := s.SetStatusReadonly("UpdateVectorIndexConfig")
 	if err != nil {
 		return fmt.Errorf("attempt to mark read-only: %w", err)
 	}
@@ -640,10 +641,10 @@ func (s *Shard) UpdateVectorIndexConfig(ctx context.Context, updated schemaConfi
 }
 
 func (s *Shard) UpdateVectorIndexConfigs(ctx context.Context, updated map[string]schemaConfig.VectorIndexConfig) error {
-	if s.isReadOnly() {
-		return storagestate.ErrStatusReadOnly
+	if err := s.isReadOnly(); err != nil {
+		return err
 	}
-	if err := s.UpdateStatus(storagestate.StatusReadOnly.String()); err != nil {
+	if err := s.SetStatusReadonly("UpdateVectorIndexConfig"); 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 4d59a674bf..2d1878132f 100644
--- a/adapters/repos/db/shard_geo_props.go
+++ b/adapters/repos/db/shard_geo_props.go
@@ -20,7 +20,6 @@ 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"
 )
 
@@ -94,9 +93,10 @@ func geoPropID(propName string) string {
 func (s *Shard) updatePropertySpecificIndices(object *storobj.Object,
 	status objectInsertStatus,
 ) error {
-	if s.isReadOnly() {
-		return storagestate.ErrStatusReadOnly
+	if err := s.isReadOnly(); err != nil {
+		return err
 	}
+
 	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 s.isReadOnly() {
-		return storagestate.ErrStatusReadOnly
+	if err := s.isReadOnly(); err != nil {
+		return err
 	}
 
 	// 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 s.isReadOnly() {
-		return storagestate.ErrStatusReadOnly
+	if err := s.isReadOnly(); err != nil {
+		return err
 	}
 
 	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 s.isReadOnly() {
-		return storagestate.ErrStatusReadOnly
+	if err := s.isReadOnly(); err != nil {
+		return err
 	}
 
 	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 65bb5885fd..169426754e 100644
--- a/adapters/repos/db/shard_init.go
+++ b/adapters/repos/db/shard_init.go
@@ -26,7 +26,6 @@ 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"
 )
 
@@ -58,7 +57,7 @@ func NewShard(ctx context.Context, promMetrics *monitoring.PrometheusMetrics,
 		shut:         false,
 		shutdownLock: new(sync.RWMutex),
 
-		status: storagestate.StatusLoading,
+		status: NewShardStatus(),
 	}
 
 	defer func() {
diff --git a/adapters/repos/db/shard_init_properties.go b/adapters/repos/db/shard_init_properties.go
index ad0a75af69..e1e55ff2f5 100644
--- a/adapters/repos/db/shard_init_properties.go
+++ b/adapters/repos/db/shard_init_properties.go
@@ -15,16 +15,14 @@ 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) {
@@ -88,8 +86,8 @@ func (s *Shard) initPropertyBuckets(ctx context.Context, eg *enterrors.ErrorGrou
 }
 
 func (s *Shard) createPropertyValueIndex(ctx context.Context, prop *models.Property) error {
-	if s.isReadOnly() {
-		return storagestate.ErrStatusReadOnly
+	if err := s.isReadOnly(); err != nil {
+		return err
 	}
 
 	bucketOpts := []lsmkv.BucketOption{
@@ -154,8 +152,8 @@ func (s *Shard) createPropertyValueIndex(ctx context.Context, prop *models.Prope
 }
 
 func (s *Shard) createPropertyLengthIndex(ctx context.Context, prop *models.Property) error {
-	if s.isReadOnly() {
-		return storagestate.ErrStatusReadOnly
+	if err := s.isReadOnly(); err != nil {
+		return err
 	}
 
 	// some datatypes are not added to the inverted index, so we can skip them here
@@ -177,8 +175,8 @@ func (s *Shard) createPropertyLengthIndex(ctx context.Context, prop *models.Prop
 }
 
 func (s *Shard) createPropertyNullIndex(ctx context.Context, prop *models.Property) error {
-	if s.isReadOnly() {
-		return storagestate.ErrStatusReadOnly
+	if err := s.isReadOnly(); err != nil {
+		return err
 	}
 
 	return s.store.CreateOrLoadBucket(ctx,
@@ -192,8 +190,8 @@ func (s *Shard) createPropertyNullIndex(ctx context.Context, prop *models.Proper
 }
 
 func (s *Shard) addIDProperty(ctx context.Context) error {
-	if s.isReadOnly() {
-		return storagestate.ErrStatusReadOnly
+	if err := s.isReadOnly(); err != nil {
+		return err
 	}
 
 	err := s.store.CreateOrLoadBucket(ctx,
@@ -212,8 +210,8 @@ func (s *Shard) addIDProperty(ctx context.Context) error {
 }
 
 func (s *Shard) addDimensionsProperty(ctx context.Context) error {
-	if s.isReadOnly() {
-		return storagestate.ErrStatusReadOnly
+	if err := s.isReadOnly(); err != nil {
+		return err
 	}
 
 	// Note: this data would fit the "Set" type better, but since the "Map" type
@@ -234,8 +232,8 @@ func (s *Shard) addDimensionsProperty(ctx context.Context) error {
 }
 
 func (s *Shard) addTimestampProperties(ctx context.Context) error {
-	if s.isReadOnly() {
-		return storagestate.ErrStatusReadOnly
+	if err := s.isReadOnly(); err != nil {
+		return err
 	}
 
 	if err := s.addCreationTimeUnixProperty(ctx); err != nil {
diff --git a/adapters/repos/db/shard_lazyloader.go b/adapters/repos/db/shard_lazyloader.go
index 0176e36f60..942555b503 100644
--- a/adapters/repos/db/shard_lazyloader.go
+++ b/adapters/repos/db/shard_lazyloader.go
@@ -165,6 +165,11 @@ 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
@@ -505,7 +510,7 @@ func (l *LazyLoadShard) Versioner() *shardVersioner {
 	return l.shard.Versioner()
 }
 
-func (l *LazyLoadShard) isReadOnly() bool {
+func (l *LazyLoadShard) isReadOnly() error {
 	l.mustLoad()
 	return l.shard.isReadOnly()
 }
diff --git a/adapters/repos/db/shard_status.go b/adapters/repos/db/shard_status.go
index feeb2816b2..747c7908e5 100644
--- a/adapters/repos/db/shard_status.go
+++ b/adapters/repos/db/shard_status.go
@@ -12,24 +12,39 @@
 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 = storagestate.StatusReady
+	s.status.Init()
 }
 
 func (s *Shard) GetStatus() storagestate.Status {
 	s.statusLock.Lock()
 	defer s.statusLock.Unlock()
 
-	return s.status
+	return s.status.Status
 }
 
 // Same implem for for a regular shard, this only differ in lazy loaded shards
@@ -37,37 +52,59 @@ func (s *Shard) GetStatusNoLoad() storagestate.Status {
 	return s.GetStatus()
 }
 
-func (s *Shard) isReadOnly() bool {
-	return s.GetStatus() == storagestate.StatusReadOnly
+// 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) compareAndSwapStatus(old, new string) (storagestate.Status, error) {
+func (s *Shard) compareAndSwapStatusIndexingAndReady(old, new string) (storagestate.Status, error) {
 	s.statusLock.Lock()
 	defer s.statusLock.Unlock()
 
-	if s.status.String() != old {
-		return s.status, nil
+	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)
 	}
 
-	return s.status, s.updateStatusUnlocked(new)
+	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)
 }
 
 func (s *Shard) UpdateStatus(in string) error {
 	s.statusLock.Lock()
 	defer s.statusLock.Unlock()
 
-	return s.updateStatusUnlocked(in)
+	reason := ""
+	if in == storagestate.StatusReadOnly.String() {
+		reason = "manually set by user"
+	}
+
+	return s.updateStatusUnlocked(in, reason)
 }
 
 // updateStatusUnlocked updates the status without locking the statusLock.
 // Warning: Use UpdateStatus instead.
-func (s *Shard) updateStatusUnlocked(in string) error {
+func (s *Shard) updateStatusUnlocked(in, reason string) error {
 	targetStatus, err := storagestate.ValidateStatus(strings.ToUpper(in))
 	if err != nil {
 		return errors.Wrap(err, in)
 	}
 
-	s.status = targetStatus
+	s.status.Status = targetStatus
+	s.status.Reason = reason
 
 	err = s.updateStoreStatus(targetStatus)
 	if err != nil {
@@ -78,7 +115,8 @@ func (s *Shard) updateStatusUnlocked(in string) error {
 		WithField("action", "update shard status").
 		WithField("class", s.index.Config.ClassName).
 		WithField("shard", s.name).
-		WithField("status", in)
+		WithField("status", in).
+		WithField("readOnlyReason", reason)
 
 	return nil
 }
diff --git a/adapters/repos/db/shard_write_batch_delete.go b/adapters/repos/db/shard_write_batch_delete.go
index 9f8e779e61..a5a9a5f1c0 100644
--- a/adapters/repos/db/shard_write_batch_delete.go
+++ b/adapters/repos/db/shard_write_batch_delete.go
@@ -24,16 +24,15 @@ 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 s.isReadOnly() {
+	if err := s.isReadOnly(); err != nil {
 		return objects.BatchSimpleObjects{
-			objects.BatchSimpleObject{Err: storagestate.ErrStatusReadOnly},
+			objects.BatchSimpleObject{Err: err},
 		}
 	}
 	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 7295b93bc7..df83871ab1 100644
--- a/adapters/repos/db/shard_write_batch_objects.go
+++ b/adapters/repos/db/shard_write_batch_objects.go
@@ -26,7 +26,6 @@ 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"
 )
 
@@ -35,8 +34,8 @@ func (s *Shard) PutObjectBatch(ctx context.Context,
 	objects []*storobj.Object,
 ) []error {
 	s.activityTracker.Add(1)
-	if s.isReadOnly() {
-		return []error{storagestate.ErrStatusReadOnly}
+	if err := s.isReadOnly(); err != nil {
+		return []error{err}
 	}
 
 	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 c6ecd81bdb..50b13db6d1 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 s.isReadOnly() {
-		return []error{errors.Errorf("shard is read-only")}
+	if err := s.isReadOnly(); err != nil {
+		return []error{err}
 	}
 
 	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 e3f299e342..1e9bed87d6 100644
--- a/adapters/repos/db/shard_write_delete.go
+++ b/adapters/repos/db/shard_write_delete.go
@@ -21,13 +21,12 @@ import (
 	"github.com/spaolacci/murmur3"
 	"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 s.isReadOnly() {
-		return storagestate.ErrStatusReadOnly
+	if err := s.isReadOnly(); err != nil {
+		return err
 	}
 
 	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 5a8c66d5c7..6d88c959e5 100644
--- a/adapters/repos/db/shard_write_merge.go
+++ b/adapters/repos/db/shard_write_merge.go
@@ -19,7 +19,6 @@ 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"
 )
@@ -28,8 +27,8 @@ var errObjectNotFound = errors.New("object not found")
 
 func (s *Shard) MergeObject(ctx context.Context, merge objects.MergeDocument) error {
 	s.activityTracker.Add(1)
-	if s.isReadOnly() {
-		return storagestate.ErrStatusReadOnly
+	if err := s.isReadOnly(); err != nil {
+		return err
 	}
 
 	if s.hasTargetVectors() {
diff --git a/adapters/repos/db/shard_write_put.go b/adapters/repos/db/shard_write_put.go
index 59c377daa3..eab9a1d421 100644
--- a/adapters/repos/db/shard_write_put.go
+++ b/adapters/repos/db/shard_write_put.go
@@ -27,20 +27,19 @@ 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 s.isReadOnly() {
-		return storagestate.ErrStatusReadOnly
+	if err := s.isReadOnly(); err != nil {
+		return err
 	}
-	uuid, err := uuid.MustParse(object.ID().String()).MarshalBinary()
+	uid, err := uuid.MustParse(object.ID().String()).MarshalBinary()
 	if err != nil {
 		return err
 	}
-	return s.putOne(ctx, uuid, object)
+	return s.putOne(ctx, uid, object)
 }
 
 func (s *Shard) putOne(ctx context.Context, uuid []byte, object *storobj.Object) error {
diff --git a/cluster/resolver/raft.go b/cluster/resolver/raft.go
index 7cb0529d05..08894c90cf 100644
--- a/cluster/resolver/raft.go
+++ b/cluster/resolver/raft.go
@@ -98,5 +98,12 @@ func (a *raft) NewTCPTransport(
 }
 
 func (a *raft) NotResolvedNodes() map[raftImpl.ServerID]struct{} {
-	return a.notResolvedNodes
+	a.nodesLock.Lock()
+	defer a.nodesLock.Unlock()
+
+	newMap := make(map[raftImpl.ServerID]struct{})
+	for k, v := range a.notResolvedNodes {
+		newMap[k] = v
+	}
+	return newMap
 }
diff --git a/entities/searchparams/retrieval.go b/entities/searchparams/retrieval.go
index f9e80ade4a..67942cd227 100644
--- a/entities/searchparams/retrieval.go
+++ b/entities/searchparams/retrieval.go
@@ -15,6 +15,8 @@ import (
 	"fmt"
 	"strings"
 
+	"github.com/weaviate/weaviate/entities/search"
+
 	"github.com/weaviate/weaviate/entities/models"
 	"github.com/weaviate/weaviate/entities/schema"
 )
@@ -148,4 +150,5 @@ type GroupBy struct {
 	Property        string
 	Groups          int
 	ObjectsPerGroup int
+	Properties      search.SelectProperties
 }
diff --git a/entities/storagestate/status.go b/entities/storagestate/status.go
index d97e6c7c93..57cedb4cca 100644
--- a/entities/storagestate/status.go
+++ b/entities/storagestate/status.go
@@ -11,7 +11,10 @@
 
 package storagestate
 
-import "errors"
+import (
+	"errors"
+	"fmt"
+)
 
 const (
 	StatusReadOnly Status = "READONLY"
@@ -20,6 +23,10 @@ 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/modules/generative-aws/clients/aws.go b/modules/generative-aws/clients/aws.go
index 82e2185de1..ee36328b0c 100644
--- a/modules/generative-aws/clients/aws.go
+++ b/modules/generative-aws/clients/aws.go
@@ -498,7 +498,7 @@ func (v *awsClient) generateForPrompt(textProperties map[string]string, prompt s
 }
 
 func (v *awsClient) getAwsAccessKey(ctx context.Context) (string, error) {
-	if awsAccessKey := v.getHeaderValue(ctx, "X-Aws-Access-Key"); awsAccessKey != "" {
+	if awsAccessKey := modulecomponents.GetValueFromContext(ctx, "X-Aws-Access-Key"); awsAccessKey != "" {
 		return awsAccessKey, nil
 	}
 	if v.awsAccessKey != "" {
@@ -510,7 +510,7 @@ func (v *awsClient) getAwsAccessKey(ctx context.Context) (string, error) {
 }
 
 func (v *awsClient) getAwsAccessSecret(ctx context.Context) (string, error) {
-	if awsSecret := v.getHeaderValue(ctx, "X-Aws-Secret-Key"); awsSecret != "" {
+	if awsSecret := modulecomponents.GetValueFromContext(ctx, "X-Aws-Secret-Key"); awsSecret != "" {
 		return awsSecret, nil
 	}
 	if v.awsSecretKey != "" {
@@ -522,7 +522,7 @@ func (v *awsClient) getAwsAccessSecret(ctx context.Context) (string, error) {
 }
 
 func (v *awsClient) getAwsSessionToken(ctx context.Context) (string, error) {
-	if awsSessionToken := v.getHeaderValue(ctx, "X-Aws-Session-Token"); awsSessionToken != "" {
+	if awsSessionToken := modulecomponents.GetValueFromContext(ctx, "X-Aws-Session-Token"); awsSessionToken != "" {
 		return awsSessionToken, nil
 	}
 	if v.awsSessionToken != "" {
@@ -531,19 +531,6 @@ func (v *awsClient) getAwsSessionToken(ctx context.Context) (string, error) {
 	return "", nil
 }
 
-func (v *awsClient) getHeaderValue(ctx context.Context, header string) string {
-	headerValue := ctx.Value(header)
-	if value, ok := headerValue.([]string); ok &&
-		len(value) > 0 && len(value[0]) > 0 {
-		return value[0]
-	}
-	// try getting header from GRPC if not successful
-	if value := modulecomponents.GetValueFromGRPC(ctx, header); len(value) > 0 && len(value[0]) > 0 {
-		return value[0]
-	}
-	return ""
-}
-
 func (v *awsClient) isAmazonModel(model string) bool {
 	return strings.HasPrefix(model, "amazon")
 }
diff --git a/modules/ref2vec-centroid/config/config.go b/modules/ref2vec-centroid/config/config.go
index 9f2e5b57cd..392eb1ace8 100644
--- a/modules/ref2vec-centroid/config/config.go
+++ b/modules/ref2vec-centroid/config/config.go
@@ -11,7 +11,11 @@
 
 package config
 
-import "github.com/weaviate/weaviate/entities/moduletools"
+import (
+	"fmt"
+
+	"github.com/weaviate/weaviate/entities/moduletools"
+)
 
 const (
 	MethodMean    = "mean"
@@ -49,8 +53,11 @@ func (c *Config) ReferenceProperties() map[string]struct{} {
 	return refProps
 }
 
-func (c *Config) CalculationMethod() string {
+func (c *Config) CalculationMethod() (string, error) {
 	props := c.class.Class()
-	calcMethod := props[calculationMethodField].(string)
-	return calcMethod
+	calcMethod, ok := props[calculationMethodField].(string)
+	if !ok {
+		return "", fmt.Errorf("could not parse calculation methode. Expected a string, got: %v", props[calculationMethodField])
+	}
+	return calcMethod, nil
 }
diff --git a/modules/ref2vec-centroid/config/validation.go b/modules/ref2vec-centroid/config/validation.go
index b2d4a6916d..1432e0d924 100644
--- a/modules/ref2vec-centroid/config/validation.go
+++ b/modules/ref2vec-centroid/config/validation.go
@@ -46,5 +46,10 @@ func Validate(cfg *Config) error {
 		}
 	}
 
+	_, err := cfg.CalculationMethod()
+	if err != nil {
+		return err
+	}
+
 	return nil
 }
diff --git a/modules/ref2vec-centroid/module.go b/modules/ref2vec-centroid/module.go
index f91321e01f..73291b3301 100644
--- a/modules/ref2vec-centroid/module.go
+++ b/modules/ref2vec-centroid/module.go
@@ -60,7 +60,10 @@ func (m *CentroidModule) VectorizeObject(ctx context.Context,
 	obj *models.Object, cfg moduletools.ClassConfig,
 	findRefVecsFn modulecapabilities.FindObjectFn,
 ) ([]float32, error) {
-	vzr := vectorizer.New(cfg, findRefVecsFn)
+	vzr, err := vectorizer.New(cfg, findRefVecsFn)
+	if err != nil {
+		return nil, err
+	}
 	return vzr.Object(ctx, obj)
 }
 
diff --git a/modules/ref2vec-centroid/vectorizer/vectorizer.go b/modules/ref2vec-centroid/vectorizer/vectorizer.go
index 77e623693c..1fedb3469c 100644
--- a/modules/ref2vec-centroid/vectorizer/vectorizer.go
+++ b/modules/ref2vec-centroid/vectorizer/vectorizer.go
@@ -33,20 +33,24 @@ type Vectorizer struct {
 	findObjectFn modulecapabilities.FindObjectFn
 }
 
-func New(cfg moduletools.ClassConfig, findFn modulecapabilities.FindObjectFn) *Vectorizer {
+func New(cfg moduletools.ClassConfig, findFn modulecapabilities.FindObjectFn) (*Vectorizer, error) {
 	v := &Vectorizer{
 		config:       config.New(cfg),
 		findObjectFn: findFn,
 	}
 
-	switch v.config.CalculationMethod() {
+	method, err := v.config.CalculationMethod()
+	if err != nil {
+		return nil, err
+	}
+	switch method {
 	case config.MethodMean:
 		v.calcFn = calculateMean
 	default:
 		v.calcFn = calculateMean
 	}
 
-	return v
+	return v, nil
 }
 
 func (v *Vectorizer) Object(ctx context.Context, obj *models.Object) ([]float32, error) {
diff --git a/modules/text2vec-aws/clients/aws.go b/modules/text2vec-aws/clients/aws.go
index 643eea511e..e720cfb515 100644
--- a/modules/text2vec-aws/clients/aws.go
+++ b/modules/text2vec-aws/clients/aws.go
@@ -310,7 +310,7 @@ func (v *awsClient) isBedrock(service string) bool {
 }
 
 func (v *awsClient) getAwsAccessKey(ctx context.Context) (string, error) {
-	if awsAccessKey := v.getHeaderValue(ctx, "X-Aws-Access-Key"); awsAccessKey != "" {
+	if awsAccessKey := modulecomponents.GetValueFromContext(ctx, "X-Aws-Access-Key"); awsAccessKey != "" {
 		return awsAccessKey, nil
 	}
 	if v.awsAccessKey != "" {
@@ -322,7 +322,7 @@ func (v *awsClient) getAwsAccessKey(ctx context.Context) (string, error) {
 }
 
 func (v *awsClient) getAwsAccessSecret(ctx context.Context) (string, error) {
-	if awsSecret := v.getHeaderValue(ctx, "X-Aws-Secret-Key"); awsSecret != "" {
+	if awsSecret := modulecomponents.GetValueFromContext(ctx, "X-Aws-Secret-Key"); awsSecret != "" {
 		return awsSecret, nil
 	}
 	if v.awsSecret != "" {
@@ -334,7 +334,7 @@ func (v *awsClient) getAwsAccessSecret(ctx context.Context) (string, error) {
 }
 
 func (v *awsClient) getAwsSessionToken(ctx context.Context) (string, error) {
-	if awsSessionToken := v.getHeaderValue(ctx, "X-Aws-Session-Token"); awsSessionToken != "" {
+	if awsSessionToken := modulecomponents.GetValueFromContext(ctx, "X-Aws-Session-Token"); awsSessionToken != "" {
 		return awsSessionToken, nil
 	}
 	if v.awsSessionToken != "" {
@@ -343,19 +343,6 @@ func (v *awsClient) getAwsSessionToken(ctx context.Context) (string, error) {
 	return "", nil
 }
 
-func (v *awsClient) getHeaderValue(ctx context.Context, header string) string {
-	headerValue := ctx.Value(header)
-	if value, ok := headerValue.([]string); ok &&
-		len(value) > 0 && len(value[0]) > 0 {
-		return value[0]
-	}
-	// try getting header from GRPC if not successful
-	if value := modulecomponents.GetValueFromGRPC(ctx, header); len(value) > 0 && len(value[0]) > 0 {
-		return value[0]
-	}
-	return ""
-}
-
 func (v *awsClient) getRegion(config ent.VectorizationConfig) string {
 	return config.Region
 }
diff --git a/usecases/objects/auto_schema.go b/usecases/objects/auto_schema.go
index 16469ee63c..f98b2063be 100644
--- a/usecases/objects/auto_schema.go
+++ b/usecases/objects/auto_schema.go
@@ -245,6 +245,19 @@ func (m *autoSchemaManager) determineType(value interface{}, ofNestedProp bool)
 				determinedDataType = dataType
 			}
 			if dataType != "" {
+				// if an array contains text and UUID/Date, the type should be text
+				if determinedDataType == schema.DataTypeTextArray && (dataType == schema.DataTypeUUIDArray || dataType == schema.DataTypeDateArray) {
+					continue
+				}
+				if determinedDataType == schema.DataTypeDateArray && (dataType == schema.DataTypeUUIDArray || dataType == schema.DataTypeTextArray) {
+					determinedDataType = schema.DataTypeTextArray
+					continue
+				}
+				if determinedDataType == schema.DataTypeUUIDArray && (dataType == schema.DataTypeDateArray || dataType == schema.DataTypeTextArray) {
+					determinedDataType = schema.DataTypeTextArray
+					continue
+				}
+
 				if isRef {
 					return nil, fmt.Errorf("element [%d]: mismatched data type - reference expected, got '%s'",
 						i, asSingleDataType(dataType))
diff --git a/usecases/objects/batch_add.go b/usecases/objects/batch_add.go
index e02fdeac26..3ca8a2b4bf 100644
--- a/usecases/objects/batch_add.go
+++ b/usecases/objects/batch_add.go
@@ -13,6 +13,7 @@ package objects
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"time"
 
@@ -100,6 +101,11 @@ func (b *BatchManager) validateAndGetVector(ctx context.Context, principal *mode
 	for i, obj := range objects {
 		batchObjects[i].OriginalIndex = i
 
+		if obj.Class == "" {
+			batchObjects[i].Err = errors.New("object has an empty class")
+			continue
+		}
+
 		schemaVersion, err := b.autoSchemaManager.autoSchema(ctx, principal, true, obj)
 		if err != nil {
 			batchObjects[i].Err = err
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/properties_validation.go b/usecases/objects/validation/properties_validation.go
index cefdbbc887..85706a20cf 100644
--- a/usecases/objects/validation/properties_validation.go
+++ b/usecases/objects/validation/properties_validation.go
@@ -197,6 +197,10 @@ func objectVal(ctx context.Context, v *Validator, val interface{}, propertyPrefi
 			return nil, fmt.Errorf("property '%s': %w", propertyName, err)
 		}
 
+		if nestedValue == nil {
+			continue
+		}
+
 		var data interface{}
 		if schema.IsNested(*nestedDataType) {
 			data, err = v.extractAndValidateNestedProperty(ctx, propertyName, nestedValue,
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
+}
diff --git a/usecases/schema/class.go b/usecases/schema/class.go
index ca76a31780..1f242d1f85 100644
--- a/usecases/schema/class.go
+++ b/usecases/schema/class.go
@@ -538,7 +538,7 @@ func (h *Handler) validateProperty(
 		}
 
 		if existingPropertyNames[strings.ToLower(property.Name)] {
-			return fmt.Errorf("class %q: conflict for property %q: already in use or provided multiple times", property.Name, class.Class)
+			return fmt.Errorf("class %q: conflict for property %q: already in use or provided multiple times", class.Class, property.Name)
 		}
 
 		// Validate data type of property.
diff --git a/usecases/traverser/explorer.go b/usecases/traverser/explorer.go
index 830c50307f..7894add79c 100644
--- a/usecases/traverser/explorer.go
+++ b/usecases/traverser/explorer.go
@@ -15,7 +15,6 @@ import (
 	"context"
 	"fmt"
 	"runtime"
-	"strings"
 
 	"github.com/weaviate/weaviate/entities/schema/configvalidation"
 
@@ -515,7 +514,7 @@ func (e *Explorer) searchResultsToGetResponseWithType(ctx context.Context, input
 
 		if len(additionalProperties) > 0 {
 			if additionalProperties["group"] != nil {
-				e.extractAdditionalPropertiesFromGroupRefs(additionalProperties["group"], params.Properties)
+				e.extractAdditionalPropertiesFromGroupRefs(additionalProperties["group"], params.GroupBy.Properties)
 			}
 			res.Schema.(map[string]interface{})["_additional"] = additionalProperties
 		}
@@ -530,23 +529,12 @@ func (e *Explorer) searchResultsToGetResponseWithType(ctx context.Context, input
 
 func (e *Explorer) extractAdditionalPropertiesFromGroupRefs(
 	additionalGroup interface{},
-	params search.SelectProperties,
+	props search.SelectProperties,
 ) {
 	if group, ok := additionalGroup.(*additional.Group); ok {
 		if len(group.Hits) > 0 {
-			var groupSelectProperties search.SelectProperties
-			for _, selectProp := range params {
-				if strings.HasPrefix(selectProp.Name, "_additional:group:hits:") {
-					groupSelectProperties = append(groupSelectProperties, search.SelectProperty{
-						Name:            strings.Replace(selectProp.Name, "_additional:group:hits:", "", 1),
-						IsPrimitive:     selectProp.IsPrimitive,
-						IncludeTypeName: selectProp.IncludeTypeName,
-						Refs:            selectProp.Refs,
-					})
-				}
-			}
 			for _, hit := range group.Hits {
-				e.extractAdditionalPropertiesFromRefs(hit, groupSelectProperties)
+				e.extractAdditionalPropertiesFromRefs(hit, props)
 			}
 		}
 	}


Test patch:
diff --git a/adapters/handlers/graphql/local/get/get_test.go b/adapters/handlers/graphql/local/get/get_test.go
index cbe01f6a5d..515cbe7126 100644
--- a/adapters/handlers/graphql/local/get/get_test.go
+++ b/adapters/handlers/graphql/local/get/get_test.go
@@ -2386,7 +2386,7 @@ func TestGroupBy(t *testing.T) {
 
 				expectedParams := dto.GetParams{
 					ClassName:            "SomeAction",
-					GroupBy:              &searchparams.GroupBy{Property: "path", Groups: 2, ObjectsPerGroup: 3},
+					GroupBy:              &searchparams.GroupBy{Property: "path", Groups: 2, ObjectsPerGroup: 3, Properties: search.SelectProperties{}},
 					AdditionalProperties: additional.Properties{Group: true},
 				}
 
diff --git a/adapters/handlers/grpc/v1/parse_search_request_test.go b/adapters/handlers/grpc/v1/parse_search_request_test.go
index 0527c02b5a..d2bb2e2591 100644
--- a/adapters/handlers/grpc/v1/parse_search_request_test.go
+++ b/adapters/handlers/grpc/v1/parse_search_request_test.go
@@ -584,7 +584,7 @@ func TestGRPCRequest(t *testing.T) {
 			},
 			out: dto.GetParams{
 				ClassName: classname, Pagination: defaultPagination, HybridSearch: &searchparams.HybridSearch{Query: "query", FusionAlgorithm: common_filters.HybridRankedFusion, Alpha: 0.75, Properties: []string{"name", "capitalizedName"}},
-				GroupBy:              &searchparams.GroupBy{Property: "name", Groups: 2, ObjectsPerGroup: 3},
+				GroupBy:              &searchparams.GroupBy{Property: "name", Groups: 2, ObjectsPerGroup: 3, Properties: defaultTestClassProps},
 				Properties:           defaultTestClassProps,
 				AdditionalProperties: additional.Properties{Vector: true, NoProps: false, Group: true},
 			},
@@ -654,7 +654,7 @@ func TestGRPCRequest(t *testing.T) {
 			out: dto.GetParams{
 				ClassName: classname, Pagination: defaultPagination,
 				KeywordRanking:       &searchparams.KeywordRanking{Query: "query", Properties: []string{"name", "capitalizedName"}, Type: "bm25"},
-				GroupBy:              &searchparams.GroupBy{Property: "name", Groups: 2, ObjectsPerGroup: 3},
+				GroupBy:              &searchparams.GroupBy{Property: "name", Groups: 2, ObjectsPerGroup: 3, Properties: defaultTestClassProps},
 				Properties:           defaultTestClassProps,
 				AdditionalProperties: additional.Properties{Vector: true, NoProps: false, Group: true},
 			},
@@ -1359,7 +1359,7 @@ func TestGRPCRequest(t *testing.T) {
 					Group:   true,
 				},
 				NearVector: &searchparams.NearVector{VectorPerTarget: map[string][]float32{"": {1, 2, 3}}},
-				GroupBy:    &searchparams.GroupBy{Groups: 2, ObjectsPerGroup: 3, Property: "name"},
+				GroupBy:    &searchparams.GroupBy{Groups: 2, ObjectsPerGroup: 3, Property: "name", Properties: defaultTestClassProps},
 			},
 			error: false,
 		},
diff --git a/adapters/repos/db/index_queue_test.go b/adapters/repos/db/index_queue_test.go
index d5c478bfd0..bae5c3a5ce 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) compareAndSwapStatus(old, new string) (storagestate.Status, error) {
+func (m *mockShard) compareAndSwapStatusIndexingAndReady(old, new string) (storagestate.Status, error) {
 	if m.compareAndSwapStatusFn == nil {
 		return storagestate.Status(new), nil
 	}
diff --git a/adapters/repos/db/refcache/cacher_test.go b/adapters/repos/db/refcache/cacher_test.go
index 3f6caac170..0b4a7aa6f9 100644
--- a/adapters/repos/db/refcache/cacher_test.go
+++ b/adapters/repos/db/refcache/cacher_test.go
@@ -37,7 +37,7 @@ func TestCacher(t *testing.T) {
 		repo := newFakeRepo()
 		logger, _ := test.NewNullLogger()
 		cr := NewCacher(repo, logger, "")
-		err := cr.Build(context.Background(), nil, nil, additional.Properties{})
+		err := cr.Build(context.Background(), nil, nil, additional.Properties{}, nil)
 		assert.Nil(t, err)
 	})
 
@@ -51,7 +51,7 @@ func TestCacher(t *testing.T) {
 				ClassName: "BestClass",
 			},
 		}
-		err := cr.Build(context.Background(), input, nil, additional.Properties{})
+		err := cr.Build(context.Background(), input, nil, additional.Properties{}, nil)
 		assert.Nil(t, err)
 	})
 
@@ -69,7 +69,7 @@ func TestCacher(t *testing.T) {
 				},
 			},
 		}
-		err := cr.Build(context.Background(), input, nil, additional.Properties{})
+		err := cr.Build(context.Background(), input, nil, additional.Properties{}, nil)
 		assert.Nil(t, err)
 	})
 
@@ -90,7 +90,7 @@ func TestCacher(t *testing.T) {
 				},
 			},
 		}
-		err := cr.Build(context.Background(), input, nil, additional.Properties{})
+		err := cr.Build(context.Background(), input, nil, additional.Properties{}, nil)
 		require.Nil(t, err)
 		_, ok := cr.Get(multi.Identifier{ID: "123", ClassName: "SomeClass"})
 		assert.False(t, ok)
@@ -145,7 +145,7 @@ func TestCacher(t *testing.T) {
 			},
 		}
 
-		err := cr.Build(context.Background(), input, selectProps, additional.Properties{})
+		err := cr.Build(context.Background(), input, selectProps, additional.Properties{}, nil)
 		require.Nil(t, err)
 		res, ok := cr.Get(multi.Identifier{ID: id1, ClassName: "SomeClass"})
 		require.True(t, ok)
@@ -251,7 +251,7 @@ func TestCacher(t *testing.T) {
 			},
 		}
 
-		err := cr.Build(context.Background(), input, selectProps, additional.Properties{})
+		err := cr.Build(context.Background(), input, selectProps, additional.Properties{}, nil)
 		require.Nil(t, err)
 		res, ok := cr.Get(multi.Identifier{ID: id1, ClassName: "SomeClass"})
 		require.True(t, ok)
@@ -378,7 +378,7 @@ func TestCacher(t *testing.T) {
 			},
 		}
 
-		err := cr.Build(context.Background(), input, selectProps, additional.Properties{})
+		err := cr.Build(context.Background(), input, selectProps, additional.Properties{}, nil)
 		require.Nil(t, err)
 		res, ok := cr.Get(multi.Identifier{ID: id1, ClassName: "SomeClass"})
 		require.True(t, ok)
@@ -574,13 +574,13 @@ func TestCacher(t *testing.T) {
 			},
 		}
 
-		err := cr.Build(context.Background(), input, selectProps, additional.Properties{})
+		err := cr.Build(context.Background(), input, selectProps, additional.Properties{}, nil)
 		require.Nil(t, err)
 		res, ok := cr.Get(multi.Identifier{ID: id1, ClassName: "SomeClass"})
 		require.True(t, ok)
 		assert.Equal(t, expectedOuter, res)
 		input2 := []search.Result{expectedInner, expectedInner2}
-		err = cr.Build(context.Background(), input2, nil, additional.Properties{})
+		err = cr.Build(context.Background(), input2, nil, additional.Properties{}, nil)
 		require.Nil(t, err)
 		nested1, ok := cr.Get(multi.Identifier{ID: id2, ClassName: "SomeNestedClass"})
 		require.True(t, ok)
@@ -639,7 +639,7 @@ func TestCacher(t *testing.T) {
 			},
 		}
 		logger, _ := test.NewNullLogger()
-		cr := NewCacherWithGroup(repo, logger, "")
+		cr := NewCacher(repo, logger, "")
 		input := []search.Result{
 			{
 				ID:        "foo",
@@ -672,9 +672,9 @@ func TestCacher(t *testing.T) {
 				},
 			},
 		}
-		selectProps := search.SelectProperties{
+		groupByProps := search.SelectProperties{
 			search.SelectProperty{
-				Name: "_additional:group:hits:nestedRef",
+				Name: "nestedRef",
 				Refs: []search.SelectClass{
 					{
 						ClassName: "SomeNestedClass",
@@ -697,7 +697,7 @@ func TestCacher(t *testing.T) {
 			},
 		}
 
-		err := cr.Build(context.Background(), input, selectProps, additional.Properties{})
+		err := cr.Build(context.Background(), input, nil, additional.Properties{}, groupByProps)
 		require.Nil(t, err)
 		res, ok := cr.Get(multi.Identifier{ID: id2, ClassName: "SomeNestedClass"})
 		require.True(t, ok)
@@ -748,7 +748,7 @@ func TestCacher(t *testing.T) {
 			},
 		}
 		logger, _ := test.NewNullLogger()
-		cr := NewCacherWithGroup(repo, logger, "")
+		cr := NewCacher(repo, logger, "")
 		input := []search.Result{
 			{
 				ID:        "foo",
@@ -781,9 +781,9 @@ func TestCacher(t *testing.T) {
 				},
 			},
 		}
-		selectProps := search.SelectProperties{
+		groupByProps := search.SelectProperties{
 			search.SelectProperty{
-				Name: "_additional:group:hits:nestedRef",
+				Name: "nestedRef",
 				Refs: []search.SelectClass{
 					{
 						ClassName: "SomeNestedClass",
@@ -797,7 +797,7 @@ func TestCacher(t *testing.T) {
 				},
 			},
 			search.SelectProperty{
-				Name: "_additional:group:hits:otherNestedRef",
+				Name: "otherNestedRef",
 				Refs: []search.SelectClass{
 					{
 						ClassName: "OtherNestedClass",
@@ -828,7 +828,7 @@ func TestCacher(t *testing.T) {
 			},
 		}
 
-		err := cr.Build(context.Background(), input, selectProps, additional.Properties{})
+		err := cr.Build(context.Background(), input, nil, additional.Properties{}, groupByProps)
 		require.Nil(t, err)
 		res, ok := cr.Get(multi.Identifier{ID: id2, ClassName: "SomeNestedClass"})
 		require.True(t, ok)
@@ -880,7 +880,7 @@ func TestCacher(t *testing.T) {
 			},
 		}
 		logger, _ := test.NewNullLogger()
-		cr := NewCacherWithGroup(repo, logger, "")
+		cr := NewCacher(repo, logger, "")
 		input := []search.Result{
 			{
 				ID:        "foo",
@@ -942,8 +942,10 @@ func TestCacher(t *testing.T) {
 					},
 				},
 			},
+		}
+		groupByProps := search.SelectProperties{
 			search.SelectProperty{
-				Name: "_additional:group:hits:innerNestedRef",
+				Name: "innerNestedRef",
 				Refs: []search.SelectClass{
 					{
 						ClassName: "InnerNestedClass",
@@ -957,7 +959,7 @@ func TestCacher(t *testing.T) {
 				},
 			},
 			search.SelectProperty{
-				Name: "_additional:group:hits:otherNestedRef",
+				Name: "otherNestedRef",
 				Refs: []search.SelectClass{
 					{
 						ClassName: "OtherNestedClass",
@@ -1014,7 +1016,7 @@ func TestCacher(t *testing.T) {
 			},
 		}
 
-		err := cr.Build(context.Background(), input, selectProps, additional.Properties{})
+		err := cr.Build(context.Background(), input, selectProps, additional.Properties{}, groupByProps)
 		require.Nil(t, err)
 		res, ok := cr.Get(multi.Identifier{ID: id1, ClassName: "SomeClass"})
 		require.True(t, ok)
diff --git a/adapters/repos/db/refcache/resolver_test.go b/adapters/repos/db/refcache/resolver_test.go
index 741d325b87..af1c4a7581 100644
--- a/adapters/repos/db/refcache/resolver_test.go
+++ b/adapters/repos/db/refcache/resolver_test.go
@@ -425,26 +425,7 @@ func TestResolver(t *testing.T) {
 				},
 			}
 		}
-		getResolver := func() *Resolver {
-			cacher := newFakeCacher()
-			r := NewResolverWithGroup(cacher)
-			cacher.lookup[multi.Identifier{ID: id1, ClassName: "SomeClass"}] = search.Result{
-				ClassName: "SomeClass",
-				ID:        strfmt.UUID(id1),
-				Schema: map[string]interface{}{
-					"bar": "some string",
-				},
-				Vector: []float32{0.1, 0.2},
-			}
-			cacher.lookup[multi.Identifier{ID: id2, ClassName: "SomeNestedClass"}] = search.Result{
-				ClassName: "SomeNestedClass",
-				ID:        strfmt.UUID(id2),
-				Schema: map[string]interface{}{
-					"name": "John Doe",
-				},
-			}
-			return r
-		}
+
 		getSelectProps := func(withVector bool) search.SelectProperties {
 			return search.SelectProperties{
 				search.SelectProperty{
@@ -464,8 +445,12 @@ func TestResolver(t *testing.T) {
 						},
 					},
 				},
+			}
+		}
+		getGroupBySelectProps := func() search.SelectProperties {
+			return search.SelectProperties{
 				search.SelectProperty{
-					Name: "_additional:group:hits:nestedRef",
+					Name: "nestedRef",
 					Refs: []search.SelectClass{
 						{
 							ClassName: "SomeNestedClass",
@@ -480,6 +465,26 @@ func TestResolver(t *testing.T) {
 				},
 			}
 		}
+		getResolver := func() *Resolver {
+			cacher := newFakeCacher()
+			r := NewResolverWithGroup(cacher, getGroupBySelectProps())
+			cacher.lookup[multi.Identifier{ID: id1, ClassName: "SomeClass"}] = search.Result{
+				ClassName: "SomeClass",
+				ID:        strfmt.UUID(id1),
+				Schema: map[string]interface{}{
+					"bar": "some string",
+				},
+				Vector: []float32{0.1, 0.2},
+			}
+			cacher.lookup[multi.Identifier{ID: id2, ClassName: "SomeNestedClass"}] = search.Result{
+				ClassName: "SomeNestedClass",
+				ID:        strfmt.UUID(id2),
+				Schema: map[string]interface{}{
+					"name": "John Doe",
+				},
+			}
+			return r
+		}
 		getExpectedResult := func(withVector bool) []search.Result {
 			fields := map[string]interface{}{
 				"bar": "some string",
@@ -540,7 +545,7 @@ type fakeCacher struct {
 }
 
 func (f *fakeCacher) Build(ctx context.Context, objects []search.Result, properties search.SelectProperties,
-	additional additional.Properties,
+	additional additional.Properties, groupByProps search.SelectProperties,
 ) error {
 	return nil
 }
diff --git a/adapters/repos/db/shard_test.go b/adapters/repos/db/shard_test.go
index be97653339..fd5f8cd001 100644
--- a/adapters/repos/db/shard_test.go
+++ b/adapters/repos/db/shard_test.go
@@ -67,11 +67,12 @@ func TestShard_UpdateStatus(t *testing.T) {
 	})
 
 	t.Run("mark shard readonly and fail to insert", func(t *testing.T) {
-		err := shd.UpdateStatus(storagestate.StatusReadOnly.String())
+		err := shd.SetStatusReadonly("testing")
 		require.Nil(t, err)
 
 		err = shd.PutObject(ctx, testObject(className))
-		require.EqualError(t, err, storagestate.ErrStatusReadOnly.Error())
+		require.Contains(t, err.Error(), storagestate.ErrStatusReadOnly.Error())
+		require.Contains(t, err.Error(), "testing")
 	})
 
 	t.Run("mark shard ready and insert successfully", func(t *testing.T) {
diff --git a/modules/ref2vec-centroid/config_test.go b/modules/ref2vec-centroid/config_test.go
index 89019963a0..b7191501ed 100644
--- a/modules/ref2vec-centroid/config_test.go
+++ b/modules/ref2vec-centroid/config_test.go
@@ -26,7 +26,10 @@ func TestConfigDefaults(t *testing.T) {
 	def := New().ClassConfigDefaults()
 	cfg := config.New(fakeClassConfig(def))
 
-	assert.Equal(t, config.MethodDefault, cfg.CalculationMethod())
+	method, err := cfg.CalculationMethod()
+
+	assert.Equal(t, config.MethodDefault, method)
+	assert.Nil(t, err)
 }
 
 func TestConfigValidator(t *testing.T) {
@@ -43,7 +46,19 @@ func TestConfigValidator(t *testing.T) {
 			class: class,
 			classConfig: fakeClassConfig{
 				"referenceProperties": []interface{}{"someRef"},
+				"method":              "mean",
+			},
+		},
+		{
+			name:  "invalid config - wrong method",
+			class: class,
+			classConfig: fakeClassConfig{
+				"referenceProperties": []interface{}{"someRef"},
+				"method":              []string{"mean"},
 			},
+			expectedErr: fmt.Errorf("validate %q: could not parse calculation methode. "+
+				"Expected a string, got: [mean]",
+				class.Class),
 		},
 		{
 			name:        "invalid config - required fields omitted",
diff --git a/modules/ref2vec-centroid/vectorizer/vectorizer_test.go b/modules/ref2vec-centroid/vectorizer/vectorizer_test.go
index 9c5111663e..6b39bf54c6 100644
--- a/modules/ref2vec-centroid/vectorizer/vectorizer_test.go
+++ b/modules/ref2vec-centroid/vectorizer/vectorizer_test.go
@@ -29,7 +29,8 @@ import (
 func TestVectorizer_New(t *testing.T) {
 	repo := &fakeObjectsRepo{}
 	t.Run("default is set correctly", func(t *testing.T) {
-		vzr := New(fakeClassConfig(config.Default()), repo.Object)
+		vzr, err := New(fakeClassConfig(config.Default()), repo.Object)
+		assert.Nil(t, err)
 
 		expected := reflect.ValueOf(calculateMean).Pointer()
 		received := reflect.ValueOf(vzr.calcFn).Pointer()
@@ -39,7 +40,8 @@ func TestVectorizer_New(t *testing.T) {
 
 	t.Run("default calcFn is used when none provided", func(t *testing.T) {
 		cfg := fakeClassConfig{"method": ""}
-		vzr := New(cfg, repo.Object)
+		vzr, err := New(cfg, repo.Object)
+		assert.Nil(t, err)
 
 		expected := reflect.ValueOf(calculateMean).Pointer()
 		received := reflect.ValueOf(vzr.calcFn).Pointer()
@@ -135,8 +137,9 @@ func TestVectorizer_Object(t *testing.T) {
 				obj := &models.Object{
 					Properties: map[string]interface{}{"toRef": modelRefs},
 				}
-
-				vec, err := New(cfg, repo.Object).Object(ctx, obj)
+				vectorizer, err := New(cfg, repo.Object)
+				assert.Nil(t, err)
+				vec, err := vectorizer.Object(ctx, obj)
 				if test.expectedCalcError != nil {
 					assert.EqualError(t, err, test.expectedCalcError.Error())
 				} else {
@@ -162,7 +165,10 @@ func TestVectorizer_Object(t *testing.T) {
 			Properties: map[string]interface{}{"toRef": []interface{}{}},
 		}
 
-		_, err := New(cfg, repo.Object).Object(ctx, obj)
+		vectorizer, err := New(cfg, repo.Object)
+		assert.Nil(t, err)
+
+		_, err = vectorizer.Object(ctx, obj)
 		assert.Nil(t, err)
 		assert.Nil(t, obj.Vector)
 	})
@@ -188,7 +194,10 @@ func TestVectorizer_Tenant(t *testing.T) {
 		Tenant:     tenant,
 	}
 
-	_, err := New(cfg, repo.Object).Object(ctx, obj)
+	vectorizer, err := New(cfg, repo.Object)
+	assert.Nil(t, err)
+
+	_, err = vectorizer.Object(ctx, obj)
 	assert.Nil(t, err)
 	assert.Nil(t, obj.Vector)
 }
diff --git a/test/acceptance/objects/objects_test.go b/test/acceptance/objects/objects_test.go
index c38b4d03fb..acaeb48dab 100644
--- a/test/acceptance/objects/objects_test.go
+++ b/test/acceptance/objects/objects_test.go
@@ -272,11 +272,11 @@ func creatingObjects(t *testing.T) {
 				return &models.Object{
 					Class: "TestObject",
 					Properties: map[string]interface{}{
-						"testReference": map[string]interface{}{
+						"testReference": []interface{}{map[string]interface{}{
 							"beacon": fakeObjectId,
 							"x":      nil,
 							"type":   "Object",
-						},
+						}},
 					},
 				}
 			},
diff --git a/test/acceptance/objects/setup_test.go b/test/acceptance/objects/setup_test.go
index ad1ef8c7df..6f97a8f46a 100644
--- a/test/acceptance/objects/setup_test.go
+++ b/test/acceptance/objects/setup_test.go
@@ -17,6 +17,8 @@ import (
 	"fmt"
 	"testing"
 
+	"github.com/weaviate/weaviate/test/docker"
+
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"github.com/weaviate/weaviate/client/objects"
@@ -24,7 +26,6 @@ import (
 	clschema "github.com/weaviate/weaviate/client/schema"
 	"github.com/weaviate/weaviate/entities/models"
 	"github.com/weaviate/weaviate/entities/schema"
-	"github.com/weaviate/weaviate/test/docker"
 	"github.com/weaviate/weaviate/test/helper"
 )
 
@@ -106,6 +107,10 @@ func testObjects(t *testing.T) {
 				Name:     "testWholeNumber",
 				DataType: []string{"int"},
 			},
+			{
+				Name:     "testReference",
+				DataType: []string{"TestObject"},
+			},
 			{
 				Name:     "testNumber",
 				DataType: []string{"number"},
diff --git a/test/acceptance_with_go_client/multi_tenancy_tests/activation_deactivation_test.go b/test/acceptance_with_go_client/multi_tenancy_tests/activation_deactivation_test.go
index 688e47430f..a3b831c0d3 100644
--- a/test/acceptance_with_go_client/multi_tenancy_tests/activation_deactivation_test.go
+++ b/test/acceptance_with_go_client/multi_tenancy_tests/activation_deactivation_test.go
@@ -646,14 +646,19 @@ func assertActiveTenantObjects(t *testing.T, client *wvt.Client, className, tena
 }
 
 func assertInactiveTenantObjects(t *testing.T, client *wvt.Client, className, tenantName string) {
-	objects, err := client.Data().ObjectsGetter().
-		WithClassName(className).
-		WithTenant(tenantName).
-		Do(context.Background())
-
-	require.NotNil(t, err)
-	clientErr := err.(*fault.WeaviateClientError)
-	assert.Equal(t, 422, clientErr.StatusCode)
-	assert.Contains(t, clientErr.Msg, "tenant not active")
-	require.Nil(t, objects)
+	// Data objects in Weaviate are eventually consistent, therefore we have to add some sleep time
+	// to make sure that the tenant was deactivate in all nodes
+	// see docs: https://github.com/weaviate/weaviate-io/blob/main/developers/weaviate/concepts/replication-architecture/consistency.md#data-objects
+	assert.EventuallyWithT(t, func(collect *assert.CollectT) {
+		objects, err := client.Data().ObjectsGetter().
+			WithClassName(className).
+			WithTenant(tenantName).
+			Do(context.Background())
+
+		assert.NotNil(t, err)
+		clientErr := err.(*fault.WeaviateClientError)
+		assert.Equal(t, 422, clientErr.StatusCode)
+		assert.Contains(t, clientErr.Msg, "tenant not active")
+		assert.Nil(t, objects)
+	}, 5*time.Second, 1*time.Second, "tenant was active, expected to be inactive")
 }
diff --git a/test/acceptance_with_python/test_named_vectors.py b/test/acceptance_with_python/test_named_vectors.py
index c7dfe905be..f46d20b407 100644
--- a/test/acceptance_with_python/test_named_vectors.py
+++ b/test/acceptance_with_python/test_named_vectors.py
@@ -8,6 +8,7 @@ from weaviate.collections.classes.grpc import (
     _MultiTargetVectorJoinEnum,
     HybridVectorType,
 )
+from weaviate.exceptions import UnexpectedStatusCodeError, WeaviateInsertManyAllFailedError
 
 from .conftest import CollectionFactory, NamedCollection
 
@@ -777,3 +778,50 @@ def test_hybrid_combinations(
     assert res.objects[1].metadata.score == 0.5
     assert res.objects[2].uuid == uuid1
     assert res.objects[2].metadata.score == 0.0
+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/auto_schema_test.go b/usecases/objects/auto_schema_test.go
index 17566f1640..dadcc11579 100644
--- a/usecases/objects/auto_schema_test.go
+++ b/usecases/objects/auto_schema_test.go
@@ -401,6 +401,51 @@ func Test_autoSchemaManager_determineType(t *testing.T) {
 			},
 			want: []schema.DataType{schema.DataTypeUUIDArray},
 		},
+		{
+			name: "determine mixed string arrays, string first",
+			fields: fields{
+				config: config.AutoSchema{
+					Enabled: true,
+				},
+			},
+			args: args{
+				value: []interface{}{
+					"string",
+					"57a8564d089b4cd9be3956681605e0da",
+				},
+			},
+			want: []schema.DataType{schema.DataTypeTextArray},
+		},
+		{
+			name: "determine mixed string/UUID arrays, string later",
+			fields: fields{
+				config: config.AutoSchema{
+					Enabled: true,
+				},
+			},
+			args: args{
+				value: []interface{}{
+					"57a8564d089b4cd9be3956681605e0da",
+					"string",
+				},
+			},
+			want: []schema.DataType{schema.DataTypeTextArray},
+		},
+		{
+			name: "determine mixed string/date arrays, string later",
+			fields: fields{
+				config: config.AutoSchema{
+					Enabled: true,
+				},
+			},
+			args: args{
+				value: []interface{}{
+					"2002-10-02T15:00:00Z",
+					"string",
+				},
+			},
+			want: []schema.DataType{schema.DataTypeTextArray},
+		},
 		{
 			name: "[deprecated string] determine string",
 			fields: fields{
diff --git a/usecases/objects/batch_add_test.go b/usecases/objects/batch_add_test.go
index 535299122a..401d21c08c 100644
--- a/usecases/objects/batch_add_test.go
+++ b/usecases/objects/batch_add_test.go
@@ -125,6 +125,36 @@ func Test_BatchManager_AddObjects_WithNoVectorizerModule(t *testing.T) {
 			"the correct vector was used")
 	})
 
+	t.Run("object without class", func(t *testing.T) {
+		reset()
+		vectorRepo.On("BatchPutObjects", mock.Anything).Return(nil).Once()
+		objects := []*models.Object{
+			{
+				Class:  "",
+				Vector: []float32{0.1, 0.1, 0.1111},
+			},
+			{
+				Class:  "Foo",
+				Vector: []float32{0.2, 0.2, 0.2222},
+			},
+		}
+
+		for range objects {
+			modulesProvider.On("BatchUpdateVector").
+				Return(nil, nil)
+		}
+
+		resp, err := manager.AddObjects(ctx, nil, objects, []*string{}, nil)
+		repoCalledWithObjects := vectorRepo.Calls[0].Arguments[0].(BatchObjects)
+		assert.Nil(t, err)
+		assert.NotNil(t, resp)
+		require.Len(t, repoCalledWithObjects, 2)
+
+		require.NotNil(t, resp[0].Err)
+		require.Equal(t, resp[0].Err.Error(), "object has an empty class")
+		require.Nil(t, resp[1].Err)
+	})
+
 	t.Run("with objects without IDs and nonexistent class and auto schema enabled", func(t *testing.T) {
 		resetAutoSchema(true)
 		vectorRepo.On("BatchPutObjects", mock.Anything).Return(nil).Once()
diff --git a/usecases/objects/validation/properties_validation_test.go b/usecases/objects/validation/properties_validation_test.go
index 280cdc0fd3..2b4c90373f 100644
--- a/usecases/objects/validation/properties_validation_test.go
+++ b/usecases/objects/validation/properties_validation_test.go
@@ -157,7 +157,14 @@ func getDataType(dataType schema.DataType) *schema.DataType {
 func TestProperties(t *testing.T) {
 	const myBeacon = "weaviate://localhost/things/8e555f0d-8590-48c2-a9a6-70772ed14c0a"
 	myJournalClass := &models.Class{
-		Properties: []*models.Property{{Name: "inJournal", DataType: []string{"Journal"}}},
+		Properties: []*models.Property{
+			{Name: "inJournal", DataType: []string{"Journal"}},
+			{
+				Name:             "nested",
+				DataType:         schema.DataTypeObject.PropString(),
+				NestedProperties: []*models.NestedProperty{{Name: "nested2", DataType: schema.DataTypeObject.PropString()}},
+			},
+		},
 	}
 	specs := map[string]struct {
 		class     *models.Class
@@ -192,6 +199,14 @@ func TestProperties(t *testing.T) {
 			},
 			expBeacon: "weaviate://localhost/Journal/8e555f0d-8590-48c2-a9a6-70772ed14c0a",
 		},
+		"nested with nil": {
+			class: myJournalClass,
+			obj: &models.Object{
+				Properties: map[string]interface{}{"nested": map[string]interface{}{
+					"nested2": nil,
+				}},
+			},
+		},
 	}
 	for name, spec := range specs {
 		t.Run(name, func(t *testing.T) {
@@ -205,8 +220,11 @@ func TestProperties(t *testing.T) {
 			}
 			require.NoError(t, gotErr)
 			props := spec.obj.Properties
-			gotBeacon := extractBeacon(t, props)
-			assert.Equal(t, spec.expBeacon, gotBeacon)
+			if spec.expBeacon != "" {
+				gotBeacon := extractBeacon(t, props)
+				assert.Equal(t, spec.expBeacon, gotBeacon)
+
+			}
 		})
 	}
 }
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)
+		})
+	}
+}