Repository: weaviate/weaviate
Pull request #7575 (2025-03-19 12:45:47)

fix: add BYOV support for mixed vectors
### What's being changed:
1. Add tests for BYOV with mixed vectors.
2. Fix any found issues. 

### 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)

-->


sonarqubecloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=weaviate_weaviate&pullRequest=7575) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=weaviate_weaviate&pullRequest=7575&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=weaviate_weaviate&pullRequest=7575&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=weaviate_weaviate&pullRequest=7575&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=weaviate_weaviate&pullRequest=7575&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=weaviate_weaviate&pullRequest=7575&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=weaviate_weaviate&pullRequest=7575)


sonarqubecloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=weaviate_weaviate&pullRequest=7575) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=weaviate_weaviate&pullRequest=7575&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=weaviate_weaviate&pullRequest=7575&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=weaviate_weaviate&pullRequest=7575&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=weaviate_weaviate&pullRequest=7575&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=weaviate_weaviate&pullRequest=7575&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=weaviate_weaviate&pullRequest=7575)


sonarqubecloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=weaviate_weaviate&pullRequest=7575) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=weaviate_weaviate&pullRequest=7575&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=weaviate_weaviate&pullRequest=7575&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=weaviate_weaviate&pullRequest=7575&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=weaviate_weaviate&pullRequest=7575&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=weaviate_weaviate&pullRequest=7575&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=weaviate_weaviate&pullRequest=7575)


sonarqubecloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=weaviate_weaviate&pullRequest=7575) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=weaviate_weaviate&pullRequest=7575&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=weaviate_weaviate&pullRequest=7575&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=weaviate_weaviate&pullRequest=7575&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=weaviate_weaviate&pullRequest=7575&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=weaviate_weaviate&pullRequest=7575&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=weaviate_weaviate&pullRequest=7575)


sonarqubecloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=weaviate_weaviate&pullRequest=7575) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=weaviate_weaviate&pullRequest=7575&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=weaviate_weaviate&pullRequest=7575&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=weaviate_weaviate&pullRequest=7575&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=weaviate_weaviate&pullRequest=7575&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=weaviate_weaviate&pullRequest=7575&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=weaviate_weaviate&pullRequest=7575)



Patch:
diff --git a/usecases/objects/validation/vector_validation.go b/usecases/objects/validation/vector_validation.go
index 890a2d2cb3..abe5fb92c2 100644
--- a/usecases/objects/validation/vector_validation.go
+++ b/usecases/objects/validation/vector_validation.go
@@ -16,32 +16,38 @@ import (
 	"fmt"
 
 	"github.com/weaviate/weaviate/entities/models"
+	schemachecks "github.com/weaviate/weaviate/entities/schema/checks"
 )
 
 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 !schemachecks.HasLegacyVectorIndex(class) && len(incomingObject.Vector) > 0 {
+		// if there is only one named vector we can assume that the single vector
+		if len(class.VectorConfig) == 1 {
+			namedVectorName := ""
+			for key := range class.VectorConfig {
+				namedVectorName = key
+			}
+			var vector []float32
+			if len(incomingObject.Vector) > 0 {
+				vector = make([]float32, len(incomingObject.Vector))
+				copy(vector, incomingObject.Vector)
+			}
+			incomingObject.Vectors = map[string]models.Vector{namedVectorName: models.Vector(vector)}
+			incomingObject.Vector = nil
+			return nil
+		}
 
-	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)
+		return fmt.Errorf("collection %v configuration does not have single vector index", class.Class)
 	}
 
