Spaces:
Running
Running
// _ _ | |
// __ _____ __ ___ ___ __ _| |_ ___ | |
// \ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \ | |
// \ V V / __/ (_| |\ V /| | (_| | || __/ | |
// \_/\_/ \___|\__,_| \_/ |_|\__,_|\__\___| | |
// | |
// Copyright © 2016 - 2024 Weaviate B.V. All rights reserved. | |
// | |
// CONTACT: [email protected] | |
// | |
package traverser | |
import ( | |
"context" | |
"testing" | |
"time" | |
"github.com/go-openapi/strfmt" | |
"github.com/pkg/errors" | |
"github.com/sirupsen/logrus/hooks/test" | |
"github.com/stretchr/testify/assert" | |
"github.com/stretchr/testify/require" | |
"github.com/weaviate/weaviate/entities/additional" | |
"github.com/weaviate/weaviate/entities/dto" | |
"github.com/weaviate/weaviate/entities/filters" | |
"github.com/weaviate/weaviate/entities/models" | |
"github.com/weaviate/weaviate/entities/modulecapabilities" | |
"github.com/weaviate/weaviate/entities/schema" | |
"github.com/weaviate/weaviate/entities/search" | |
"github.com/weaviate/weaviate/entities/searchparams" | |
"github.com/weaviate/weaviate/entities/vectorindex/hnsw" | |
"github.com/weaviate/weaviate/usecases/config" | |
) | |
var defaultConfig = config.Config{ | |
QueryDefaults: config.QueryDefaults{ | |
Limit: 100, | |
}, | |
QueryMaximumResults: 100, | |
} | |
func Test_Explorer_GetClass(t *testing.T) { | |
t.Run("when an explore param is set for nearVector", func(t *testing.T) { | |
// TODO: this is a module specific test case, which relies on the | |
// text2vec-contextionary module | |
params := dto.GetParams{ | |
ClassName: "BestClass", | |
NearVector: &searchparams.NearVector{ | |
Vector: []float32{0.8, 0.2, 0.7}, | |
}, | |
Pagination: &filters.Pagination{Limit: 100}, | |
Filters: nil, | |
} | |
searchResults := []search.Result{ | |
{ | |
ID: "id1", | |
Schema: map[string]interface{}{ | |
"name": "Foo", | |
}, | |
Dims: 128, | |
}, | |
{ | |
ID: "id2", | |
Schema: map[string]interface{}{ | |
"age": 200, | |
}, | |
Dims: 128, | |
}, | |
} | |
search := &fakeVectorSearcher{} | |
metrics := &fakeMetrics{} | |
log, _ := test.NewNullLogger() | |
explorer := NewExplorer(search, log, getFakeModulesProvider(), metrics, defaultConfig) | |
explorer.SetSchemaGetter(&fakeSchemaGetter{ | |
schema: schema.Schema{Objects: &models.Schema{Classes: []*models.Class{ | |
{Class: "BestClass"}, | |
}}}, | |
}) | |
expectedParamsToSearch := params | |
expectedParamsToSearch.SearchVector = []float32{0.8, 0.2, 0.7} | |
search. | |
On("VectorSearch", expectedParamsToSearch). | |
Return(searchResults, nil) | |
metrics.On("AddUsageDimensions", "BestClass", "get_graphql", "nearVector", 128) | |
res, err := explorer.GetClass(context.Background(), params) | |
t.Run("vector search must be called with right params", func(t *testing.T) { | |
assert.Nil(t, err) | |
search.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", | |
}, res[0]) | |
assert.Equal(t, | |
map[string]interface{}{ | |
"age": 200, | |
}, res[1]) | |
}) | |
t.Run("usage must be tracked", func(t *testing.T) { | |
metrics.AssertExpectations(t) | |
}) | |
}) | |
t.Run("when an explore param is set for nearObject without id and beacon", func(t *testing.T) { | |
t.Run("with distance", func(t *testing.T) { | |
// TODO: this is a module specific test case, which relies on the | |
// text2vec-contextionary module | |
params := dto.GetParams{ | |
ClassName: "BestClass", | |
NearObject: &searchparams.NearObject{ | |
Distance: 0.1, | |
}, | |
Pagination: &filters.Pagination{Limit: 100}, | |
Filters: nil, | |
} | |
search := &fakeVectorSearcher{} | |
log, _ := test.NewNullLogger() | |
metrics := &fakeMetrics{} | |
explorer := NewExplorer(search, log, getFakeModulesProvider(), metrics, defaultConfig) | |
explorer.SetSchemaGetter(&fakeSchemaGetter{ | |
schema: schema.Schema{Objects: &models.Schema{Classes: []*models.Class{ | |
{Class: "BestClass"}, | |
}}}, | |
}) | |
res, err := explorer.GetClass(context.Background(), params) | |
t.Run("vector search must be called with right params", func(t *testing.T) { | |
assert.NotNil(t, err) | |
assert.Nil(t, res) | |
assert.Contains(t, err.Error(), "explorer: get class: vectorize params: nearObject params: empty id and beacon") | |
}) | |
}) | |
t.Run("with certainty", func(t *testing.T) { | |
// TODO: this is a module specific test case, which relies on the | |
// text2vec-contextionary module | |
params := dto.GetParams{ | |
ClassName: "BestClass", | |
NearObject: &searchparams.NearObject{ | |
Certainty: 0.9, | |
}, | |
Pagination: &filters.Pagination{Limit: 100}, | |
Filters: nil, | |
} | |
search := &fakeVectorSearcher{} | |
log, _ := test.NewNullLogger() | |
metrics := &fakeMetrics{} | |
explorer := NewExplorer(search, log, getFakeModulesProvider(), metrics, defaultConfig) | |
explorer.SetSchemaGetter(&fakeSchemaGetter{ | |
schema: schema.Schema{Objects: &models.Schema{Classes: []*models.Class{ | |
{Class: "BestClass"}, | |
}}}, | |
}) | |
res, err := explorer.GetClass(context.Background(), params) | |
t.Run("vector search must be called with right params", func(t *testing.T) { | |
assert.NotNil(t, err) | |
assert.Nil(t, res) | |
assert.Contains(t, err.Error(), "explorer: get class: vectorize params: nearObject params: empty id and beacon") | |
}) | |
}) | |
}) | |
t.Run("when an explore param is set for nearObject with beacon", func(t *testing.T) { | |
t.Run("with distance", func(t *testing.T) { | |
t.Run("with certainty", func(t *testing.T) { | |
// TODO: this is a module specific test case, which relies on the | |
// text2vec-contextionary module | |
params := dto.GetParams{ | |
ClassName: "BestClass", | |
NearObject: &searchparams.NearObject{ | |
Beacon: "weaviate://localhost/e9c12c22-766f-4bde-b140-d4cf8fd6e041", | |
Distance: 0.1, | |
}, | |
Pagination: &filters.Pagination{Limit: 100}, | |
Filters: nil, | |
} | |
searchRes := search.Result{ | |
ID: "e9c12c22-766f-4bde-b140-d4cf8fd6e041", | |
Schema: map[string]interface{}{ | |
"name": "Foo", | |
}, | |
} | |
searchResults := []search.Result{ | |
{ | |
ID: "id1", | |
Schema: map[string]interface{}{ | |
"name": "Foo", | |
}, | |
Dims: 128, | |
}, | |
{ | |
ID: "id2", | |
Schema: map[string]interface{}{ | |
"age": 200, | |
}, | |
Dims: 128, | |
}, | |
} | |
search := &fakeVectorSearcher{} | |
log, _ := test.NewNullLogger() | |
metrics := &fakeMetrics{} | |
explorer := NewExplorer(search, log, getFakeModulesProvider(), metrics, defaultConfig) | |
explorer.SetSchemaGetter(&fakeSchemaGetter{ | |
schema: schema.Schema{Objects: &models.Schema{Classes: []*models.Class{ | |
{Class: "BestClass"}, | |
}}}, | |
}) | |
expectedParamsToSearch := params | |
search. | |
On("Object", "BestClass", strfmt.UUID("e9c12c22-766f-4bde-b140-d4cf8fd6e041")). | |
Return(&searchRes, nil) | |
search. | |
On("VectorSearch", expectedParamsToSearch). | |
Return(searchResults, nil) | |
metrics.On("AddUsageDimensions", "BestClass", "get_graphql", "nearObject", 128) | |
res, err := explorer.GetClass(context.Background(), params) | |
t.Run("vector search must be called with right params", func(t *testing.T) { | |
assert.Nil(t, err) | |
search.AssertExpectations(t) | |
}) | |
t.Run("response must contain object", func(t *testing.T) { | |
require.Len(t, res, 2) | |
assert.Equal(t, | |
map[string]interface{}{ | |
"name": "Foo", | |
}, res[0]) | |
assert.Equal(t, | |
map[string]interface{}{ | |
"age": 200, | |
}, res[1]) | |
}) | |
}) | |
}) | |
t.Run("with certainty", func(t *testing.T) { | |
// TODO: this is a module specific test case, which relies on the | |
// text2vec-contextionary module | |
params := dto.GetParams{ | |
ClassName: "BestClass", | |
NearObject: &searchparams.NearObject{ | |
Beacon: "weaviate://localhost/e9c12c22-766f-4bde-b140-d4cf8fd6e041", | |
Certainty: 0.9, | |
}, | |
Pagination: &filters.Pagination{Limit: 100}, | |
Filters: nil, | |
} | |
searchRes := search.Result{ | |
ID: "e9c12c22-766f-4bde-b140-d4cf8fd6e041", | |
Schema: map[string]interface{}{ | |
"name": "Foo", | |
}, | |
} | |
searchResults := []search.Result{ | |
{ | |
ID: "id1", | |
Schema: map[string]interface{}{ | |
"name": "Foo", | |
}, | |
Dims: 128, | |
}, | |
{ | |
ID: "id2", | |
Schema: map[string]interface{}{ | |
"age": 200, | |
}, | |
Dims: 128, | |
}, | |
} | |
search := &fakeVectorSearcher{} | |
log, _ := test.NewNullLogger() | |
metrics := &fakeMetrics{} | |
explorer := NewExplorer(search, log, getFakeModulesProvider(), metrics, defaultConfig) | |
explorer.SetSchemaGetter(&fakeSchemaGetter{ | |
schema: schema.Schema{Objects: &models.Schema{Classes: []*models.Class{ | |
{Class: "BestClass"}, | |
}}}, | |
}) | |
expectedParamsToSearch := params | |
search. | |
On("Object", "BestClass", strfmt.UUID("e9c12c22-766f-4bde-b140-d4cf8fd6e041")). | |
Return(&searchRes, nil) | |
search. | |
On("VectorSearch", expectedParamsToSearch). | |
Return(searchResults, nil) | |
metrics.On("AddUsageDimensions", "BestClass", "get_graphql", "nearObject", 128) | |
res, err := explorer.GetClass(context.Background(), params) | |
t.Run("vector search must be called with right params", func(t *testing.T) { | |
assert.Nil(t, err) | |
search.AssertExpectations(t) | |
}) | |
t.Run("response must contain object", func(t *testing.T) { | |
require.Len(t, res, 2) | |
assert.Equal(t, | |
map[string]interface{}{ | |
"name": "Foo", | |
}, res[0]) | |
assert.Equal(t, | |
map[string]interface{}{ | |
"age": 200, | |
}, res[1]) | |
}) | |
}) | |
}) | |
t.Run("when an explore param is set for nearObject with id", func(t *testing.T) { | |
t.Run("with distance", func(t *testing.T) { | |
// TODO: this is a module specific test case, which relies on the | |
// text2vec-contextionary module | |
params := dto.GetParams{ | |
ClassName: "BestClass", | |
NearObject: &searchparams.NearObject{ | |
ID: "e9c12c22-766f-4bde-b140-d4cf8fd6e041", | |
Distance: 0.1, | |
}, | |
Pagination: &filters.Pagination{Limit: 100}, | |
Filters: nil, | |
} | |
searchRes := search.Result{ | |
ID: "e9c12c22-766f-4bde-b140-d4cf8fd6e041", | |
Schema: map[string]interface{}{ | |
"name": "Foo", | |
}, | |
} | |
searchResults := []search.Result{ | |
{ | |
ID: "id1", | |
Schema: map[string]interface{}{ | |
"name": "Foo", | |
}, | |
Dims: 128, | |
}, | |
{ | |
ID: "id2", | |
Schema: map[string]interface{}{ | |
"age": 200, | |
}, | |
Dims: 128, | |
}, | |
} | |
search := &fakeVectorSearcher{} | |
log, _ := test.NewNullLogger() | |
metrics := &fakeMetrics{} | |
explorer := NewExplorer(search, log, getFakeModulesProvider(), metrics, defaultConfig) | |
explorer.SetSchemaGetter(&fakeSchemaGetter{ | |
schema: schema.Schema{Objects: &models.Schema{Classes: []*models.Class{ | |
{Class: "BestClass"}, | |
}}}, | |
}) | |
expectedParamsToSearch := params | |
search. | |
On("Object", "BestClass", strfmt.UUID("e9c12c22-766f-4bde-b140-d4cf8fd6e041")). | |
Return(&searchRes, nil) | |
search. | |
On("VectorSearch", expectedParamsToSearch). | |
Return(searchResults, nil) | |
metrics.On("AddUsageDimensions", "BestClass", "get_graphql", "nearObject", 128) | |
res, err := explorer.GetClass(context.Background(), params) | |
t.Run("vector search must be called with right params", func(t *testing.T) { | |
assert.Nil(t, err) | |
search.AssertExpectations(t) | |
}) | |
t.Run("response must contain object", func(t *testing.T) { | |
require.Len(t, res, 2) | |
assert.Equal(t, | |
map[string]interface{}{ | |
"name": "Foo", | |
}, res[0]) | |
assert.Equal(t, | |
map[string]interface{}{ | |
"age": 200, | |
}, res[1]) | |
}) | |
}) | |
t.Run("with certainty", func(t *testing.T) { | |
// TODO: this is a module specific test case, which relies on the | |
// text2vec-contextionary module | |
params := dto.GetParams{ | |
ClassName: "BestClass", | |
NearObject: &searchparams.NearObject{ | |
ID: "e9c12c22-766f-4bde-b140-d4cf8fd6e041", | |
Certainty: 0.9, | |
}, | |
Pagination: &filters.Pagination{Limit: 100}, | |
Filters: nil, | |
} | |
searchRes := search.Result{ | |
ID: "e9c12c22-766f-4bde-b140-d4cf8fd6e041", | |
Schema: map[string]interface{}{ | |
"name": "Foo", | |
}, | |
} | |
searchResults := []search.Result{ | |
{ | |
ID: "id1", | |
Schema: map[string]interface{}{ | |
"name": "Foo", | |
}, | |
Dims: 128, | |
}, | |
{ | |
ID: "id2", | |
Schema: map[string]interface{}{ | |
"age": 200, | |
}, | |
Dims: 128, | |
}, | |
} | |
search := &fakeVectorSearcher{} | |
metrics := &fakeMetrics{} | |
log, _ := test.NewNullLogger() | |
explorer := NewExplorer(search, log, getFakeModulesProvider(), metrics, defaultConfig) | |
explorer.SetSchemaGetter(&fakeSchemaGetter{ | |
schema: schema.Schema{Objects: &models.Schema{Classes: []*models.Class{ | |
{Class: "BestClass"}, | |
}}}, | |
}) | |
expectedParamsToSearch := params | |
search. | |
On("Object", "BestClass", strfmt.UUID("e9c12c22-766f-4bde-b140-d4cf8fd6e041")). | |
Return(&searchRes, nil) | |
search. | |
On("VectorSearch", expectedParamsToSearch). | |
Return(searchResults, nil) | |
metrics.On("AddUsageDimensions", "BestClass", "get_graphql", "nearObject", 128) | |
res, err := explorer.GetClass(context.Background(), params) | |
t.Run("vector search must be called with right params", func(t *testing.T) { | |
assert.Nil(t, err) | |
search.AssertExpectations(t) | |
}) | |
t.Run("response must contain object", func(t *testing.T) { | |
require.Len(t, res, 2) | |
assert.Equal(t, | |
map[string]interface{}{ | |
"name": "Foo", | |
}, res[0]) | |
assert.Equal(t, | |
map[string]interface{}{ | |
"age": 200, | |
}, res[1]) | |
}) | |
}) | |
}) | |
t.Run("when an explore param is set for nearVector and the required distance not met", | |
func(t *testing.T) { | |
t.Run("with distance", func(t *testing.T) { | |
params := dto.GetParams{ | |
ClassName: "BestClass", | |
NearVector: &searchparams.NearVector{ | |
Vector: []float32{0.8, 0.2, 0.7}, | |
Distance: 0.4, | |
WithDistance: true, | |
}, | |
Pagination: &filters.Pagination{Limit: 100}, | |
Filters: nil, | |
} | |
searchResults := []search.Result{ | |
{ | |
ID: "id1", | |
Dist: 2 * 0.69, | |
Dims: 128, | |
}, | |
{ | |
ID: "id2", | |
Dist: 2 * 0.69, | |
Dims: 128, | |
}, | |
} | |
search := &fakeVectorSearcher{} | |
log, _ := test.NewNullLogger() | |
metrics := &fakeMetrics{} | |
explorer := NewExplorer(search, log, getFakeModulesProvider(), metrics, defaultConfig) | |
explorer.SetSchemaGetter(&fakeSchemaGetter{ | |
schema: schema.Schema{Objects: &models.Schema{Classes: []*models.Class{ | |
{Class: "BestClass"}, | |
}}}, | |
}) | |
expectedParamsToSearch := params | |
expectedParamsToSearch.SearchVector = []float32{0.8, 0.2, 0.7} | |
search. | |
On("VectorSearch", expectedParamsToSearch). | |
Return(searchResults, nil) | |
metrics.On("AddUsageDimensions", "BestClass", "get_graphql", "nearVector", 128) | |
res, err := explorer.GetClass(context.Background(), params) | |
t.Run("vector search must be called with right params", func(t *testing.T) { | |
assert.Nil(t, err) | |
search.AssertExpectations(t) | |
}) | |
t.Run("no concept met the required certainty", func(t *testing.T) { | |
assert.Len(t, res, 0) | |
}) | |
}) | |
t.Run("with certainty", func(t *testing.T) { | |
params := dto.GetParams{ | |
ClassName: "BestClass", | |
NearVector: &searchparams.NearVector{ | |
Vector: []float32{0.8, 0.2, 0.7}, | |
Certainty: 0.8, | |
}, | |
Pagination: &filters.Pagination{Limit: 100}, | |
Filters: nil, | |
} | |
searchResults := []search.Result{ | |
{ | |
ID: "id1", | |
Dist: 2 * 0.69, | |
Dims: 128, | |
}, | |
{ | |
ID: "id2", | |
Dist: 2 * 0.69, | |
Dims: 128, | |
}, | |
} | |
search := &fakeVectorSearcher{} | |
log, _ := test.NewNullLogger() | |
metrics := &fakeMetrics{} | |
explorer := NewExplorer(search, log, getFakeModulesProvider(), metrics, defaultConfig) | |
explorer.SetSchemaGetter(&fakeSchemaGetter{ | |
schema: schema.Schema{Objects: &models.Schema{Classes: []*models.Class{ | |
{Class: "BestClass"}, | |
}}}, | |
}) | |
expectedParamsToSearch := params | |
expectedParamsToSearch.SearchVector = []float32{0.8, 0.2, 0.7} | |
search. | |
On("VectorSearch", expectedParamsToSearch). | |
Return(searchResults, nil) | |
metrics.On("AddUsageDimensions", "BestClass", "get_graphql", "nearVector", 128) | |
res, err := explorer.GetClass(context.Background(), params) | |
t.Run("vector search must be called with right params", func(t *testing.T) { | |
assert.Nil(t, err) | |
search.AssertExpectations(t) | |
}) | |
t.Run("no concept met the required certainty", func(t *testing.T) { | |
assert.Len(t, res, 0) | |
}) | |
}) | |
}) | |
t.Run("when two conflicting (nearVector, nearObject) near searchers are set", func(t *testing.T) { | |
params := dto.GetParams{ | |
ClassName: "BestClass", | |
Pagination: &filters.Pagination{Limit: 100}, | |
Filters: nil, | |
NearVector: &searchparams.NearVector{ | |
Vector: []float32{0.8, 0.2, 0.7}, | |
}, | |
NearObject: &searchparams.NearObject{ | |
Beacon: "weaviate://localhost/e9c12c22-766f-4bde-b140-d4cf8fd6e041", | |
}, | |
} | |
search := &fakeVectorSearcher{} | |
log, _ := test.NewNullLogger() | |
metrics := &fakeMetrics{} | |
explorer := NewExplorer(search, log, getFakeModulesProvider(), metrics, defaultConfig) | |
explorer.SetSchemaGetter(&fakeSchemaGetter{ | |
schema: schema.Schema{Objects: &models.Schema{Classes: []*models.Class{ | |
{Class: "BestClass"}, | |
}}}, | |
}) | |
_, err := explorer.GetClass(context.Background(), params) | |
require.NotNil(t, err) | |
assert.Contains(t, err.Error(), "parameters which are conflicting") | |
}) | |
t.Run("when no explore param is set", func(t *testing.T) { | |
params := dto.GetParams{ | |
ClassName: "BestClass", | |
Pagination: &filters.Pagination{Limit: 100}, | |
Filters: nil, | |
} | |
searchResults := []search.Result{ | |
{ | |
ID: "id1", | |
Schema: map[string]interface{}{ | |
"name": "Foo", | |
}, | |
}, | |
{ | |
ID: "id2", | |
Schema: map[string]interface{}{ | |
"age": 200, | |
}, | |
}, | |
} | |
search := &fakeVectorSearcher{} | |
log, _ := test.NewNullLogger() | |
metrics := &fakeMetrics{} | |
explorer := NewExplorer(search, log, getFakeModulesProvider(), metrics, defaultConfig) | |
explorer.SetSchemaGetter(&fakeSchemaGetter{ | |
schema: schema.Schema{Objects: &models.Schema{Classes: []*models.Class{ | |
{Class: "BestClass"}, | |
}}}, | |
}) | |
expectedParamsToSearch := params | |
expectedParamsToSearch.SearchVector = nil | |
search. | |
On("Search", 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) | |
search.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", | |
}, res[0]) | |
assert.Equal(t, | |
map[string]interface{}{ | |
"age": 200, | |
}, res[1]) | |
}) | |
}) | |
t.Run("near vector with group", func(t *testing.T) { | |
params := dto.GetParams{ | |
ClassName: "BestClass", | |
Pagination: &filters.Pagination{Limit: 100}, | |
Filters: nil, | |
NearVector: &searchparams.NearVector{ | |
Vector: []float32{0.8, 0.2, 0.7}, | |
}, | |
Group: &dto.GroupParams{ | |
Strategy: "closest", | |
Force: 1.0, | |
}, | |
} | |
searchResults := []search.Result{ | |
{ | |
ID: "id1", | |
Schema: map[string]interface{}{ | |
"name": "Foo", | |
}, | |
Dims: 128, | |
}, | |
} | |
search := &fakeVectorSearcher{} | |
log, _ := test.NewNullLogger() | |
metrics := &fakeMetrics{} | |
explorer := NewExplorer(search, log, getFakeModulesProvider(), metrics, defaultConfig) | |
explorer.SetSchemaGetter(&fakeSchemaGetter{ | |
schema: schema.Schema{Objects: &models.Schema{Classes: []*models.Class{ | |
{Class: "BestClass"}, | |
}}}, | |
}) | |
expectedParamsToSearch := params | |
expectedParamsToSearch.SearchVector = []float32{0.8, 0.2, 0.7} | |
expectedParamsToSearch.AdditionalProperties.Vector = true | |
search. | |
On("VectorSearch", expectedParamsToSearch). | |
Return(searchResults, nil) | |
metrics.On("AddUsageDimensions", "BestClass", "get_graphql", "nearVector", 128) | |
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) | |
search.AssertExpectations(t) | |
}) | |
t.Run("response must contain concepts", func(t *testing.T) { | |
require.Len(t, res, 1) | |
}) | |
}) | |
t.Run("when the semanticPath prop is set but cannot be", func(t *testing.T) { | |
params := dto.GetParams{ | |
ClassName: "BestClass", | |
Pagination: &filters.Pagination{Limit: 100}, | |
Filters: nil, | |
AdditionalProperties: additional.Properties{ | |
ModuleParams: map[string]interface{}{ | |
"semanticPath": getDefaultParam("semanticPath"), | |
}, | |
}, | |
} | |
searchResults := []search.Result{ | |
{ | |
ID: "id1", | |
Schema: map[string]interface{}{ | |
"name": "Foo", | |
}, | |
}, | |
{ | |
ID: "id2", | |
Schema: map[string]interface{}{ | |
"age": 200, | |
}, | |
}, | |
} | |
search := &fakeVectorSearcher{} | |
log, _ := test.NewNullLogger() | |
metrics := &fakeMetrics{} | |
explorer := NewExplorer(search, log, getFakeModulesProvider(), metrics, defaultConfig) | |
explorer.SetSchemaGetter(&fakeSchemaGetter{ | |
schema: schema.Schema{Objects: &models.Schema{Classes: []*models.Class{ | |
{Class: "BestClass"}, | |
}}}, | |
}) | |
expectedParamsToSearch := params | |
expectedParamsToSearch.SearchVector = nil | |
search. | |
On("Search", expectedParamsToSearch). | |
Return(searchResults, nil) | |
res, err := explorer.GetClass(context.Background(), params) | |
t.Run("error can't be nil", func(t *testing.T) { | |
assert.NotNil(t, err) | |
assert.Nil(t, res) | |
assert.Contains(t, err.Error(), "unknown capability: semanticPath") | |
}) | |
}) | |
t.Run("when the classification prop is set", func(t *testing.T) { | |
params := dto.GetParams{ | |
ClassName: "BestClass", | |
Pagination: &filters.Pagination{Limit: 100}, | |
Filters: nil, | |
AdditionalProperties: additional.Properties{ | |
Classification: true, | |
}, | |
} | |
searchResults := []search.Result{ | |
{ | |
ID: "id1", | |
Schema: map[string]interface{}{ | |
"name": "Foo", | |
}, | |
AdditionalProperties: models.AdditionalProperties{ | |
"classification": nil, | |
}, | |
}, | |
{ | |
ID: "id2", | |
Schema: map[string]interface{}{ | |
"age": 200, | |
}, | |
AdditionalProperties: models.AdditionalProperties{ | |
"classification": &additional.Classification{ | |
ID: "1234", | |
}, | |
}, | |
}, | |
} | |
search := &fakeVectorSearcher{} | |
log, _ := test.NewNullLogger() | |
metrics := &fakeMetrics{} | |
explorer := NewExplorer(search, log, getFakeModulesProvider(), metrics, defaultConfig) | |
explorer.SetSchemaGetter(&fakeSchemaGetter{ | |
schema: schema.Schema{Objects: &models.Schema{Classes: []*models.Class{ | |
{Class: "BestClass"}, | |
}}}, | |
}) | |
expectedParamsToSearch := params | |
expectedParamsToSearch.SearchVector = nil | |
search. | |
On("Search", 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) | |
search.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", | |
}, res[0]) | |
assert.Equal(t, | |
map[string]interface{}{ | |
"age": 200, | |
"_additional": map[string]interface{}{ | |
"classification": &additional.Classification{ | |
ID: "1234", | |
}, | |
}, | |
}, res[1]) | |
}) | |
}) | |
t.Run("when the interpretation prop is set", func(t *testing.T) { | |
params := dto.GetParams{ | |
ClassName: "BestClass", | |
Pagination: &filters.Pagination{Limit: 100}, | |
Filters: nil, | |
AdditionalProperties: additional.Properties{ | |
ModuleParams: map[string]interface{}{ | |
"interpretation": true, | |
}, | |
}, | |
} | |
searchResults := []search.Result{ | |
{ | |
ID: "id1", | |
Schema: map[string]interface{}{ | |
"name": "Foo", | |
}, | |
AdditionalProperties: models.AdditionalProperties{ | |
"interpretation": nil, | |
}, | |
}, | |
{ | |
ID: "id2", | |
Schema: map[string]interface{}{ | |
"age": 200, | |
}, | |
AdditionalProperties: models.AdditionalProperties{ | |
"interpretation": &Interpretation{ | |
Source: []*InterpretationSource{ | |
{ | |
Concept: "foo", | |
Weight: 0.123, | |
Occurrence: 123, | |
}, | |
}, | |
}, | |
}, | |
}, | |
} | |
search := &fakeVectorSearcher{} | |
log, _ := test.NewNullLogger() | |
metrics := &fakeMetrics{} | |
explorer := NewExplorer(search, log, getFakeModulesProvider(), metrics, defaultConfig) | |
explorer.SetSchemaGetter(&fakeSchemaGetter{ | |
schema: schema.Schema{Objects: &models.Schema{Classes: []*models.Class{ | |
{Class: "BestClass"}, | |
}}}, | |
}) | |
expectedParamsToSearch := params | |
expectedParamsToSearch.SearchVector = nil | |
search. | |
On("Search", 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) | |
search.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", | |
}, res[0]) | |
assert.Equal(t, | |
map[string]interface{}{ | |
"age": 200, | |
"_additional": map[string]interface{}{ | |
"interpretation": &Interpretation{ | |
Source: []*InterpretationSource{ | |
{ | |
Concept: "foo", | |
Weight: 0.123, | |
Occurrence: 123, | |
}, | |
}, | |
}, | |
}, | |
}, res[1]) | |
}) | |
}) | |
t.Run("when the vector _additional prop is set", func(t *testing.T) { | |
params := dto.GetParams{ | |
ClassName: "BestClass", | |
Pagination: &filters.Pagination{Limit: 100}, | |
Filters: nil, | |
AdditionalProperties: additional.Properties{ | |
Vector: true, | |
}, | |
} | |
searchResults := []search.Result{ | |
{ | |
ID: "id1", | |
Schema: map[string]interface{}{ | |
"name": "Foo", | |
}, | |
Vector: []float32{0.1, -0.3}, | |
Dims: 128, | |
}, | |
} | |
search := &fakeVectorSearcher{} | |
log, _ := test.NewNullLogger() | |
metrics := &fakeMetrics{} | |
explorer := NewExplorer(search, log, getFakeModulesProvider(), metrics, defaultConfig) | |
explorer.SetSchemaGetter(&fakeSchemaGetter{ | |
schema: schema.Schema{Objects: &models.Schema{Classes: []*models.Class{ | |
{Class: "BestClass"}, | |
}}}, | |
}) | |
expectedParamsToSearch := params | |
expectedParamsToSearch.SearchVector = nil | |
search. | |
On("Search", expectedParamsToSearch). | |
Return(searchResults, nil) | |
metrics.On("AddUsageDimensions", "BestClass", "get_graphql", "_additional.vector", 128) | |
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) | |
search.AssertExpectations(t) | |
}) | |
t.Run("response must contain vector", func(t *testing.T) { | |
require.Len(t, res, 1) | |
assert.Equal(t, | |
map[string]interface{}{ | |
"name": "Foo", | |
"_additional": map[string]interface{}{ | |
"vector": []float32{0.1, -0.3}, | |
}, | |
}, res[0]) | |
}) | |
}) | |
t.Run("when the creationTimeUnix _additional prop is set", func(t *testing.T) { | |
params := dto.GetParams{ | |
ClassName: "BestClass", | |
Pagination: &filters.Pagination{Limit: 100}, | |
Filters: nil, | |
AdditionalProperties: additional.Properties{ | |
CreationTimeUnix: true, | |
}, | |
} | |
now := time.Now().UnixNano() / int64(time.Millisecond) | |
searchResults := []search.Result{ | |
{ | |
ID: "id1", | |
Schema: map[string]interface{}{ | |
"name": "Foo", | |
}, | |
Created: now, | |
}, | |
} | |
search := &fakeVectorSearcher{} | |
log, _ := test.NewNullLogger() | |
metrics := &fakeMetrics{} | |
explorer := NewExplorer(search, log, getFakeModulesProvider(), metrics, defaultConfig) | |
explorer.SetSchemaGetter(&fakeSchemaGetter{ | |
schema: schema.Schema{Objects: &models.Schema{Classes: []*models.Class{ | |
{Class: "BestClass"}, | |
}}}, | |
}) | |
expectedParamsToSearch := params | |
expectedParamsToSearch.SearchVector = nil | |
search. | |
On("Search", 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) | |
search.AssertExpectations(t) | |
}) | |
t.Run("response must contain creationTimeUnix", func(t *testing.T) { | |
require.Len(t, res, 1) | |
assert.Equal(t, | |
map[string]interface{}{ | |
"name": "Foo", | |
"_additional": map[string]interface{}{ | |
"creationTimeUnix": now, | |
}, | |
}, res[0]) | |
}) | |
}) | |
t.Run("when the lastUpdateTimeUnix _additional prop is set", func(t *testing.T) { | |
params := dto.GetParams{ | |
ClassName: "BestClass", | |
Pagination: &filters.Pagination{Limit: 100}, | |
Filters: nil, | |
AdditionalProperties: additional.Properties{ | |
LastUpdateTimeUnix: true, | |
}, | |
} | |
now := time.Now().UnixNano() / int64(time.Millisecond) | |
searchResults := []search.Result{ | |
{ | |
ID: "id1", | |
Schema: map[string]interface{}{ | |
"name": "Foo", | |
}, | |
Updated: now, | |
}, | |
} | |
search := &fakeVectorSearcher{} | |
log, _ := test.NewNullLogger() | |
metrics := &fakeMetrics{} | |
explorer := NewExplorer(search, log, getFakeModulesProvider(), metrics, defaultConfig) | |
explorer.SetSchemaGetter(&fakeSchemaGetter{ | |
schema: schema.Schema{Objects: &models.Schema{Classes: []*models.Class{ | |
{Class: "BestClass"}, | |
}}}, | |
}) | |
expectedParamsToSearch := params | |
expectedParamsToSearch.SearchVector = nil | |
search. | |
On("Search", 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) | |
search.AssertExpectations(t) | |
}) | |
t.Run("response must contain lastUpdateTimeUnix", func(t *testing.T) { | |
require.Len(t, res, 1) | |
assert.Equal(t, | |
map[string]interface{}{ | |
"name": "Foo", | |
"_additional": map[string]interface{}{ | |
"lastUpdateTimeUnix": now, | |
}, | |
}, res[0]) | |
}) | |
}) | |
t.Run("when the nearestNeighbors prop is set", func(t *testing.T) { | |
params := dto.GetParams{ | |
ClassName: "BestClass", | |
Pagination: &filters.Pagination{Limit: 100}, | |
Filters: nil, | |
AdditionalProperties: additional.Properties{ | |
ModuleParams: map[string]interface{}{ | |
"nearestNeighbors": true, | |
}, | |
}, | |
} | |
searchResults := []search.Result{ | |
{ | |
ID: "id1", | |
Schema: map[string]interface{}{ | |
"name": "Foo", | |
}, | |
}, | |
{ | |
ID: "id2", | |
Schema: map[string]interface{}{ | |
"name": "Bar", | |
}, | |
}, | |
} | |
searcher := &fakeVectorSearcher{} | |
log, _ := test.NewNullLogger() | |
extender := &fakeExtender{ | |
returnArgs: []search.Result{ | |
{ | |
ID: "id1", | |
Schema: map[string]interface{}{ | |
"name": "Foo", | |
}, | |
AdditionalProperties: models.AdditionalProperties{ | |
"nearestNeighbors": &NearestNeighbors{ | |
Neighbors: []*NearestNeighbor{ | |
{ | |
Concept: "foo", | |
Distance: 0.1, | |
}, | |
}, | |
}, | |
}, | |
}, | |
{ | |
ID: "id2", | |
Schema: map[string]interface{}{ | |
"name": "Bar", | |
}, | |
AdditionalProperties: models.AdditionalProperties{ | |
"nearestNeighbors": &NearestNeighbors{ | |
Neighbors: []*NearestNeighbor{ | |
{ | |
Concept: "bar", | |
Distance: 0.1, | |
}, | |
}, | |
}, | |
}, | |
}, | |
}, | |
} | |
explorer := NewExplorer(searcher, log, getFakeModulesProviderWithCustomExtenders(extender, nil, nil), nil, defaultConfig) | |
explorer.SetSchemaGetter(&fakeSchemaGetter{ | |
schema: schema.Schema{Objects: &models.Schema{Classes: []*models.Class{ | |
{Class: "BestClass"}, | |
}}}, | |
}) | |
expectedParamsToSearch := params | |
expectedParamsToSearch.SearchVector = nil | |
searcher. | |
On("Search", 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{}{ | |
"nearestNeighbors": &NearestNeighbors{ | |
Neighbors: []*NearestNeighbor{ | |
{ | |
Concept: "foo", | |
Distance: 0.1, | |
}, | |
}, | |
}, | |
}, | |
}, res[0]) | |
assert.Equal(t, | |
map[string]interface{}{ | |
"name": "Bar", | |
"_additional": map[string]interface{}{ | |
"nearestNeighbors": &NearestNeighbors{ | |
Neighbors: []*NearestNeighbor{ | |
{ | |
Concept: "bar", | |
Distance: 0.1, | |
}, | |
}, | |
}, | |
}, | |
}, res[1]) | |
}) | |
}) | |
t.Run("when the featureProjection prop is set", func(t *testing.T) { | |
params := dto.GetParams{ | |
ClassName: "BestClass", | |
Pagination: &filters.Pagination{Limit: 100}, | |
Filters: nil, | |
AdditionalProperties: additional.Properties{ | |
ModuleParams: map[string]interface{}{ | |
"featureProjection": getDefaultParam("featureProjection"), | |
}, | |
}, | |
} | |
searchResults := []search.Result{ | |
{ | |
ID: "id1", | |
Schema: map[string]interface{}{ | |
"name": "Foo", | |
}, | |
}, | |
{ | |
ID: "id2", | |
Schema: map[string]interface{}{ | |
"name": "Bar", | |
}, | |
}, | |
} | |
searcher := &fakeVectorSearcher{} | |
log, _ := test.NewNullLogger() | |
projector := &fakeProjector{ | |
returnArgs: []search.Result{ | |
{ | |
ID: "id1", | |
Schema: map[string]interface{}{ | |
"name": "Foo", | |
}, | |
AdditionalProperties: models.AdditionalProperties{ | |
"featureProjection": &FeatureProjection{ | |
Vector: []float32{0, 1}, | |
}, | |
}, | |
}, | |
{ | |
ID: "id2", | |
Schema: map[string]interface{}{ | |
"name": "Bar", | |
}, | |
AdditionalProperties: models.AdditionalProperties{ | |
"featureProjection": &FeatureProjection{ | |
Vector: []float32{1, 0}, | |
}, | |
}, | |
}, | |
}, | |
} | |
explorer := NewExplorer(searcher, log, getFakeModulesProviderWithCustomExtenders(nil, projector, nil), nil, defaultConfig) | |
explorer.SetSchemaGetter(&fakeSchemaGetter{ | |
schema: schema.Schema{Objects: &models.Schema{Classes: []*models.Class{ | |
{Class: "BestClass"}, | |
}}}, | |
}) | |
expectedParamsToSearch := params | |
expectedParamsToSearch.SearchVector = nil | |
searcher. | |
On("Search", 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{}{ | |
"featureProjection": &FeatureProjection{ | |
Vector: []float32{0, 1}, | |
}, | |
}, | |
}, res[0]) | |
assert.Equal(t, | |
map[string]interface{}{ | |
"name": "Bar", | |
"_additional": map[string]interface{}{ | |
"featureProjection": &FeatureProjection{ | |
Vector: []float32{1, 0}, | |
}, | |
}, | |
}, res[1]) | |
}) | |
}) | |
t.Run("when the _additional on ref prop is set", func(t *testing.T) { | |
now := time.Now().UnixMilli() | |
params := dto.GetParams{ | |
ClassName: "BestClass", | |
Pagination: &filters.Pagination{Limit: 100}, | |
Filters: nil, | |
Properties: []search.SelectProperty{ | |
{ | |
Name: "ofBestRefClass", | |
Refs: []search.SelectClass{ | |
{ | |
ClassName: "BestRefClass", | |
AdditionalProperties: additional.Properties{ | |
ID: true, | |
Vector: true, | |
CreationTimeUnix: true, | |
LastUpdateTimeUnix: true, | |
}, | |
}, | |
}, | |
}, | |
}, | |
} | |
searchResults := []search.Result{ | |
{ | |
ID: "id1", | |
Schema: map[string]interface{}{ | |
"name": "Foo", | |
"ofBestRefClass": []interface{}{ | |
search.LocalRef{ | |
Class: "BestRefClass", | |
Fields: map[string]interface{}{ | |
"id": "2d68456c-73b4-4cfc-a6dc-718efc5b4cea", | |
"vector": []float32{1, 0}, | |
"creationTimeUnix": now, | |
"lastUpdateTimeUnix": now, | |
}, | |
}, | |
}, | |
}, | |
AdditionalProperties: models.AdditionalProperties{ | |
"classification": nil, | |
}, | |
}, | |
{ | |
ID: "id2", | |
Schema: map[string]interface{}{ | |
"age": 200, | |
"ofBestRefClass": []interface{}{ | |
search.LocalRef{ | |
Class: "BestRefClass", | |
Fields: map[string]interface{}{ | |
"id": "2d68456c-73b4-4cfc-a6dc-718efc5b4ceb", | |
}, | |
}, | |
}, | |
}, | |
AdditionalProperties: models.AdditionalProperties{ | |
"classification": &additional.Classification{ | |
ID: "1234", | |
}, | |
}, | |
}, | |
} | |
fakeSearch := &fakeVectorSearcher{} | |
log, _ := test.NewNullLogger() | |
metrics := &fakeMetrics{} | |
explorer := NewExplorer(fakeSearch, log, getFakeModulesProvider(), metrics, defaultConfig) | |
explorer.SetSchemaGetter(&fakeSchemaGetter{ | |
schema: schema.Schema{Objects: &models.Schema{Classes: []*models.Class{ | |
{Class: "BestClass"}, | |
}}}, | |
}) | |
expectedParamsToSearch := params | |
expectedParamsToSearch.SearchVector = nil | |
fakeSearch. | |
On("Search", 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) | |
fakeSearch.AssertExpectations(t) | |
}) | |
t.Run("response must contain _additional id and vector params for ref prop", func(t *testing.T) { | |
require.Len(t, res, 2) | |
assert.Equal(t, | |
map[string]interface{}{ | |
"name": "Foo", | |
"ofBestRefClass": []interface{}{ | |
search.LocalRef{ | |
Class: "BestRefClass", | |
Fields: map[string]interface{}{ | |
"_additional": map[string]interface{}{ | |
"id": "2d68456c-73b4-4cfc-a6dc-718efc5b4cea", | |
"vector": []float32{1, 0}, | |
"creationTimeUnix": now, | |
"lastUpdateTimeUnix": now, | |
}, | |
"id": "2d68456c-73b4-4cfc-a6dc-718efc5b4cea", | |
"vector": []float32{1, 0}, | |
"creationTimeUnix": now, | |
"lastUpdateTimeUnix": now, | |
}, | |
}, | |
}, | |
}, res[0]) | |
assert.Equal(t, | |
map[string]interface{}{ | |
"age": 200, | |
"ofBestRefClass": []interface{}{ | |
search.LocalRef{ | |
Class: "BestRefClass", | |
Fields: map[string]interface{}{ | |
"_additional": map[string]interface{}{ | |
"id": "2d68456c-73b4-4cfc-a6dc-718efc5b4ceb", | |
"vector": nil, | |
"creationTimeUnix": nil, | |
"lastUpdateTimeUnix": nil, | |
}, | |
"id": "2d68456c-73b4-4cfc-a6dc-718efc5b4ceb", | |
}, | |
}, | |
}, | |
"_additional": map[string]interface{}{ | |
"classification": &additional.Classification{ | |
ID: "1234", | |
}, | |
}, | |
}, res[1]) | |
}) | |
}) | |
t.Run("when the _additional on all refs prop is set", func(t *testing.T) { | |
params := dto.GetParams{ | |
ClassName: "BestClass", | |
Pagination: &filters.Pagination{Limit: 100}, | |
Filters: nil, | |
Properties: []search.SelectProperty{ | |
{ | |
Name: "ofBestRefClass", | |
Refs: []search.SelectClass{ | |
{ | |
ClassName: "BestRefClass", | |
AdditionalProperties: additional.Properties{ | |
ID: true, | |
}, | |
RefProperties: search.SelectProperties{ | |
search.SelectProperty{ | |
Name: "ofBestRefInnerClass", | |
Refs: []search.SelectClass{ | |
{ | |
ClassName: "BestRefInnerClass", | |
AdditionalProperties: additional.Properties{ | |
ID: true, | |
}, | |
}, | |
}, | |
}, | |
}, | |
}, | |
}, | |
}, | |
}, | |
} | |
searchResults := []search.Result{ | |
{ | |
ID: "id1", | |
Schema: map[string]interface{}{ | |
"name": "Foo", | |
"ofBestRefClass": []interface{}{ | |
search.LocalRef{ | |
Class: "BestRefClass", | |
Fields: map[string]interface{}{ | |
"id": "2d68456c-73b4-4cfc-a6dc-718efc5b4cea", | |
"ofBestRefInnerClass": []interface{}{ | |
search.LocalRef{ | |
Class: "BestRefInnerClass", | |
Fields: map[string]interface{}{ | |
"id": "2d68456c-73b4-4cfc-a6dc-718efc5b4caa", | |
}, | |
}, | |
}, | |
}, | |
}, | |
}, | |
}, | |
AdditionalProperties: models.AdditionalProperties{ | |
"classification": nil, | |
}, | |
}, | |
{ | |
ID: "id2", | |
Schema: map[string]interface{}{ | |
"age": 200, | |
"ofBestRefClass": []interface{}{ | |
search.LocalRef{ | |
Class: "BestRefClass", | |
Fields: map[string]interface{}{ | |
"id": "2d68456c-73b4-4cfc-a6dc-718efc5b4ceb", | |
"ofBestRefInnerClass": []interface{}{ | |
search.LocalRef{ | |
Class: "BestRefInnerClass", | |
Fields: map[string]interface{}{ | |
"id": "2d68456c-73b4-4cfc-a6dc-718efc5b4cbb", | |
}, | |
}, | |
}, | |
}, | |
}, | |
}, | |
}, | |
AdditionalProperties: models.AdditionalProperties{ | |
"classification": &additional.Classification{ | |
ID: "1234", | |
}, | |
}, | |
}, | |
} | |
fakeSearch := &fakeVectorSearcher{} | |
log, _ := test.NewNullLogger() | |
metrics := &fakeMetrics{} | |
explorer := NewExplorer(fakeSearch, log, getFakeModulesProvider(), metrics, defaultConfig) | |
explorer.SetSchemaGetter(&fakeSchemaGetter{ | |
schema: schema.Schema{Objects: &models.Schema{Classes: []*models.Class{ | |
{Class: "BestClass"}, | |
}}}, | |
}) | |
expectedParamsToSearch := params | |
expectedParamsToSearch.SearchVector = nil | |
fakeSearch. | |
On("Search", 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) | |
fakeSearch.AssertExpectations(t) | |
}) | |
t.Run("response must contain _additional id param for ref prop", func(t *testing.T) { | |
require.Len(t, res, 2) | |
assert.Equal(t, | |
map[string]interface{}{ | |
"name": "Foo", | |
"ofBestRefClass": []interface{}{ | |
search.LocalRef{ | |
Class: "BestRefClass", | |
Fields: map[string]interface{}{ | |
"_additional": map[string]interface{}{ | |
"id": "2d68456c-73b4-4cfc-a6dc-718efc5b4cea", | |
}, | |
"id": "2d68456c-73b4-4cfc-a6dc-718efc5b4cea", | |
"ofBestRefInnerClass": []interface{}{ | |
search.LocalRef{ | |
Class: "BestRefInnerClass", | |
Fields: map[string]interface{}{ | |
"_additional": map[string]interface{}{ | |
"id": "2d68456c-73b4-4cfc-a6dc-718efc5b4caa", | |
}, | |
"id": "2d68456c-73b4-4cfc-a6dc-718efc5b4caa", | |
}, | |
}, | |
}, | |
}, | |
}, | |
}, | |
}, res[0]) | |
assert.Equal(t, | |
map[string]interface{}{ | |
"age": 200, | |
"ofBestRefClass": []interface{}{ | |
search.LocalRef{ | |
Class: "BestRefClass", | |
Fields: map[string]interface{}{ | |
"_additional": map[string]interface{}{ | |
"id": "2d68456c-73b4-4cfc-a6dc-718efc5b4ceb", | |
}, | |
"id": "2d68456c-73b4-4cfc-a6dc-718efc5b4ceb", | |
"ofBestRefInnerClass": []interface{}{ | |
search.LocalRef{ | |
Class: "BestRefInnerClass", | |
Fields: map[string]interface{}{ | |
"_additional": map[string]interface{}{ | |
"id": "2d68456c-73b4-4cfc-a6dc-718efc5b4cbb", | |
}, | |
"id": "2d68456c-73b4-4cfc-a6dc-718efc5b4cbb", | |
}, | |
}, | |
}, | |
}, | |
}, | |
}, | |
"_additional": map[string]interface{}{ | |
"classification": &additional.Classification{ | |
ID: "1234", | |
}, | |
}, | |
}, res[1]) | |
}) | |
}) | |
t.Run("when the _additional on lots of refs prop is set", func(t *testing.T) { | |
now := time.Now().UnixMilli() | |
vec := []float32{1, 2, 3} | |
params := dto.GetParams{ | |
ClassName: "BestClass", | |
Pagination: &filters.Pagination{Limit: 100}, | |
Filters: nil, | |
Properties: []search.SelectProperty{ | |
{ | |
Name: "ofBestRefClass", | |
Refs: []search.SelectClass{ | |
{ | |
ClassName: "BestRefClass", | |
AdditionalProperties: additional.Properties{ | |
ID: true, | |
Vector: true, | |
CreationTimeUnix: true, | |
LastUpdateTimeUnix: true, | |
}, | |
RefProperties: search.SelectProperties{ | |
search.SelectProperty{ | |
Name: "ofBestRefInnerClass", | |
Refs: []search.SelectClass{ | |
{ | |
ClassName: "BestRefInnerClass", | |
AdditionalProperties: additional.Properties{ | |
ID: true, | |
Vector: true, | |
CreationTimeUnix: true, | |
LastUpdateTimeUnix: true, | |
}, | |
RefProperties: search.SelectProperties{ | |
search.SelectProperty{ | |
Name: "ofBestRefInnerInnerClass", | |
Refs: []search.SelectClass{ | |
{ | |
ClassName: "BestRefInnerInnerClass", | |
AdditionalProperties: additional.Properties{ | |
ID: true, | |
Vector: true, | |
CreationTimeUnix: true, | |
LastUpdateTimeUnix: true, | |
}, | |
}, | |
}, | |
}, | |
}, | |
}, | |
}, | |
}, | |
}, | |
}, | |
}, | |
}, | |
}, | |
} | |
searchResults := []search.Result{ | |
{ | |
ID: "id1", | |
Schema: map[string]interface{}{ | |
"name": "Foo", | |
"ofBestRefClass": []interface{}{ | |
search.LocalRef{ | |
Class: "BestRefClass", | |
Fields: map[string]interface{}{ | |
"id": "2d68456c-73b4-4cfc-a6dc-718efc5b4cea", | |
"creationTimeUnix": now, | |
"lastUpdateTimeUnix": now, | |
"vector": vec, | |
"ofBestRefInnerClass": []interface{}{ | |
search.LocalRef{ | |
Class: "BestRefInnerClass", | |
Fields: map[string]interface{}{ | |
"id": "2d68456c-73b4-4cfc-a6dc-718efc5b4caa", | |
"creationTimeUnix": now, | |
"lastUpdateTimeUnix": now, | |
"vector": vec, | |
"ofBestRefInnerInnerClass": []interface{}{ | |
search.LocalRef{ | |
Class: "BestRefInnerInnerClass", | |
Fields: map[string]interface{}{ | |
"id": "2d68456c-73b4-4cfc-a6dc-718efc5b4aaa", | |
"creationTimeUnix": now, | |
"lastUpdateTimeUnix": now, | |
"vector": vec, | |
}, | |
}, | |
}, | |
}, | |
}, | |
}, | |
}, | |
}, | |
}, | |
}, | |
AdditionalProperties: models.AdditionalProperties{ | |
"classification": nil, | |
}, | |
}, | |
{ | |
ID: "id2", | |
Schema: map[string]interface{}{ | |
"age": 200, | |
"ofBestRefClass": []interface{}{ | |
search.LocalRef{ | |
Class: "BestRefClass", | |
Fields: map[string]interface{}{ | |
"id": "2d68456c-73b4-4cfc-a6dc-718efc5b4ceb", | |
"creationTimeUnix": now, | |
"lastUpdateTimeUnix": now, | |
"vector": vec, | |
"ofBestRefInnerClass": []interface{}{ | |
search.LocalRef{ | |
Class: "BestRefInnerClass", | |
Fields: map[string]interface{}{ | |
"id": "2d68456c-73b4-4cfc-a6dc-718efc5b4cbb", | |
"creationTimeUnix": now, | |
"lastUpdateTimeUnix": now, | |
"vector": vec, | |
"ofBestRefInnerInnerClass": []interface{}{ | |
search.LocalRef{ | |
Class: "BestRefInnerInnerClass", | |
Fields: map[string]interface{}{ | |
"id": "2d68456c-73b4-4cfc-a6dc-718efc5b4bbb", | |
"creationTimeUnix": now, | |
"lastUpdateTimeUnix": now, | |
"vector": vec, | |
}, | |
}, | |
}, | |
}, | |
}, | |
}, | |
}, | |
}, | |
}, | |
}, | |
AdditionalProperties: models.AdditionalProperties{ | |
"classification": &additional.Classification{ | |
ID: "1234", | |
}, | |
}, | |
}, | |
} | |
fakeSearch := &fakeVectorSearcher{} | |
log, _ := test.NewNullLogger() | |
metrics := &fakeMetrics{} | |
explorer := NewExplorer(fakeSearch, log, getFakeModulesProvider(), metrics, defaultConfig) | |
explorer.SetSchemaGetter(&fakeSchemaGetter{ | |
schema: schema.Schema{Objects: &models.Schema{Classes: []*models.Class{ | |
{Class: "BestClass"}, | |
}}}, | |
}) | |
expectedParamsToSearch := params | |
expectedParamsToSearch.SearchVector = nil | |
fakeSearch. | |
On("Search", 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) | |
fakeSearch.AssertExpectations(t) | |
}) | |
t.Run("response must contain _additional id param for ref prop", func(t *testing.T) { | |
require.Len(t, res, 2) | |
assert.Equal(t, | |
map[string]interface{}{ | |
"name": "Foo", | |
"ofBestRefClass": []interface{}{ | |
search.LocalRef{ | |
Class: "BestRefClass", | |
Fields: map[string]interface{}{ | |
"_additional": map[string]interface{}{ | |
"id": "2d68456c-73b4-4cfc-a6dc-718efc5b4cea", | |
"creationTimeUnix": now, | |
"lastUpdateTimeUnix": now, | |
"vector": vec, | |
}, | |
"id": "2d68456c-73b4-4cfc-a6dc-718efc5b4cea", | |
"creationTimeUnix": now, | |
"lastUpdateTimeUnix": now, | |
"vector": vec, | |
"ofBestRefInnerClass": []interface{}{ | |
search.LocalRef{ | |
Class: "BestRefInnerClass", | |
Fields: map[string]interface{}{ | |
"_additional": map[string]interface{}{ | |
"id": "2d68456c-73b4-4cfc-a6dc-718efc5b4caa", | |
"creationTimeUnix": now, | |
"lastUpdateTimeUnix": now, | |
"vector": vec, | |
}, | |
"id": "2d68456c-73b4-4cfc-a6dc-718efc5b4caa", | |
"creationTimeUnix": now, | |
"lastUpdateTimeUnix": now, | |
"vector": vec, | |
"ofBestRefInnerInnerClass": []interface{}{ | |
search.LocalRef{ | |
Class: "BestRefInnerInnerClass", | |
Fields: map[string]interface{}{ | |
"_additional": map[string]interface{}{ | |
"id": "2d68456c-73b4-4cfc-a6dc-718efc5b4aaa", | |
"creationTimeUnix": now, | |
"lastUpdateTimeUnix": now, | |
"vector": vec, | |
}, | |
"id": "2d68456c-73b4-4cfc-a6dc-718efc5b4aaa", | |
"creationTimeUnix": now, | |
"lastUpdateTimeUnix": now, | |
"vector": vec, | |
}, | |
}, | |
}, | |
}, | |
}, | |
}, | |
}, | |
}, | |
}, | |
}, res[0]) | |
assert.Equal(t, | |
map[string]interface{}{ | |
"age": 200, | |
"ofBestRefClass": []interface{}{ | |
search.LocalRef{ | |
Class: "BestRefClass", | |
Fields: map[string]interface{}{ | |
"_additional": map[string]interface{}{ | |
"id": "2d68456c-73b4-4cfc-a6dc-718efc5b4ceb", | |
"creationTimeUnix": now, | |
"lastUpdateTimeUnix": now, | |
"vector": vec, | |
}, | |
"id": "2d68456c-73b4-4cfc-a6dc-718efc5b4ceb", | |
"creationTimeUnix": now, | |
"lastUpdateTimeUnix": now, | |
"vector": vec, | |
"ofBestRefInnerClass": []interface{}{ | |
search.LocalRef{ | |
Class: "BestRefInnerClass", | |
Fields: map[string]interface{}{ | |
"_additional": map[string]interface{}{ | |
"id": "2d68456c-73b4-4cfc-a6dc-718efc5b4cbb", | |
"creationTimeUnix": now, | |
"lastUpdateTimeUnix": now, | |
"vector": vec, | |
}, | |
"id": "2d68456c-73b4-4cfc-a6dc-718efc5b4cbb", | |
"creationTimeUnix": now, | |
"lastUpdateTimeUnix": now, | |
"vector": vec, | |
"ofBestRefInnerInnerClass": []interface{}{ | |
search.LocalRef{ | |
Class: "BestRefInnerInnerClass", | |
Fields: map[string]interface{}{ | |
"_additional": map[string]interface{}{ | |
"id": "2d68456c-73b4-4cfc-a6dc-718efc5b4bbb", | |
"creationTimeUnix": now, | |
"lastUpdateTimeUnix": now, | |
"vector": vec, | |
}, | |
"id": "2d68456c-73b4-4cfc-a6dc-718efc5b4bbb", | |
"creationTimeUnix": now, | |
"lastUpdateTimeUnix": now, | |
"vector": vec, | |
}, | |
}, | |
}, | |
}, | |
}, | |
}, | |
}, | |
}, | |
}, | |
"_additional": map[string]interface{}{ | |
"classification": &additional.Classification{ | |
ID: "1234", | |
}, | |
}, | |
}, res[1]) | |
}) | |
}) | |
t.Run("when the almost all _additional props set", func(t *testing.T) { | |
params := dto.GetParams{ | |
ClassName: "BestClass", | |
Pagination: &filters.Pagination{Limit: 100}, | |
Filters: nil, | |
AdditionalProperties: additional.Properties{ | |
ID: true, | |
Classification: true, | |
ModuleParams: map[string]interface{}{ | |
"interpretation": true, | |
"nearestNeighbors": true, | |
}, | |
}, | |
} | |
searchResults := []search.Result{ | |
{ | |
ID: "id1", | |
Schema: map[string]interface{}{ | |
"name": "Foo", | |
}, | |
AdditionalProperties: models.AdditionalProperties{ | |
"classification": &additional.Classification{ | |
ID: "1234", | |
}, | |
"nearestNeighbors": &NearestNeighbors{ | |
Neighbors: []*NearestNeighbor{ | |
{ | |
Concept: "foo", | |
Distance: 0.1, | |
}, | |
}, | |
}, | |
}, | |
}, | |
{ | |
ID: "id2", | |
Schema: map[string]interface{}{ | |
"name": "Bar", | |
}, | |
AdditionalProperties: models.AdditionalProperties{ | |
"classification": &additional.Classification{ | |
ID: "5678", | |
}, | |
"nearestNeighbors": &NearestNeighbors{ | |
Neighbors: []*NearestNeighbor{ | |
{ | |
Concept: "bar", | |
Distance: 0.1, | |
}, | |
}, | |
}, | |
}, | |
}, | |
} | |
searcher := &fakeVectorSearcher{} | |
log, _ := test.NewNullLogger() | |
extender := &fakeExtender{ | |
returnArgs: []search.Result{ | |
{ | |
ID: "id1", | |
Schema: map[string]interface{}{ | |
"name": "Foo", | |
}, | |
AdditionalProperties: models.AdditionalProperties{ | |
"classification": &additional.Classification{ | |
ID: "1234", | |
}, | |
"interpretation": &Interpretation{ | |
Source: []*InterpretationSource{ | |
{ | |
Concept: "foo", | |
Weight: 0.123, | |
Occurrence: 123, | |
}, | |
}, | |
}, | |
"nearestNeighbors": &NearestNeighbors{ | |
Neighbors: []*NearestNeighbor{ | |
{ | |
Concept: "foo", | |
Distance: 0.1, | |
}, | |
}, | |
}, | |
}, | |
}, | |
{ | |
ID: "id2", | |
Schema: map[string]interface{}{ | |
"name": "Bar", | |
}, | |
AdditionalProperties: models.AdditionalProperties{ | |
"classification": &additional.Classification{ | |
ID: "5678", | |
}, | |
"interpretation": &Interpretation{ | |
Source: []*InterpretationSource{ | |
{ | |
Concept: "bar", | |
Weight: 0.456, | |
Occurrence: 456, | |
}, | |
}, | |
}, | |
"nearestNeighbors": &NearestNeighbors{ | |
Neighbors: []*NearestNeighbor{ | |
{ | |
Concept: "bar", | |
Distance: 0.1, | |
}, | |
}, | |
}, | |
}, | |
}, | |
}, | |
} | |
explorer := NewExplorer(searcher, log, getFakeModulesProviderWithCustomExtenders(extender, nil, nil), nil, defaultConfig) | |
explorer.SetSchemaGetter(&fakeSchemaGetter{ | |
schema: schema.Schema{Objects: &models.Schema{Classes: []*models.Class{ | |
{Class: "BestClass"}, | |
}}}, | |
}) | |
expectedParamsToSearch := params | |
expectedParamsToSearch.SearchVector = nil | |
searcher. | |
On("Search", 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": &additional.Classification{ | |
ID: "1234", | |
}, | |
"nearestNeighbors": &NearestNeighbors{ | |
Neighbors: []*NearestNeighbor{ | |
{ | |
Concept: "foo", | |
Distance: 0.1, | |
}, | |
}, | |
}, | |
"interpretation": &Interpretation{ | |
Source: []*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": &additional.Classification{ | |
ID: "5678", | |
}, | |
"nearestNeighbors": &NearestNeighbors{ | |
Neighbors: []*NearestNeighbor{ | |
{ | |
Concept: "bar", | |
Distance: 0.1, | |
}, | |
}, | |
}, | |
"interpretation": &Interpretation{ | |
Source: []*InterpretationSource{ | |
{ | |
Concept: "bar", | |
Weight: 0.456, | |
Occurrence: 456, | |
}, | |
}, | |
}, | |
}, | |
}, res[1]) | |
}) | |
}) | |
} | |
func Test_Explorer_GetClass_With_Modules(t *testing.T) { | |
t.Run("when an explore param is set for nearCustomText", func(t *testing.T) { | |
params := dto.GetParams{ | |
ClassName: "BestClass", | |
ModuleParams: map[string]interface{}{ | |
"nearCustomText": extractNearCustomTextParam(map[string]interface{}{ | |
"concepts": []interface{}{"foo"}, | |
}), | |
}, | |
Pagination: &filters.Pagination{Limit: 100}, | |
Filters: nil, | |
} | |
searchResults := []search.Result{ | |
{ | |
ID: "id1", | |
Schema: map[string]interface{}{ | |
"name": "Foo", | |
}, | |
Dims: 128, | |
}, | |
{ | |
ID: "id2", | |
Schema: map[string]interface{}{ | |
"age": 200, | |
}, | |
Dims: 128, | |
}, | |
} | |
search := &fakeVectorSearcher{} | |
log, _ := test.NewNullLogger() | |
metrics := &fakeMetrics{} | |
explorer := NewExplorer(search, log, getFakeModulesProvider(), metrics, defaultConfig) | |
explorer.SetSchemaGetter(&fakeSchemaGetter{ | |
schema: schema.Schema{Objects: &models.Schema{Classes: []*models.Class{ | |
{Class: "BestClass"}, | |
}}}, | |
}) | |
expectedParamsToSearch := params | |
expectedParamsToSearch.SearchVector = []float32{1, 2, 3} | |
search. | |
On("VectorSearch", expectedParamsToSearch). | |
Return(searchResults, nil) | |
metrics.On("AddUsageDimensions", "BestClass", "get_graphql", "nearCustomText", 128) | |
res, err := explorer.GetClass(context.Background(), params) | |
t.Run("vector search must be called with right params", func(t *testing.T) { | |
assert.Nil(t, err) | |
search.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", | |
}, res[0]) | |
assert.Equal(t, | |
map[string]interface{}{ | |
"age": 200, | |
}, res[1]) | |
}) | |
}) | |
t.Run("when an explore param is set for nearCustomText and the required distance not met", | |
func(t *testing.T) { | |
params := dto.GetParams{ | |
ClassName: "BestClass", | |
ModuleParams: map[string]interface{}{ | |
"nearCustomText": extractNearCustomTextParam(map[string]interface{}{ | |
"concepts": []interface{}{"foo"}, | |
"distance": float64(0.2), | |
}), | |
}, | |
Pagination: &filters.Pagination{Limit: 100}, | |
Filters: nil, | |
} | |
searchResults := []search.Result{ | |
{ | |
ID: "id1", | |
Dist: 2 * 0.69, | |
Dims: 128, | |
}, | |
{ | |
ID: "id2", | |
Dist: 2 * 0.69, | |
Dims: 128, | |
}, | |
} | |
search := &fakeVectorSearcher{} | |
log, _ := test.NewNullLogger() | |
metrics := &fakeMetrics{} | |
explorer := NewExplorer(search, log, getFakeModulesProvider(), metrics, defaultConfig) | |
explorer.SetSchemaGetter(&fakeSchemaGetter{ | |
schema: schema.Schema{Objects: &models.Schema{Classes: []*models.Class{ | |
{Class: "BestClass"}, | |
}}}, | |
}) | |
expectedParamsToSearch := params | |
expectedParamsToSearch.SearchVector = []float32{1, 2, 3} | |
search. | |
On("VectorSearch", expectedParamsToSearch). | |
Return(searchResults, nil) | |
metrics.On("AddUsageDimensions", "BestClass", "get_graphql", "nearCustomText", 128) | |
res, err := explorer.GetClass(context.Background(), params) | |
t.Run("vector search must be called with right params", func(t *testing.T) { | |
assert.Nil(t, err) | |
search.AssertExpectations(t) | |
}) | |
t.Run("no object met the required distance", func(t *testing.T) { | |
assert.Len(t, res, 0) | |
}) | |
}) | |
t.Run("when an explore param is set for nearCustomText and the required certainty not met", | |
func(t *testing.T) { | |
params := dto.GetParams{ | |
ClassName: "BestClass", | |
ModuleParams: map[string]interface{}{ | |
"nearCustomText": extractNearCustomTextParam(map[string]interface{}{ | |
"concepts": []interface{}{"foo"}, | |
"certainty": float64(0.8), | |
}), | |
}, | |
Pagination: &filters.Pagination{Limit: 100}, | |
Filters: nil, | |
} | |
searchResults := []search.Result{ | |
{ | |
ID: "id1", | |
Dist: 2 * 0.69, | |
Dims: 128, | |
}, | |
{ | |
ID: "id2", | |
Dist: 2 * 0.69, | |
Dims: 128, | |
}, | |
} | |
search := &fakeVectorSearcher{} | |
log, _ := test.NewNullLogger() | |
metrics := &fakeMetrics{} | |
explorer := NewExplorer(search, log, getFakeModulesProvider(), metrics, defaultConfig) | |
explorer.SetSchemaGetter(&fakeSchemaGetter{ | |
schema: schema.Schema{Objects: &models.Schema{Classes: []*models.Class{ | |
{Class: "BestClass"}, | |
}}}, | |
}) | |
expectedParamsToSearch := params | |
expectedParamsToSearch.SearchVector = []float32{1, 2, 3} | |
search. | |
On("VectorSearch", expectedParamsToSearch). | |
Return(searchResults, nil) | |
metrics.On("AddUsageDimensions", "BestClass", "get_graphql", "nearCustomText", 128) | |
res, err := explorer.GetClass(context.Background(), params) | |
t.Run("vector search must be called with right params", func(t *testing.T) { | |
assert.Nil(t, err) | |
search.AssertExpectations(t) | |
}) | |
t.Run("no object met the required certainty", func(t *testing.T) { | |
assert.Len(t, res, 0) | |
}) | |
}) | |
t.Run("when two conflicting (nearVector, nearCustomText) near searchers are set", func(t *testing.T) { | |
params := dto.GetParams{ | |
ClassName: "BestClass", | |
Pagination: &filters.Pagination{Limit: 100}, | |
Filters: nil, | |
NearVector: &searchparams.NearVector{ | |
Vector: []float32{0.8, 0.2, 0.7}, | |
}, | |
ModuleParams: map[string]interface{}{ | |
"nearCustomText": extractNearCustomTextParam(map[string]interface{}{ | |
"concepts": []interface{}{"foo"}, | |
}), | |
}, | |
} | |
search := &fakeVectorSearcher{} | |
log, _ := test.NewNullLogger() | |
metrics := &fakeMetrics{} | |
explorer := NewExplorer(search, log, getFakeModulesProvider(), metrics, defaultConfig) | |
explorer.SetSchemaGetter(&fakeSchemaGetter{ | |
schema: schema.Schema{Objects: &models.Schema{Classes: []*models.Class{ | |
{Class: "BestClass"}, | |
}}}, | |
}) | |
_, err := explorer.GetClass(context.Background(), params) | |
require.NotNil(t, err) | |
assert.Contains(t, err.Error(), "parameters which are conflicting") | |
}) | |
t.Run("when two conflicting (nearCustomText, nearObject) near searchers are set", func(t *testing.T) { | |
params := dto.GetParams{ | |
ClassName: "BestClass", | |
Pagination: &filters.Pagination{Limit: 100}, | |
Filters: nil, | |
NearObject: &searchparams.NearObject{ | |
Beacon: "weaviate://localhost/e9c12c22-766f-4bde-b140-d4cf8fd6e041", | |
}, | |
ModuleParams: map[string]interface{}{ | |
"nearCustomText": extractNearCustomTextParam(map[string]interface{}{ | |
"concepts": []interface{}{"foo"}, | |
}), | |
}, | |
} | |
search := &fakeVectorSearcher{} | |
log, _ := test.NewNullLogger() | |
metrics := &fakeMetrics{} | |
explorer := NewExplorer(search, log, getFakeModulesProvider(), metrics, defaultConfig) | |
explorer.SetSchemaGetter(&fakeSchemaGetter{ | |
schema: schema.Schema{Objects: &models.Schema{Classes: []*models.Class{ | |
{Class: "BestClass"}, | |
}}}, | |
}) | |
_, err := explorer.GetClass(context.Background(), params) | |
require.NotNil(t, err) | |
assert.Contains(t, err.Error(), "parameters which are conflicting") | |
}) | |
t.Run("when three conflicting (nearCustomText, nearVector, nearObject) near searchers are set", func(t *testing.T) { | |
params := dto.GetParams{ | |
ClassName: "BestClass", | |
Pagination: &filters.Pagination{Limit: 100}, | |
Filters: nil, | |
NearVector: &searchparams.NearVector{ | |
Vector: []float32{0.8, 0.2, 0.7}, | |
}, | |
NearObject: &searchparams.NearObject{ | |
Beacon: "weaviate://localhost/e9c12c22-766f-4bde-b140-d4cf8fd6e041", | |
}, | |
ModuleParams: map[string]interface{}{ | |
"nearCustomText": extractNearCustomTextParam(map[string]interface{}{ | |
"concepts": []interface{}{"foo"}, | |
}), | |
}, | |
} | |
search := &fakeVectorSearcher{} | |
log, _ := test.NewNullLogger() | |
metrics := &fakeMetrics{} | |
explorer := NewExplorer(search, log, getFakeModulesProvider(), metrics, defaultConfig) | |
explorer.SetSchemaGetter(&fakeSchemaGetter{ | |
schema: schema.Schema{Objects: &models.Schema{Classes: []*models.Class{ | |
{Class: "BestClass"}, | |
}}}, | |
}) | |
_, err := explorer.GetClass(context.Background(), params) | |
require.NotNil(t, err) | |
assert.Contains(t, err.Error(), "parameters which are conflicting") | |
}) | |
t.Run("when nearCustomText.moveTo has no concepts and objects defined", func(t *testing.T) { | |
params := dto.GetParams{ | |
ClassName: "BestClass", | |
Pagination: &filters.Pagination{Limit: 100}, | |
Filters: nil, | |
ModuleParams: map[string]interface{}{ | |
"nearCustomText": extractNearCustomTextParam(map[string]interface{}{ | |
"concepts": []interface{}{"foo"}, | |
"moveTo": map[string]interface{}{ | |
"force": float64(0.1), | |
}, | |
}), | |
}, | |
} | |
search := &fakeVectorSearcher{} | |
log, _ := test.NewNullLogger() | |
metrics := &fakeMetrics{} | |
explorer := NewExplorer(search, log, getFakeModulesProvider(), metrics, defaultConfig) | |
explorer.SetSchemaGetter(&fakeSchemaGetter{ | |
schema: schema.Schema{Objects: &models.Schema{Classes: []*models.Class{ | |
{Class: "BestClass"}, | |
}}}, | |
}) | |
_, err := explorer.GetClass(context.Background(), params) | |
require.NotNil(t, err) | |
assert.Contains(t, err.Error(), "needs to have defined either 'concepts' or 'objects' fields") | |
}) | |
t.Run("when nearCustomText.moveAwayFrom has no concepts and objects defined", func(t *testing.T) { | |
params := dto.GetParams{ | |
ClassName: "BestClass", | |
Pagination: &filters.Pagination{Limit: 100}, | |
Filters: nil, | |
ModuleParams: map[string]interface{}{ | |
"nearCustomText": extractNearCustomTextParam(map[string]interface{}{ | |
"concepts": []interface{}{"foo"}, | |
"moveAwayFrom": map[string]interface{}{ | |
"force": float64(0.1), | |
}, | |
}), | |
}, | |
} | |
search := &fakeVectorSearcher{} | |
log, _ := test.NewNullLogger() | |
metrics := &fakeMetrics{} | |
explorer := NewExplorer(search, log, getFakeModulesProvider(), metrics, defaultConfig) | |
explorer.SetSchemaGetter(&fakeSchemaGetter{ | |
schema: schema.Schema{Objects: &models.Schema{Classes: []*models.Class{ | |
{Class: "BestClass"}, | |
}}}, | |
}) | |
explorer.SetSchemaGetter(&fakeSchemaGetter{ | |
schema: schema.Schema{Objects: &models.Schema{Classes: []*models.Class{ | |
{Class: "BestClass"}, | |
}}}, | |
}) | |
_, err := explorer.GetClass(context.Background(), params) | |
require.NotNil(t, err) | |
assert.Contains(t, err.Error(), "needs to have defined either 'concepts' or 'objects' fields") | |
}) | |
t.Run("when the distance prop is set", func(t *testing.T) { | |
params := dto.GetParams{ | |
Filters: nil, | |
ClassName: "BestClass", | |
Pagination: &filters.Pagination{Limit: 100}, | |
SearchVector: []float32{1.0, 2.0, 3.0}, | |
AdditionalProperties: additional.Properties{ | |
Distance: true, | |
}, | |
ModuleParams: map[string]interface{}{ | |
"nearCustomText": extractNearCustomTextParam(map[string]interface{}{ | |
"concepts": []interface{}{"foobar"}, | |
"limit": 100, | |
"distance": float64(1.38), | |
}), | |
}, | |
} | |
searchResults := []search.Result{ | |
{ | |
ID: "id2", | |
Schema: map[string]interface{}{ | |
"age": 200, | |
}, | |
Vector: []float32{0.5, 1.5, 0.0}, | |
Dist: 2 * 0.69, | |
Dims: 128, | |
}, | |
} | |
search := &fakeVectorSearcher{} | |
log, _ := test.NewNullLogger() | |
metrics := &fakeMetrics{} | |
explorer := NewExplorer(search, log, getFakeModulesProvider(), metrics, defaultConfig) | |
explorer.SetSchemaGetter(&fakeSchemaGetter{ | |
schema: schema.Schema{Objects: &models.Schema{Classes: []*models.Class{ | |
{Class: "BestClass"}, | |
}}}, | |
}) | |
expectedParamsToSearch := params | |
expectedParamsToSearch.SearchVector = []float32{1.0, 2.0, 3.0} | |
// expectedParamsToSearch.SearchVector = nil | |
search. | |
On("VectorSearch", expectedParamsToSearch). | |
Return(searchResults, nil) | |
metrics.On("AddUsageDimensions", "BestClass", "get_graphql", "nearCustomText", 128) | |
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) | |
search.AssertExpectations(t) | |
}) | |
t.Run("response must contain concepts", func(t *testing.T) { | |
require.Len(t, res, 1) | |
resMap := res[0].(map[string]interface{}) | |
assert.Equal(t, 2, len(resMap)) | |
assert.Contains(t, resMap, "age") | |
assert.Equal(t, 200, resMap["age"]) | |
additionalMap := resMap["_additional"] | |
assert.Contains(t, additionalMap, "distance") | |
assert.InEpsilon(t, 1.38, additionalMap.(map[string]interface{})["distance"].(float32), 0.000001) | |
}) | |
}) | |
t.Run("when the certainty prop is set", func(t *testing.T) { | |
params := dto.GetParams{ | |
Filters: nil, | |
ClassName: "BestClass", | |
Pagination: &filters.Pagination{Limit: 100}, | |
SearchVector: []float32{1.0, 2.0, 3.0}, | |
AdditionalProperties: additional.Properties{ | |
Certainty: true, | |
}, | |
ModuleParams: map[string]interface{}{ | |
"nearCustomText": extractNearCustomTextParam(map[string]interface{}{ | |
"concepts": []interface{}{"foobar"}, | |
"limit": 100, | |
"certainty": float64(0.1), | |
}), | |
}, | |
} | |
searchResults := []search.Result{ | |
{ | |
ID: "id2", | |
Schema: map[string]interface{}{ | |
"age": 200, | |
}, | |
Vector: []float32{0.5, 1.5, 0.0}, | |
Dist: 2 * 0.69, | |
Dims: 128, | |
}, | |
} | |
search := &fakeVectorSearcher{} | |
log, _ := test.NewNullLogger() | |
metrics := &fakeMetrics{} | |
explorer := NewExplorer(search, log, getFakeModulesProvider(), metrics, defaultConfig) | |
explorer.SetSchemaGetter(&fakeSchemaGetter{ | |
schema: schema.Schema{Objects: &models.Schema{Classes: []*models.Class{ | |
{Class: "BestClass"}, | |
}}}, | |
}) | |
schemaGetter := newFakeSchemaGetter("BestClass") | |
schemaGetter.SetVectorIndexConfig(hnsw.UserConfig{Distance: "cosine"}) | |
explorer.schemaGetter = schemaGetter | |
expectedParamsToSearch := params | |
expectedParamsToSearch.SearchVector = []float32{1.0, 2.0, 3.0} | |
// expectedParamsToSearch.SearchVector = nil | |
search. | |
On("VectorSearch", expectedParamsToSearch). | |
Return(searchResults, nil) | |
metrics.On("AddUsageDimensions", "BestClass", "get_graphql", "nearCustomText", 128) | |
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) | |
search.AssertExpectations(t) | |
}) | |
t.Run("response must contain concepts", func(t *testing.T) { | |
require.Len(t, res, 1) | |
resMap := res[0].(map[string]interface{}) | |
assert.Equal(t, 2, len(resMap)) | |
assert.Contains(t, resMap, "age") | |
assert.Equal(t, 200, resMap["age"]) | |
additionalMap := resMap["_additional"] | |
assert.Contains(t, additionalMap, "certainty") | |
// Certainty is fixed to 0.69 in this mock | |
assert.InEpsilon(t, 0.31, additionalMap.(map[string]interface{})["certainty"], 0.000001) | |
}) | |
}) | |
t.Run("when the semanticPath prop is set", func(t *testing.T) { | |
params := dto.GetParams{ | |
ClassName: "BestClass", | |
Pagination: &filters.Pagination{Limit: 100}, | |
Filters: nil, | |
AdditionalProperties: additional.Properties{ | |
ModuleParams: map[string]interface{}{ | |
"semanticPath": getDefaultParam("semanticPath"), | |
}, | |
}, | |
ModuleParams: map[string]interface{}{ | |
"nearCustomText": extractNearCustomTextParam(map[string]interface{}{ | |
"concepts": []interface{}{"foobar"}, | |
}), | |
}, | |
} | |
searchResults := []search.Result{ | |
{ | |
ID: "id1", | |
Schema: map[string]interface{}{ | |
"name": "Foo", | |
}, | |
}, | |
{ | |
ID: "id2", | |
Schema: map[string]interface{}{ | |
"name": "Bar", | |
}, | |
}, | |
} | |
searcher := &fakeVectorSearcher{} | |
log, _ := test.NewNullLogger() | |
pathBuilder := &fakePathBuilder{ | |
returnArgs: []search.Result{ | |
{ | |
ID: "id1", | |
Dims: 128, | |
Schema: map[string]interface{}{ | |
"name": "Foo", | |
}, | |
AdditionalProperties: models.AdditionalProperties{ | |
"semanticPath": &SemanticPath{ | |
Path: []*SemanticPathElement{ | |
{ | |
Concept: "pathelem1", | |
DistanceToQuery: 0, | |
DistanceToResult: 2.1, | |
DistanceToPrevious: nil, | |
DistanceToNext: ptFloat32(0.5), | |
}, | |
{ | |
Concept: "pathelem2", | |
DistanceToQuery: 2.1, | |
DistanceToResult: 0, | |
DistanceToPrevious: ptFloat32(0.5), | |
DistanceToNext: nil, | |
}, | |
}, | |
}, | |
}, | |
}, | |
{ | |
ID: "id2", | |
Dims: 128, | |
Schema: map[string]interface{}{ | |
"name": "Bar", | |
}, | |
AdditionalProperties: models.AdditionalProperties{ | |
"semanticPath": &SemanticPath{ | |
Path: []*SemanticPathElement{ | |
{ | |
Concept: "pathelem1", | |
DistanceToQuery: 0, | |
DistanceToResult: 2.1, | |
DistanceToPrevious: nil, | |
DistanceToNext: ptFloat32(0.5), | |
}, | |
{ | |
Concept: "pathelem2", | |
DistanceToQuery: 2.1, | |
DistanceToResult: 0, | |
DistanceToPrevious: ptFloat32(0.5), | |
DistanceToNext: nil, | |
}, | |
}, | |
}, | |
}, | |
}, | |
}, | |
} | |
metrics := &fakeMetrics{} | |
explorer := NewExplorer(searcher, log, getFakeModulesProviderWithCustomExtenders(nil, nil, pathBuilder), metrics, defaultConfig) | |
explorer.SetSchemaGetter(&fakeSchemaGetter{ | |
schema: schema.Schema{Objects: &models.Schema{Classes: []*models.Class{ | |
{Class: "BestClass"}, | |
}}}, | |
}) | |
expectedParamsToSearch := params | |
expectedParamsToSearch.SearchVector = []float32{1, 2, 3} | |
expectedParamsToSearch.AdditionalProperties.Vector = true // any custom additional params will trigger vector | |
searcher. | |
On("VectorSearch", expectedParamsToSearch). | |
Return(searchResults, nil) | |
metrics.On("AddUsageDimensions", "BestClass", "get_graphql", "nearCustomText", 128) | |
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{}{ | |
"vector": []float32(nil), | |
"semanticPath": &SemanticPath{ | |
Path: []*SemanticPathElement{ | |
{ | |
Concept: "pathelem1", | |
DistanceToQuery: 0, | |
DistanceToResult: 2.1, | |
DistanceToPrevious: nil, | |
DistanceToNext: ptFloat32(0.5), | |
}, | |
{ | |
Concept: "pathelem2", | |
DistanceToQuery: 2.1, | |
DistanceToResult: 0, | |
DistanceToPrevious: ptFloat32(0.5), | |
DistanceToNext: nil, | |
}, | |
}, | |
}, | |
}, | |
}, res[0]) | |
assert.Equal(t, | |
map[string]interface{}{ | |
"name": "Bar", | |
"_additional": map[string]interface{}{ | |
"vector": []float32(nil), | |
"semanticPath": &SemanticPath{ | |
Path: []*SemanticPathElement{ | |
{ | |
Concept: "pathelem1", | |
DistanceToQuery: 0, | |
DistanceToResult: 2.1, | |
DistanceToPrevious: nil, | |
DistanceToNext: ptFloat32(0.5), | |
}, | |
{ | |
Concept: "pathelem2", | |
DistanceToQuery: 2.1, | |
DistanceToResult: 0, | |
DistanceToPrevious: ptFloat32(0.5), | |
DistanceToNext: nil, | |
}, | |
}, | |
}, | |
}, | |
}, res[1]) | |
}) | |
}) | |
} | |
func ptFloat32(in float32) *float32 { | |
return &in | |
} | |
type fakeModulesProvider struct { | |
customC11yModule *fakeText2vecContextionaryModule | |
} | |
func (p *fakeModulesProvider) VectorFromInput(ctx context.Context, className string, input string) ([]float32, error) { | |
panic("not implemented") | |
} | |
func (p *fakeModulesProvider) VectorFromSearchParam(ctx context.Context, className, | |
param string, params interface{}, | |
findVectorFn modulecapabilities.FindVectorFn, tenant string, | |
) ([]float32, error) { | |
txt2vec := p.getFakeT2Vec() | |
vectorForParams := txt2vec.VectorSearches()["nearCustomText"] | |
return vectorForParams(ctx, params, "", findVectorFn, nil) | |
} | |
func (p *fakeModulesProvider) CrossClassVectorFromSearchParam(ctx context.Context, | |
param string, params interface{}, | |
findVectorFn modulecapabilities.FindVectorFn, | |
) ([]float32, error) { | |
txt2vec := p.getFakeT2Vec() | |
vectorForParams := txt2vec.VectorSearches()["nearCustomText"] | |
return vectorForParams(ctx, params, "", findVectorFn, nil) | |
} | |
func (p *fakeModulesProvider) CrossClassValidateSearchParam(name string, value interface{}) error { | |
return p.ValidateSearchParam(name, value, "") | |
} | |
func (p *fakeModulesProvider) ValidateSearchParam(name string, value interface{}, className string) error { | |
txt2vec := p.getFakeT2Vec() | |
arg := txt2vec.Arguments()["nearCustomText"] | |
return arg.ValidateFunction(value) | |
} | |
func (p *fakeModulesProvider) GetExploreAdditionalExtend(ctx context.Context, in []search.Result, | |
moduleParams map[string]interface{}, searchVector []float32, | |
argumentModuleParams map[string]interface{}, | |
) ([]search.Result, error) { | |
return p.additionalExtend(ctx, in, moduleParams, searchVector, "ExploreGet") | |
} | |
func (p *fakeModulesProvider) ListExploreAdditionalExtend(ctx context.Context, in []search.Result, | |
moduleParams map[string]interface{}, | |
argumentModuleParams map[string]interface{}, | |
) ([]search.Result, error) { | |
return p.additionalExtend(ctx, in, moduleParams, nil, "ExploreList") | |
} | |
func (p *fakeModulesProvider) additionalExtend(ctx context.Context, | |
in search.Results, moduleParams map[string]interface{}, | |
searchVector []float32, capability string, | |
) (search.Results, error) { | |
txt2vec := p.getFakeT2Vec() | |
if additionalProperties := txt2vec.AdditionalProperties(); len(additionalProperties) > 0 { | |
for name, value := range moduleParams { | |
additionalPropertyFn := p.getAdditionalPropertyFn(additionalProperties[name], capability) | |
if additionalPropertyFn != nil && value != nil { | |
searchValue := value | |
if searchVectorValue, ok := value.(modulecapabilities.AdditionalPropertyWithSearchVector); ok { | |
searchVectorValue.SetSearchVector(searchVector) | |
searchValue = searchVectorValue | |
} | |
resArray, err := additionalPropertyFn(ctx, in, searchValue, nil, nil, nil) | |
if err != nil { | |
return nil, err | |
} | |
in = resArray | |
} else { | |
return nil, errors.Errorf("unknown capability: %s", name) | |
} | |
} | |
} | |
return in, nil | |
} | |
func (p *fakeModulesProvider) getAdditionalPropertyFn(additionalProperty modulecapabilities.AdditionalProperty, | |
capability string, | |
) modulecapabilities.AdditionalPropertyFn { | |
switch capability { | |
case "ObjectGet": | |
return additionalProperty.SearchFunctions.ObjectGet | |
case "ObjectList": | |
return additionalProperty.SearchFunctions.ObjectList | |
case "ExploreGet": | |
return additionalProperty.SearchFunctions.ExploreGet | |
case "ExploreList": | |
return additionalProperty.SearchFunctions.ExploreList | |
default: | |
return nil | |
} | |
} | |
func (p *fakeModulesProvider) getFakeT2Vec() *fakeText2vecContextionaryModule { | |
if p.customC11yModule != nil { | |
return p.customC11yModule | |
} | |
return &fakeText2vecContextionaryModule{} | |
} | |
func extractNearCustomTextParam(param map[string]interface{}) interface{} { | |
txt2vec := &fakeText2vecContextionaryModule{} | |
argument := txt2vec.Arguments()["nearCustomText"] | |
return argument.ExtractFunction(param) | |
} | |
func getDefaultParam(name string) interface{} { | |
switch name { | |
case "featureProjection": | |
return &fakeProjectorParams{} | |
case "semanticPath": | |
return &pathBuilderParams{} | |
case "nearestNeighbors": | |
return true | |
default: | |
return nil | |
} | |
} | |
func getFakeModulesProviderWithCustomExtenders( | |
customExtender *fakeExtender, | |
customProjector *fakeProjector, | |
customPathBuilder *fakePathBuilder, | |
) ModulesProvider { | |
return &fakeModulesProvider{ | |
newFakeText2vecContextionaryModuleWithCustomExtender(customExtender, customProjector, customPathBuilder), | |
} | |
} | |
func getFakeModulesProvider() ModulesProvider { | |
return &fakeModulesProvider{} | |
} | |