Repository: weaviate/weaviate
Pull request #7711 (2025-04-02 07:25:19)

feat(named-vectors): forward 'default' vector to legacy vector in case of mixed vector
### What's being changed:
It was not possible to send your own vectors for a mixed vectors collection using a Python client because the client allowed you to specify either legacy or named vectors:

```python
collection.data.insert(properties={"text": "hello"}, vector=[1.0, 2.0, 3.0])
# either
collection.data.insert(
    properties={"text": "hello"}, 
    vector={"vec1": [1.0, 2.0, 3.0], "vec2": [4.0, 5.0, 6.0]
})
```

This PR introduces a QoL thing so that the `weaviate` server will recognize that there are mixed vectors configured and will intercept and forward the `default` vector to the legacy vector.

```python
collection.data.insert(properties={
    "text": "hello"
}, vector={
   "default": [1.0, 2.0, 3.0], # => inserted into the legacy vector on disk
   "vec1": [4.0, 5.0, 6.0], # => will be treated as regular named vector
   "vec2": [4.0, 5.0, 6.0] # => will be treated as regular named vector
})
```

### 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=7711) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [1 New issue](https://sonarcloud.io/project/issues?id=weaviate_weaviate&pullRequest=7711&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=7711&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=7711&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=7711&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=7711&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=weaviate_weaviate&pullRequest=7711)


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=7711) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [1 New issue](https://sonarcloud.io/project/issues?id=weaviate_weaviate&pullRequest=7711&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=7711&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=7711&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=7711&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=7711&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=weaviate_weaviate&pullRequest=7711)


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=7711) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [1 New issue](https://sonarcloud.io/project/issues?id=weaviate_weaviate&pullRequest=7711&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=7711&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=7711&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=7711&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=7711&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=weaviate_weaviate&pullRequest=7711)



Patch:
diff --git a/usecases/objects/validation/vector_validation.go b/usecases/objects/validation/vector_validation.go
index 2943442913..2a681227d6 100644
--- a/usecases/objects/validation/vector_validation.go
+++ b/usecases/objects/validation/vector_validation.go
@@ -16,12 +16,24 @@ import (
 	"fmt"
 
 	"github.com/weaviate/weaviate/entities/models"
+	"github.com/weaviate/weaviate/entities/schema"
 	schemachecks "github.com/weaviate/weaviate/entities/schema/checks"
 )
 
 func (v *Validator) vector(ctx context.Context, class *models.Class,
 	incomingObject *models.Object,
 ) error {
+	// In case the schema has a legacy vector index, we need to check if there is a default named vector which
+	// is used to transport legacy vector via the named vector API.
+	if defaultVec, ok := incomingObject.Vectors[schema.DefaultNamedVectorName]; ok && schemachecks.HasLegacyVectorIndex(class) {
+		vec, ok := defaultVec.([]float32)
+		if !ok {
+			return fmt.Errorf("vector %s has to be a float32 array", schema.DefaultNamedVectorName)
+		}
+		incomingObject.Vector = vec
+		delete(incomingObject.Vectors, schema.DefaultNamedVectorName)
+	}
+
 	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 {


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 7e246a59be..eee843e564 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
@@ -23,5 +23,6 @@ func AllMixedVectorsTests(endpoint string) func(t *testing.T) {
 		t.Run("batch byov", testMixedVectorsBatchBYOV(endpoint))
 		t.Run("hybrid", testMixedVectorsHybrid(endpoint))
 		t.Run("aggregate", testMixedVectorsAggregate(endpoint))
+		t.Run("name forwarding", testMixedVectorsNamedForwarding(endpoint))
 	}
 }
diff --git a/test/acceptance_with_go_client/named_vectors_tests/test_suits/mixed_vectors_name_forwarding.go b/test/acceptance_with_go_client/named_vectors_tests/test_suits/mixed_vectors_name_forwarding.go
new file mode 100644
index 0000000000..bb2b14e308
--- /dev/null
+++ b/test/acceptance_with_go_client/named_vectors_tests/test_suits/mixed_vectors_name_forwarding.go
@@ -0,0 +1,95 @@
+//                           _       _
+// __      _____  __ ___   ___  __ _| |_ ___
+// \ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \
+//  \ V  V /  __/ (_| |\ V /| | (_| | ||  __/
+//   \_/\_/ \___|\__,_| \_/ |_|\__,_|\__\___|
+//
+//  Copyright © 2016 - 2024 Weaviate B.V. All rights reserved.
+//
+//  CONTACT: hello@weaviate.io
+//
+
+package test_suits
+
+import (
+	"context"
+	"testing"
+
+	"github.com/davecgh/go-spew/spew"
+	"github.com/stretchr/testify/require"
+	wvt "github.com/weaviate/weaviate-go-client/v5/weaviate"
+	"github.com/weaviate/weaviate/entities/models"
+)
+
+func testMixedVectorsNamedForwarding(endpoint string) func(t *testing.T) {
+	return func(t *testing.T) {
+		ctx := context.Background()
+
+		client, err := wvt.NewClient(wvt.Config{Scheme: "http", Host: endpoint})
+		require.NoError(t, err)
+
+		class := createMixedVectorsSchema(t, client)
+
+		verifyObject := func(t *testing.T, id string, expectVector []float32) {
+			objWrap, err := client.Data().ObjectsGetter().
+				WithClassName(class.Class).
+				WithID(id).
+				WithVector().
+				Do(ctx)
+			require.NoError(t, err)
+
+			require.Len(t, objWrap, 1)
+			obj := objWrap[0]
+			require.Equal(t, []float32(obj.Vector), expectVector)
+			require.Len(t, obj.Vectors, 3)
+		}
+
+		t.Run("simple", func(t *testing.T) {
+			_, err = client.Data().Creator().
+				WithClassName(class.Class).
+				WithID(UUID1).
+				WithProperties(map[string]any{
+					"text": "Lorem ipsum dolor sit amet",
+				}).
+				WithVectors(map[string]models.Vector{
+					"default": []float32{1, 2, 3},
+				}).
+				Do(ctx)
+			require.NoError(t, err)
+
+			verifyObject(t, UUID1, []float32{1, 2, 3})
+
+			err = client.Data().Updater().
+				WithClassName(class.Class).
+				WithID(UUID1).
+				WithVectors(map[string]models.Vector{
+					"default": []float32{4, 5, 6},
+				}).
+				Do(ctx)
+			require.NoError(t, err)
+
+			verifyObject(t, UUID1, []float32{4, 5, 6})
+		})
+
+		t.Run("batch", func(t *testing.T) {
+			resp, err := client.Batch().ObjectsBatcher().
+				WithObjects(&models.Object{
+					Class: class.Class,
+					ID:    UUID2,
+					Properties: map[string]any{
+						"text": "Lorem ipsum dolor sit amet",
+					},
+					Vectors: map[string]models.Vector{
+						"default": []float32{1, 2, 3},
+					},
+				}).
+				Do(ctx)
+			require.NoError(t, err)
+			for _, r := range resp {
+				require.Empty(t, r.Result.Errors, spew.Sdump(r.Result))
+			}
+
+			verifyObject(t, UUID2, []float32{1, 2, 3})
+		})
+	}
+}
diff --git a/usecases/objects/validation/vector_validation_test.go b/usecases/objects/validation/vector_validation_test.go
index 281d522d6b..d6d4f2df37 100644
--- a/usecases/objects/validation/vector_validation_test.go
+++ b/usecases/objects/validation/vector_validation_test.go
@@ -19,6 +19,7 @@ import (
 	"github.com/stretchr/testify/require"
 	"github.com/weaviate/weaviate/entities/additional"
 	"github.com/weaviate/weaviate/entities/models"
+	"github.com/weaviate/weaviate/entities/schema"
 )
 
 func TestVectors(t *testing.T) {
@@ -84,6 +85,31 @@ func TestVectors(t *testing.T) {
 			},
 			expErr: false,
 		},
+		"default vector set to legacy in mixed vector class": {
+			class: &models.Class{
+				Vectorizer:      "legacy",
+				VectorIndexType: "hnsw",
+				VectorConfig:    map[string]models.VectorConfig{"first": {}, "second": {}},
+			},
+			obj: &models.Object{
+				Vectors: models.Vectors{"first": []float32{1, 2, 3}, "second": []float32{4, 5, 6}, schema.DefaultNamedVectorName: []float32{7, 8, 9}},
+			},
+			objNew: &models.Object{
+				Vectors: models.Vectors{"first": []float32{1, 2, 3}, "second": []float32{4, 5, 6}},
+				Vector:  []float32{7, 8, 9},
+			},
+		},
+		"default vector not touched in named vector class": {
+			class: &models.Class{
+				VectorConfig: map[string]models.VectorConfig{schema.DefaultNamedVectorName: {}},
+			},
+			obj: &models.Object{
+				Vectors: models.Vectors{schema.DefaultNamedVectorName: []float32{1, 2, 3}},
+			},
+			objNew: &models.Object{
+				Vectors: models.Vectors{schema.DefaultNamedVectorName: []float32{1, 2, 3}},
+			},
+		},
 	}
 	for name, spec := range specs {
 		t.Run(name, func(t *testing.T) {