Repository: weaviate/weaviate (Go)

Issue:
GraphQL additional properties show null value if more than 1 prop is queried.
If more than 1 additional property in GraphQL is queried, than all but 1 value will be `null`. For example, if the `id` is one of the requested values, then this will be given correct and all other values will return `null`. 

```graphql
{
  Get {
    SocialMediaPost (
      limit: 1,
      nearText: {
        concepts: ["relationships"]
      }
    ) {
      _additional {
        id
        certainty
        interpretation {
          source {
            concept
            occurrence
            weight
          }
        }
      }
    }
  }
}

```

with the unexpected result:

```json
{
  "data": {
    "Get": {
      "SocialMediaPost": [
        {
          "_additional": {
            "certainty": null,
            "id": "4df7de7e-67f9-538a-ab49-b0fd7c4cf17a",
            "interpretation": null
          }
        }
      ]
    }
  },
  "errors": null
}
```


Discussion:

Patch:
diff --git a/deprecations/data.go b/deprecations/data.go
index 8ca508f5ee9..343b627f533 100644
--- a/deprecations/data.go
+++ b/deprecations/data.go
@@ -11,6 +11,7 @@
 
 // Code generated by go generate; DO NOT EDIT.
 // This file was generated by go generate ./deprecations at 2021-01-15
+
 package deprecations
 
 import (
diff --git a/usecases/traverser/explorer.go b/usecases/traverser/explorer.go
index 2e7442d733e..29bef125736 100644
--- a/usecases/traverser/explorer.go
+++ b/usecases/traverser/explorer.go
@@ -183,30 +183,27 @@ func (e *Explorer) searchResultsToGetResponse(ctx context.Context,
 	output := make([]interface{}, 0, len(input))
 
 	for _, res := range input {
+		additionalProperties := make(map[string]interface{})
+
 		if res.AdditionalProperties != nil {
 			if res.AdditionalProperties.Classification != nil {
-				classification := map[string]interface{}{"classification": res.AdditionalProperties.Classification}
-				res.Schema.(map[string]interface{})["_additional"] = classification
+				additionalProperties["classification"] = res.AdditionalProperties.Classification
 			}
 
 			if res.AdditionalProperties.Interpretation != nil {
-				interpretation := map[string]interface{}{"interpretation": res.AdditionalProperties.Interpretation}
-				res.Schema.(map[string]interface{})["_additional"] = interpretation
+				additionalProperties["interpretation"] = res.AdditionalProperties.Interpretation
 			}
 
 			if res.AdditionalProperties.NearestNeighbors != nil {
-				nearestNeighbors := map[string]interface{}{"nearestNeighbors": res.AdditionalProperties.NearestNeighbors}
-				res.Schema.(map[string]interface{})["_additional"] = nearestNeighbors
+				additionalProperties["nearestNeighbors"] = res.AdditionalProperties.NearestNeighbors
 			}
 
 			if res.AdditionalProperties.FeatureProjection != nil {
-				featureProjection := map[string]interface{}{"featureProjection": res.AdditionalProperties.FeatureProjection}
-				res.Schema.(map[string]interface{})["_additional"] = featureProjection
+				additionalProperties["featureProjection"] = res.AdditionalProperties.FeatureProjection
 			}
 
 			if res.AdditionalProperties.SemanticPath != nil {
-				semanticPath := map[string]interface{}{"semanticPath": res.AdditionalProperties.SemanticPath}
-				res.Schema.(map[string]interface{})["_additional"] = semanticPath
+				additionalProperties["semanticPath"] = res.AdditionalProperties.SemanticPath
 			}
 		}
 
@@ -222,14 +219,16 @@ func (e *Explorer) searchResultsToGetResponse(ctx context.Context,
 			}
 
 			if params.AdditionalProperties.Certainty {
-				certainty := map[string]interface{}{"certainty": 1 - dist}
-				res.Schema.(map[string]interface{})["_additional"] = certainty
+				additionalProperties["certainty"] = 1 - dist
 			}
 		}
 
 		if params.AdditionalProperties.ID {
-			id := map[string]interface{}{"id": res.ID}
-			res.Schema.(map[string]interface{})["_additional"] = id
+			additionalProperties["id"] = res.ID
+		}
+
+		if len(additionalProperties) > 0 {
+			res.Schema.(map[string]interface{})["_additional"] = additionalProperties
 		}
 
 		e.extractAdditionalPropertiesFromRefs(res.Schema, params.Properties)
@@ -281,8 +280,9 @@ func (e *Explorer) exctractAdditionalPropertiesFromRef(ref interface{}, refClass
 			}
 			if innerRef.Class == refClass.ClassName {
 				if refClass.AdditionalProperties.ID {
-					innerRefID := map[string]interface{}{"id": innerRef.Fields["id"]}
-					innerRef.Fields["_additional"] = innerRefID
+					additionalProperties := make(map[string]interface{})
+					additionalProperties["id"] = innerRef.Fields["id"]
+					innerRef.Fields["_additional"] = additionalProperties
 				}
 			}
 		}


Test patch:
diff --git a/usecases/traverser/explorer_test.go b/usecases/traverser/explorer_test.go
index 813730aac51..6bea89d95d2 100644
--- a/usecases/traverser/explorer_test.go
+++ b/usecases/traverser/explorer_test.go
@@ -15,6 +15,7 @@ import (
 	"context"
 	"testing"
 
+	"github.com/go-openapi/strfmt"
 	"github.com/semi-technologies/weaviate/entities/filters"
 	"github.com/semi-technologies/weaviate/entities/models"
 	"github.com/semi-technologies/weaviate/entities/schema/kind"
@@ -1457,6 +1458,203 @@ func Test_Explorer_GetClass(t *testing.T) {
 				}, res[1])
 		})
 	})