-	// 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
-		}
-		var vector []float32
-		if len(incomingObject.Vector) > 0 {
-			vector = make([]float32, len(incomingObject.Vector))
-			copy(vector, incomingObject.Vector)
+	if len(class.VectorConfig) == 0 && len(incomingObject.Vectors) > 0 {
+		var targetVectors []string
+		for name := range incomingObject.Vectors {
+			targetVectors = append(targetVectors, name)
 		}
-		incomingObject.Vectors = map[string]models.Vector{namedVectorName: models.Vector(vector)}
-		incomingObject.Vector = nil
+		return fmt.Errorf("collection %v is configured without multiple named vectors, but received named vectors: %v", class.Class, targetVectors)
 	}
 
 	return nil


Test patch:
diff --git a/test/acceptance_with_go_client/named_vectors_tests/test_suits/mixed_vectors.go b/test/acceptance_with_go_client/named_vectors_tests/test_suits/mixed_vectors.go
index 06dcd80193..72c3c1bed8 100644
--- a/test/acceptance_with_go_client/named_vectors_tests/test_suits/mixed_vectors.go
+++ b/test/acceptance_with_go_client/named_vectors_tests/test_suits/mixed_vectors.go
@@ -19,6 +19,7 @@ func AllMixedVectorsTests(endpoint string) func(t *testing.T) {
 	return func(t *testing.T) {
 		t.Run("schema", testMixedVectorsCreateSchema(endpoint))
 		t.Run("object", testMixedVectorsObject(endpoint))
+		t.Run("batch byov", testMixedVectorsBatchBYOV(endpoint))
 		t.Run("hybrid", testMixedVectorsHybrid(endpoint))
 		t.Run("aggregate", testMixedVectorsAggregate(endpoint))
 	}
diff --git a/test/acceptance_with_go_client/named_vectors_tests/test_suits/mixed_vectors_objects.go b/test/acceptance_with_go_client/named_vectors_tests/test_suits/mixed_vectors_objects.go
index 618e2220d5..26248bf5d8 100644
--- a/test/acceptance_with_go_client/named_vectors_tests/test_suits/mixed_vectors_objects.go
+++ b/test/acceptance_with_go_client/named_vectors_tests/test_suits/mixed_vectors_objects.go
@@ -18,6 +18,8 @@ import (
 
 	acceptance_with_go_client "acceptance_tests_with_client"
 
+	"github.com/davecgh/go-spew/spew"
+	"github.com/go-openapi/strfmt"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	wvt "github.com/weaviate/weaviate-go-client/v5/weaviate"
@@ -190,6 +192,58 @@ func testMixedVectorsObject(host string) func(t *testing.T) {
 	}
 }
 
+func testMixedVectorsBatchBYOV(host string) func(t *testing.T) {
+	return func(t *testing.T) {
+		ctx := context.Background()
+
+		client, err := wvt.NewClient(wvt.Config{Scheme: "http", Host: host})
+		require.NoError(t, err)
+
+		require.NoError(t, client.Schema().AllDeleter().Do(ctx))
+		class := createMixedVectorsSchema(t, client)
+
+		var objects []*models.Object
+		for i, data := range []struct {
+			id     strfmt.UUID
+			text   string
+			vector []float32
+		}{
+			{id: UUID1, text: "I like reading books", vector: []float32{1, 2, 3, 4}},
+			{id: UUID2, text: "I like programming", vector: []float32{5, 6, 7, 8}},
+		} {
+			objects = append(objects, &models.Object{
+				Class: class.Class,
+				ID:    data.id,
+				Properties: map[string]interface{}{
+					"text":   data.text,
+					"number": i,
+				},
+				Vector: data.vector,
+				Vectors: models.Vectors{
+					contextionary: data.vector,
+				},
+			})
+		}
+
+		resp, err := client.Batch().
+			ObjectsBatcher().
+			WithObjects(objects...).
+			Do(ctx)
+		require.NoError(t, err)
+		for _, r := range resp {
+			require.Nil(t, r.Result.Errors, spew.Sdump(r.Result.Errors))
+		}
+
+		for _, obj := range objects {
+			vectors := getVectors(t, client, class.Class, obj.ID.String(), "", contextionary, transformers)
+			require.Len(t, vectors, 3)
+			require.Equal(t, []float32(obj.Vector), vectors[""].([]float32))
+			require.Equal(t, []float32(obj.Vector), vectors[contextionary].([]float32))
+			require.Len(t, vectors[transformers], 384)
+		}
+	}
+}
+
 func createMixedVectorsSchema(t *testing.T, client *wvt.Client) *models.Class {
 	contextionaryConfig := map[string]interface{}{
 		text2vecContextionary: map[string]interface{}{
diff --git a/usecases/objects/validation/vector_validation_test.go b/usecases/objects/validation/vector_validation_test.go
index c9db6932f3..80f07affed 100644
--- a/usecases/objects/validation/vector_validation_test.go
+++ b/usecases/objects/validation/vector_validation_test.go
@@ -63,6 +63,18 @@ func TestVectors(t *testing.T) {
 			},
 			expErr: true,
 		},
+		"mixed vectors": {
+			class: &models.Class{
+				Vectorizer:      "legacy",
+				VectorIndexType: "hnsw",
+				VectorConfig:    map[string]models.VectorConfig{"first": {}, "second": {}},
+			},
+			obj: &models.Object{
+				Vector:  []float32{1, 2, 3},
+				Vectors: models.Vectors{"first": []float32{1, 2, 3}, "second": []float32{4, 5, 6}},
+			},
+			expErr: false,
+		},
 	}
 	for name, spec := range specs {
 		t.Run(name, func(t *testing.T) {