+
+	t.Run("when the almost all _additional props set", func(t *testing.T) {
+		params := GetParams{
+			Kind:       kind.Object,
+			ClassName:  "BestClass",
+			Pagination: &filters.Pagination{Limit: 100},
+			Filters:    nil,
+			AdditionalProperties: AdditionalProperties{
+				ID:               true,
+				NearestNeighbors: true,
+				Classification:   true,
+				Interpretation:   true,
+			},
+		}
+
+		searchResults := []search.Result{
+			{
+				Kind: kind.Object,
+				ID:   "id1",
+				Schema: map[string]interface{}{
+					"name": "Foo",
+				},
+				AdditionalProperties: &models.AdditionalProperties{
+					Classification: &models.AdditionalPropertiesClassification{
+						ID: "1234",
+					},
+					NearestNeighbors: &models.NearestNeighbors{
+						Neighbors: []*models.NearestNeighbor{
+							&models.NearestNeighbor{
+								Concept:  "foo",
+								Distance: 0.1,
+							},
+						},
+					},
+				},
+			},
+			{
+				Kind: kind.Object,
+				ID:   "id2",
+				Schema: map[string]interface{}{
+					"name": "Bar",
+				},
+				AdditionalProperties: &models.AdditionalProperties{
+					Classification: &models.AdditionalPropertiesClassification{
+						ID: "5678",
+					},
+					NearestNeighbors: &models.NearestNeighbors{
+						Neighbors: []*models.NearestNeighbor{
+							&models.NearestNeighbor{
+								Concept:  "bar",
+								Distance: 0.1,
+							},
+						},
+					},
+				},
+			},
+		}
+
+		searcher := &fakeVectorSearcher{}
+		vectorizer := &fakeVectorizer{}
+		log, _ := test.NewNullLogger()
+		extender := &fakeExtender{
+			returnArgs: []search.Result{
+				{
+					Kind: kind.Object,
+					ID:   "id1",
+					Schema: map[string]interface{}{
+						"name": "Foo",
+					},
+					AdditionalProperties: &models.AdditionalProperties{
+						Classification: &models.AdditionalPropertiesClassification{
+							ID: "1234",
+						},
+						NearestNeighbors: &models.NearestNeighbors{
+							Neighbors: []*models.NearestNeighbor{
+								&models.NearestNeighbor{
+									Concept:  "foo",
+									Distance: 0.1,
+								},
+							},
+						},
+						Interpretation: &models.Interpretation{
+							Source: []*models.InterpretationSource{
+								&models.InterpretationSource{
+									Concept:    "foo",
+									Weight:     0.123,
+									Occurrence: 123,
+								},
+							},
+						},
+					},
+				},
+				{
+					Kind: kind.Object,
+					ID:   "id2",
+					Schema: map[string]interface{}{
+						"name": "Bar",
+					},
+					AdditionalProperties: &models.AdditionalProperties{
+						Classification: &models.AdditionalPropertiesClassification{
+							ID: "5678",
+						},
+						NearestNeighbors: &models.NearestNeighbors{
+							Neighbors: []*models.NearestNeighbor{
+								&models.NearestNeighbor{
+									Concept:  "bar",
+									Distance: 0.1,
+								},
+							},
+						},
+						Interpretation: &models.Interpretation{
+							Source: []*models.InterpretationSource{
+								&models.InterpretationSource{
+									Concept:    "bar",
+									Weight:     0.456,
+									Occurrence: 456,
+								},
+							},
+						},
+					},
+				},
+			},
+		}
+		projector := &fakeProjector{}
+		pathBuilder := &fakePathBuilder{}
+		explorer := NewExplorer(searcher, vectorizer, newFakeDistancer(), log, extender, projector, pathBuilder)
+		expectedParamsToSearch := params
+		expectedParamsToSearch.SearchVector = nil
+		searcher.
+			On("ClassSearch", expectedParamsToSearch).
+			Return(searchResults, nil)
+
+		res, err := explorer.GetClass(context.Background(), params)
+
+		t.Run("class search must be called with right params", func(t *testing.T) {
+			assert.Nil(t, err)
+			searcher.AssertExpectations(t)
+		})
+
+		t.Run("response must contain concepts", func(t *testing.T) {
+			require.Len(t, res, 2)
+			assert.Equal(t,
+				map[string]interface{}{
+					"name": "Foo",
+					"_additional": map[string]interface{}{
+						"id": strfmt.UUID("id1"),
+						"classification": &models.AdditionalPropertiesClassification{
+							ID: "1234",
+						},
+						"nearestNeighbors": &models.NearestNeighbors{
+							Neighbors: []*models.NearestNeighbor{
+								&models.NearestNeighbor{
+									Concept:  "foo",
+									Distance: 0.1,
+								},
+							},
+						},
+						"interpretation": &models.Interpretation{
+							Source: []*models.InterpretationSource{
+								&models.InterpretationSource{
+									Concept:    "foo",
+									Weight:     0.123,
+									Occurrence: 123,
+								},
+							},
+						},
+					},
+				}, res[0])
+			assert.Equal(t,
+				map[string]interface{}{
+					"name": "Bar",
+					"_additional": map[string]interface{}{
+						"id": strfmt.UUID("id2"),
+						"classification": &models.AdditionalPropertiesClassification{
+							ID: "5678",
+						},
+						"nearestNeighbors": &models.NearestNeighbors{
+							Neighbors: []*models.NearestNeighbor{
+								&models.NearestNeighbor{
+									Concept:  "bar",
+									Distance: 0.1,
+								},
+							},
+						},
+						"interpretation": &models.Interpretation{
+							Source: []*models.InterpretationSource{
+								&models.InterpretationSource{
+									Concept:    "bar",
+									Weight:     0.456,
+									Occurrence: 456,
+								},
+							},
+						},
+					},
+				}, res[1])
+		})
+	})
 }
 
 func newFakeDistancer() func(a, b []float32) (float32, error) {