From 019186104f4ff3b7b3198cfe8c9e09e7f88c9160 Mon Sep 17 00:00:00 2001 From: shivaji-dgraph Date: Wed, 10 Apr 2024 17:47:47 +0530 Subject: [PATCH] add tests for graphql vector --- graphql/admin/admin.go | 72 ++------ graphql/admin/endpoints_ee.go | 143 --------------- graphql/e2e/auth/add_mutation_test.go | 147 +++++++++++++++ graphql/e2e/auth/debug_off/debugoff_test.go | 144 +++++++++++++++ graphql/e2e/common/admin.go | 14 -- graphql/e2e/common/common.go | 138 -------------- graphql/e2e/common/error_test.yaml | 7 +- graphql/e2e/directives/schema_response.json | 86 +-------- graphql/e2e/normal/schema_response.json | 86 +-------- graphql/resolve/mutation_test.go | 2 +- graphql/resolve/query_test.yaml | 165 +++++++++++++++++ graphql/resolve/schema.graphql | 14 ++ graphql/resolve/update_mutation_test.yaml | 7 +- graphql/resolve/validate_mutation_test.yaml | 6 +- graphql/schema/dgraph_schemagen_test.yml | 8 +- query/vector_graphql_test.go | 192 +++++++------------- 16 files changed, 564 insertions(+), 667 deletions(-) diff --git a/graphql/admin/admin.go b/graphql/admin/admin.go index 3fbd6e74256..5143e72f385 100644 --- a/graphql/admin/admin.go +++ b/graphql/admin/admin.go @@ -508,15 +508,11 @@ var ( "getGQLSchema": stdAdminQryMWs, // for queries and mutations related to User/Group, dgraph handles Guardian auth, // so no need to apply GuardianAuth Middleware - "queryUser": minimalAdminQryMWs, - "queryGroup": minimalAdminQryMWs, - "queryEmbeddingSpec": minimalAdminQryMWs, - "queryModelSpec": minimalAdminQryMWs, - "getUser": minimalAdminQryMWs, - "getCurrentUser": minimalAdminQryMWs, - "getGroup": minimalAdminQryMWs, - "getEmbeddingSpec": minimalAdminQryMWs, - "getModelSpec": minimalAdminQryMWs, + "queryUser": minimalAdminQryMWs, + "queryGroup": minimalAdminQryMWs, + "getUser": minimalAdminQryMWs, + "getCurrentUser": minimalAdminQryMWs, + "getGroup": minimalAdminQryMWs, } adminMutationMWConfig = map[string]resolve.MutationMiddlewares{ "backup": gogMutMWs, @@ -536,18 +532,12 @@ var ( "resetPassword": gogAclMutMWs, // for queries and mutations related to User/Group, dgraph handles Guardian auth, // so no need to apply GuardianAuth Middleware - "addUser": minimalAdminMutMWs, - "addGroup": minimalAdminMutMWs, - "addEmbeddingSpec": minimalAdminMutMWs, - "addModelSpec": minimalAdminMutMWs, - "updateUser": minimalAdminMutMWs, - "updateGroup": minimalAdminMutMWs, - "updateEmbeddingSpec": minimalAdminMutMWs, - "updateModelSpec": minimalAdminMutMWs, - "deleteUser": minimalAdminMutMWs, - "deleteGroup": minimalAdminMutMWs, - "deleteEmbeddingSpec": minimalAdminMutMWs, - "deleteModelSpec": minimalAdminMutMWs, + "addUser": minimalAdminMutMWs, + "addGroup": minimalAdminMutMWs, + "updateUser": minimalAdminMutMWs, + "updateGroup": minimalAdminMutMWs, + "deleteUser": minimalAdminMutMWs, + "deleteGroup": minimalAdminMutMWs, } // mainHealthStore stores the health of the main GraphQL server. mainHealthStore = &GraphQLHealthStore{} @@ -916,14 +906,6 @@ func (as *adminServer) addConnectedAdminResolvers() { func(q schema.Query) resolve.QueryResolver { return resolve.NewQueryResolver(qryRw, dgEx) }). - WithQueryResolver("queryEmbeddingSpec", - func(q schema.Query) resolve.QueryResolver { - return resolve.NewQueryResolver(qryRw, dgEx) - }). - WithQueryResolver("queryModelSpec", - func(q schema.Query) resolve.QueryResolver { - return resolve.NewQueryResolver(qryRw, dgEx) - }). WithQueryResolver("getGroup", func(q schema.Query) resolve.QueryResolver { return resolve.NewQueryResolver(qryRw, dgEx) @@ -936,14 +918,6 @@ func (as *adminServer) addConnectedAdminResolvers() { func(q schema.Query) resolve.QueryResolver { return resolve.NewQueryResolver(qryRw, dgEx) }). - WithQueryResolver("getEmbeddingSpec", - func(q schema.Query) resolve.QueryResolver { - return resolve.NewQueryResolver(qryRw, dgEx) - }). - WithQueryResolver("getModelSpec", - func(q schema.Query) resolve.QueryResolver { - return resolve.NewQueryResolver(qryRw, dgEx) - }). WithMutationResolver("addUser", func(m schema.Mutation) resolve.MutationResolver { return resolve.NewDgraphResolver(resolve.NewAddRewriter(), dgEx) @@ -952,14 +926,6 @@ func (as *adminServer) addConnectedAdminResolvers() { func(m schema.Mutation) resolve.MutationResolver { return resolve.NewDgraphResolver(NewAddGroupRewriter(), dgEx) }). - WithMutationResolver("addEmbeddingSpec", - func(m schema.Mutation) resolve.MutationResolver { - return resolve.NewDgraphResolver(resolve.NewAddRewriter(), dgEx) - }). - WithMutationResolver("addModelSpec", - func(m schema.Mutation) resolve.MutationResolver { - return resolve.NewDgraphResolver(resolve.NewAddRewriter(), dgEx) - }). WithMutationResolver("updateUser", func(m schema.Mutation) resolve.MutationResolver { return resolve.NewDgraphResolver(resolve.NewUpdateRewriter(), dgEx) @@ -968,27 +934,11 @@ func (as *adminServer) addConnectedAdminResolvers() { func(m schema.Mutation) resolve.MutationResolver { return resolve.NewDgraphResolver(NewUpdateGroupRewriter(), dgEx) }). - WithMutationResolver("updateEmbeddingSpec", - func(m schema.Mutation) resolve.MutationResolver { - return resolve.NewDgraphResolver(resolve.NewUpdateRewriter(), dgEx) - }). - WithMutationResolver("updateModelSpec", - func(m schema.Mutation) resolve.MutationResolver { - return resolve.NewDgraphResolver(resolve.NewUpdateRewriter(), dgEx) - }). WithMutationResolver("deleteUser", func(m schema.Mutation) resolve.MutationResolver { return resolve.NewDgraphResolver(resolve.NewDeleteRewriter(), dgEx) }). WithMutationResolver("deleteGroup", - func(m schema.Mutation) resolve.MutationResolver { - return resolve.NewDgraphResolver(resolve.NewDeleteRewriter(), dgEx) - }). - WithMutationResolver("deleteEmbeddingSpec", - func(m schema.Mutation) resolve.MutationResolver { - return resolve.NewDgraphResolver(resolve.NewDeleteRewriter(), dgEx) - }). - WithMutationResolver("deleteModelSpec", func(m schema.Mutation) resolve.MutationResolver { return resolve.NewDgraphResolver(resolve.NewDeleteRewriter(), dgEx) }) diff --git a/graphql/admin/endpoints_ee.go b/graphql/admin/endpoints_ee.go index b460a2466e1..27d1e913a73 100644 --- a/graphql/admin/endpoints_ee.go +++ b/graphql/admin/endpoints_ee.go @@ -278,47 +278,6 @@ const adminTypes = ` groups: [Group] @dgraph(pred: "dgraph.user.group") } - type EmbeddingSpec @dgraph(type: "hypermode.type.EmbeddingSpec") { - """ - unique id of embedding spec based on entityType.predicate, Dgraph ensures uniqueness of id. - """ - id: String! @id @dgraph(pred: "hypermode.embedding.id") - """ - The type of object having this predicate. - """ - entityType: String! @dgraph(pred: "hypermode.embedding.entityType") - """ - The vector predicate computed by this embedding. - """ - predicate: String! @dgraph(pred: "hypermode.embedding.predicate") - """ - The model spec used to compute the embedding - """ - modelSpec: ModelSpec! @dgraph(pred: "hypermode.embedding.modelSpec") - """ - the query used to build the prompt for this embedding - """ - query: String! @dgraph(pred: "hypermode.embedding.query") - """ - the prompt used to build the embedding - """ - prompt: String @dgraph(pred: "hypermode.embedding.prompt") - } - - type ModelSpec @dgraph(type: "hypermode.type.ModelSpec") { - """ - unique id of the model, given from hypermode console. - """ - id: String! @id @dgraph(pred: "hypermode.model.id") - """ - Type of the model. - """ - type: String! @dgraph(pred: "hypermode.model.type") - """ - Endpoint of the model. - """ - endpoint: String! @dgraph(pred: "hypermode.model.endpoint") - } type Group @dgraph(type: "dgraph.type.Group") { @@ -375,21 +334,6 @@ const adminTypes = ` groups: [GroupRef] } - input AddEmbeddingSpecInput { - id: String! - entityType: String! - predicate: String! - query: String! - prompt: String! - modelSpec: ModelSpecRef! - } - - input AddModelSpecInput { - id: String! - type: String! - endpoint: String! - } - input AddGroupInput { name: String! rules: [RuleRef] @@ -436,26 +380,6 @@ const adminTypes = ` not: UserFilter } - input ModelSpecRef { - id: String! - type: String! - endpoint: String! - } - - input EmbeddingSpecFilter { - id: StringHashFilter - and: EmbeddingSpecFilter - or: EmbeddingSpecFilter - not: EmbeddingSpecFilter - } - - input ModelSpecFilter { - id: StringHashFilter - and: ModelSpecFilter - or: ModelSpecFilter - not: ModelSpecFilter - } - input UserOrder { asc: UserOrderable desc: UserOrderable @@ -473,33 +397,12 @@ const adminTypes = ` groups: [GroupRef] } - input EmbeddingSpecPatch { - query: String - prompt: String - modelSpec: ModelSpecRef - } - - input ModelSpecPatch { - type: String - endpoint: String - } - input UpdateUserInput { filter: UserFilter! set: UserPatch remove: UserPatch } - input UpdateEmbeddingSpecInput { - filter: EmbeddingSpecFilter! - set: EmbeddingSpecPatch - } - - input UpdateModelSpecInput { - filter: ModelSpecFilter! - set: ModelSpecPatch - } - input GroupFilter { name: StringHashFilter and: UserFilter @@ -524,14 +427,6 @@ const adminTypes = ` type AddUserPayload { user: [User] } - - type AddEmbeddingSpecPayload { - embeddingSpec: [EmbeddingSpec] - } - - type AddModelSpecPayload { - modelSpec: [ModelSpec] - } type AddGroupPayload { group: [Group] @@ -546,16 +441,6 @@ const adminTypes = ` msg: String numUids: Int } - - type DeleteEmbeddingSpecPayload { - msg: String - numUids: Int - } - - type DeleteModelSpecPayload { - msg: String - numUids: Int - } input AddNamespaceInput { """ @@ -631,16 +516,6 @@ const adminMutations = ` """ addUser(input: [AddUserInput!]!): AddUserPayload - """ - Add an embedding spec. You must pass in a full model spec object, as that isn't a supported resolver - """ - addEmbeddingSpec(input: [AddEmbeddingSpecInput!]!): AddEmbeddingSpecPayload - - """ - Add a model spec. - """ - addModelSpec(input: [AddModelSpecInput!]!): AddModelSpecPayload - """ Add a new group and (optionally) set the rules for the group. """ @@ -659,22 +534,8 @@ const adminMutations = ` """ updateGroup(input: UpdateGroupInput!): AddGroupPayload - """ - Update embedding specs, their queries, prompts and model specs. If the filter doesn't match - any embedding specs, the mutation has no effect. - """ - updateEmbeddingSpec(input: UpdateEmbeddingSpecInput!): AddEmbeddingSpecPayload - - """ - Update model specs, their types and endpoints. If the filter doesn't match - any model specs, the mutation has no effect. - """ - updateModelSpec(input: UpdateModelSpecInput!): AddModelSpecPayload - deleteGroup(filter: GroupFilter!): DeleteGroupPayload deleteUser(filter: UserFilter!): DeleteUserPayload - deleteEmbeddingSpec(filter: EmbeddingSpecFilter!): DeleteEmbeddingSpecPayload - deleteModelSpec(filter: ModelSpecFilter!): DeleteModelSpecPayload """ Add a new namespace. @@ -701,8 +562,6 @@ const adminMutations = ` const adminQueries = ` getUser(name: String!): User getGroup(name: String!): Group - getEmbeddingSpec(id: String!): EmbeddingSpec - getModelSpec(id: String!): ModelSpec """ Get the currently logged in user. @@ -711,8 +570,6 @@ const adminQueries = ` queryUser(filter: UserFilter, order: UserOrder, first: Int, offset: Int): [User] queryGroup(filter: GroupFilter, order: GroupOrder, first: Int, offset: Int): [Group] - queryEmbeddingSpec(filter: EmbeddingSpecFilter, first: Int, offset: Int): [EmbeddingSpec] - queryModelSpec(filter: ModelSpecFilter, first: Int, offset: Int): [ModelSpec] """ Get the information about the backups at a given location. diff --git a/graphql/e2e/auth/add_mutation_test.go b/graphql/e2e/auth/add_mutation_test.go index 70be41f6b32..ee465c6b50f 100644 --- a/graphql/e2e/auth/add_mutation_test.go +++ b/graphql/e2e/auth/add_mutation_test.go @@ -1164,3 +1164,150 @@ func TestAddMutationWithAuthOnIDFieldHavingInterfaceArg(t *testing.T) { // cleanup common.DeleteGqlType(t, "LibraryMember", map[string]interface{}{}, 1, nil) } + +func TestUpdateMutationWithIDFields(t *testing.T) { + addEmployerParams := &common.GraphQLParams{ + Query: `mutation addEmployer($input: [AddEmployerInput!]!) { + addEmployer(input: $input, upsert: false) { + numUids + } + }`, + Variables: map[string]interface{}{"input": []interface{}{ + map[string]interface{}{ + "company": "ABC tech", + "name": "ABC", + "worker": map[string]interface{}{ + "empId": "E01", + "regNo": 101, + }, + }, map[string]interface{}{ + "company": " XYZ tech", + "name": "XYZ", + "worker": map[string]interface{}{ + "empId": "E02", + "regNo": 102, + }, + }, + }, + }, + } + + gqlResponse := addEmployerParams.ExecuteAsPost(t, common.GraphqlURL) + common.RequireNoGQLErrors(t, gqlResponse) + var resultEmployer struct { + AddEmployer struct { + NumUids int + } + } + err := json.Unmarshal(gqlResponse.Data, &resultEmployer) + require.NoError(t, err) + require.Equal(t, 4, resultEmployer.AddEmployer.NumUids) + + // errors while updating node should be returned in debug mode, + // if type have auth rules defined on it + + tcases := []struct { + name string + query string + variables string + error string + }{{ + name: "update mutation gives error when multiple nodes are selected in filter", + query: `mutation update($patch: UpdateEmployerInput!) { + updateEmployer(input: $patch) { + numUids + } + }`, + variables: `{ + "patch": { + "filter": { + "name": { + "in": [ + "ABC", + "XYZ" + ] + } + }, + "set": { + "name": "MNO", + "company": "MNO tech" + } + } + }`, + error: "mutation updateEmployer failed because GraphQL debug: only one node is allowed" + + " in the filter while updating fields with @id directive", + }, { + name: "update mutation gives error when given @id field already exist in some node", + query: `mutation update($patch: UpdateEmployerInput!) { + updateEmployer(input: $patch) { + numUids + } + }`, + variables: `{ + "patch": { + "filter": { + "name": { + "in": "ABC" + } + }, + "set": { + "company": "ABC tech" + } + } + }`, + error: "couldn't rewrite mutation updateEmployer because failed to rewrite mutation" + + " payload because GraphQL debug: id ABC tech already exists for field company" + + " inside type Employer", + }, + { + name: "update mutation gives error when multiple nodes are found at nested level" + + "while linking rot object to nested object", + query: `mutation update($patch: UpdateEmployerInput!) { + updateEmployer(input: $patch) { + numUids + } + }`, + variables: `{ + "patch": { + "filter": { + "name": { + "in": "ABC" + } + }, + "set": { + "name": "JKL", + "worker":{ + "empId":"E01", + "regNo":102 + } + } + } + }`, + error: "couldn't rewrite mutation updateEmployer because failed to rewrite mutation" + + " payload because multiple nodes found for given xid values, updation not possible", + }, + } + + for _, tcase := range tcases { + t.Run(tcase.name, func(t *testing.T) { + var vars map[string]interface{} + if tcase.variables != "" { + err := json.Unmarshal([]byte(tcase.variables), &vars) + require.NoError(t, err) + } + params := &common.GraphQLParams{ + Query: tcase.query, + Variables: vars, + } + + resp := params.ExecuteAsPost(t, common.GraphqlURL) + require.Equal(t, tcase.error, resp.Errors[0].Error()) + }) + } + + // cleanup + filterEmployer := map[string]interface{}{"name": map[string]interface{}{"in": []string{"ABC", "XYZ"}}} + filterWorker := map[string]interface{}{"empId": map[string]interface{}{"in": []string{"E01", "E02"}}} + common.DeleteGqlType(t, "Employer", filterEmployer, 2, nil) + common.DeleteGqlType(t, "Worker", filterWorker, 2, nil) +} diff --git a/graphql/e2e/auth/debug_off/debugoff_test.go b/graphql/e2e/auth/debug_off/debugoff_test.go index b7715052f6e..6ab7a37b315 100644 --- a/graphql/e2e/auth/debug_off/debugoff_test.go +++ b/graphql/e2e/auth/debug_off/debugoff_test.go @@ -184,6 +184,150 @@ func TestAddMutationWithAuthOnIDFieldHavingInterfaceArg(t *testing.T) { common.DeleteGqlType(t, "LibraryMember", map[string]interface{}{}, 1, nil) } +func TestUpdateMutationWithIDFields(t *testing.T) { + addEmployerParams := &common.GraphQLParams{ + Query: `mutation addEmployer($input: [AddEmployerInput!]!) { + addEmployer(input: $input, upsert: false) { + numUids + } + }`, + Variables: map[string]interface{}{"input": []interface{}{ + map[string]interface{}{ + "company": "ABC tech", + "name": "ABC", + "worker": map[string]interface{}{ + "empId": "E01", + "regNo": 101, + }, + }, map[string]interface{}{ + "company": " XYZ tech", + "name": "XYZ", + "worker": map[string]interface{}{ + "empId": "E02", + "regNo": 102, + }, + }, + }, + }, + } + + gqlResponse := addEmployerParams.ExecuteAsPost(t, common.GraphqlURL) + common.RequireNoGQLErrors(t, gqlResponse) + type resEmployer struct { + AddEmployer struct { + NumUids int + } + } + var resultEmployer resEmployer + err := json.Unmarshal(gqlResponse.Data, &resultEmployer) + require.NoError(t, err) + require.Equal(t, 4, resultEmployer.AddEmployer.NumUids) + + // errors while updating node should be returned in debug mode, + // if type have auth rules defined on it + + tcases := []struct { + name string + query string + variables string + error string + }{{ + name: "update mutation gives error when multiple nodes are selected in filter", + query: `mutation update($patch: UpdateEmployerInput!) { + updateEmployer(input: $patch) { + numUids + } + }`, + variables: `{ + "patch": { + "filter": { + "name": { + "in": [ + "ABC", + "XYZ" + ] + } + }, + "set": { + "name": "MNO", + "company": "MNO tech" + } + } + }`, + }, { + name: "update mutation gives error when given @id field already exist in some node", + query: `mutation update($patch: UpdateEmployerInput!) { + updateEmployer(input: $patch) { + numUids + } + }`, + variables: `{ + "patch": { + "filter": { + "name": { + "in": "ABC" + } + }, + "set": { + "company": "ABC tech" + } + } + }`, + }, + { + name: "update mutation gives error when multiple nodes are found at nested level" + + "while linking rot object to nested object", + query: `mutation update($patch: UpdateEmployerInput!) { + updateEmployer(input: $patch) { + numUids + } + }`, + variables: `{ + "patch": { + "filter": { + "name": { + "in": "ABC" + } + }, + "set": { + "name": "JKL", + "worker":{ + "empId":"E01", + "regNo":102 + } + } + } + }`, + }, + } + + for _, tcase := range tcases { + t.Run(tcase.name, func(t *testing.T) { + var vars map[string]interface{} + var resultEmployerErr resEmployer + if tcase.variables != "" { + err := json.Unmarshal([]byte(tcase.variables), &vars) + require.NoError(t, err) + } + params := &common.GraphQLParams{ + Query: tcase.query, + Variables: vars, + } + + resp := params.ExecuteAsPost(t, common.GraphqlURL) + err := json.Unmarshal(resp.Data, &resultEmployerErr) + require.NoError(t, err) + require.Equal(t, 0, resultEmployerErr.AddEmployer.NumUids) + }) + } + + // cleanup + filterEmployer := map[string]interface{}{"name": map[string]interface{}{"in": []string{"ABC", "XYZ"}}} + filterWorker := map[string]interface{}{"empId": map[string]interface{}{"in": []string{"E01", "E02"}}} + common.DeleteGqlType(t, "Employer", filterEmployer, 2, nil) + common.DeleteGqlType(t, "Worker", filterWorker, 2, nil) +} + func TestMain(m *testing.M) { schemaFile := "../schema.graphql" schema, err := os.ReadFile(schemaFile) diff --git a/graphql/e2e/common/admin.go b/graphql/e2e/common/admin.go index 60860ff0f57..2113b7bba5e 100644 --- a/graphql/e2e/common/admin.go +++ b/graphql/e2e/common/admin.go @@ -182,9 +182,6 @@ func admin(t *testing.T) { updateSchemaThroughAdminSchemaEndpt(t, client) gqlSchemaNodeHasXid(t, client) - updateModelSpec(t, client) - updateEmbeddingSpec(t, client) - // restore the state to the initial schema and data. testutil.DropAll(t, client) @@ -229,17 +226,6 @@ func updateSchema(t *testing.T, client *dgo.Dgraph) { introspect(t, updatedIntrospectionResponse) } -func updateModelSpec(t *testing.T, client *dgo.Dgraph) { - err := addModelSpec(GraphqlAdminURL, "test modelId", "test modelEndpoint", "test modelType") - require.NoError(t, err) -} - -func updateEmbeddingSpec(t *testing.T, client *dgo.Dgraph) { - err := addEmbeddingSpec(GraphqlAdminURL, "test id", "test endpoint", "test model type", "test query", "test prompt", - "test modelId", "test modelType", "test modelEndpoint") - require.NoError(t, err) -} - func updateSchemaThroughAdminSchemaEndpt(t *testing.T, client *dgo.Dgraph) { assertUpdateGqlSchemaUsingAdminSchemaEndpt(t, Alpha1HTTP, adminSchemaEndptGqlSchema, nil) diff --git a/graphql/e2e/common/common.go b/graphql/e2e/common/common.go index f7777871402..635fe388437 100644 --- a/graphql/e2e/common/common.go +++ b/graphql/e2e/common/common.go @@ -1395,144 +1395,6 @@ func addSchema(url, schema string) error { return nil } -func addModelSpec(url, modelId, modelEndpoint, modelType string) error { - add := &GraphQLParams{ - Query: `mutation addModelSpec($id: String!, $endpoint: String!, $type: String!) { - addModelSpec(input: { id: $id, type: $type, endpoint: $endpoint }) { - modelSpec { - id - type - endpoint - } - } - }`, - Variables: map[string]interface{}{"id": modelId, "endpoint": modelEndpoint, "type": modelType}, - } - req, err := add.CreateGQLPost(url) - if err != nil { - return errors.Wrap(err, "error creating GraphQL query") - } - - resp, err := RunGQLRequest(req) - if err != nil { - return errors.Wrap(err, "error running GraphQL query") - } - - var addResult struct { - Data struct { - AddModelSpec struct { - ModelSpec []struct { - ID string - Type string - Endpoint string - } - } - } - Errors []interface{} - } - - err = json.Unmarshal(resp, &addResult) - if err != nil { - return errors.Wrap(err, "error trying to unmarshal GraphQL mutation result") - } - - if len(addResult.Errors) > 0 { - return errors.Errorf("%v", addResult.Errors...) - } - - if addResult.Data.AddModelSpec.ModelSpec[0].ID != modelId || - addResult.Data.AddModelSpec.ModelSpec[0].Type != modelType || - addResult.Data.AddModelSpec.ModelSpec[0].Endpoint != modelEndpoint { - return errors.New("GraphQL addModelSpec mutation failed") - } - - return nil -} - -func addEmbeddingSpec(url, id, entityType, predicate, query, prompt, modelId, modelType, modelEndpoint string) error { - add := &GraphQLParams{ - Query: `mutation addEmbeddingSpec($id: String!, $entityType: String!, $predicate: String!, $query: String!, - $prompt: String!, $modelId: String!, $modelType: String!, $modelEndpoint: String!) { - addEmbeddingSpec(input: { id: $id, entityType: $entityType, predicate: $predicate, query: $query, - prompt: $prompt, modelSpec: { id: $modelId, type: $modelType, endpoint: $modelEndpoint } }) { - embeddingSpec { - id - entityType - predicate - query - prompt - modelSpec { - id - type - endpoint - } - } - } - }`, - Variables: map[string]interface{}{ - "id": id, - "entityType": entityType, - "predicate": predicate, - "query": query, - "prompt": prompt, - "modelId": modelId, - "modelType": modelType, - "modelEndpoint": modelEndpoint, - }, - } - req, err := add.CreateGQLPost(url) - if err != nil { - return errors.Wrap(err, "error creating GraphQL query") - } - - resp, err := RunGQLRequest(req) - if err != nil { - return errors.Wrap(err, "error trying to unmarshal GraphQL mutation result") - } - - var addResult struct { - Data struct { - AddEmbeddingSpec struct { - EmbeddingSpec []struct { - ID string - EntityType string - Predicate string - Query string - Prompt string - ModelSpec struct { - ID string - Type string - Endpoint string - } - } - } - } - Errors []interface{} - } - - err = json.Unmarshal(resp, &addResult) - if err != nil { - return errors.Wrap(err, "error trying to unmarshal GraphQL mutation result") - } - - if len(addResult.Errors) > 0 { - return errors.Errorf("%v", addResult.Errors...) - } - - if addResult.Data.AddEmbeddingSpec.EmbeddingSpec[0].ID != id || - addResult.Data.AddEmbeddingSpec.EmbeddingSpec[0].EntityType != entityType || - addResult.Data.AddEmbeddingSpec.EmbeddingSpec[0].Predicate != predicate || - addResult.Data.AddEmbeddingSpec.EmbeddingSpec[0].Query != query || - addResult.Data.AddEmbeddingSpec.EmbeddingSpec[0].Prompt != prompt || - addResult.Data.AddEmbeddingSpec.EmbeddingSpec[0].ModelSpec.ID != modelId || - addResult.Data.AddEmbeddingSpec.EmbeddingSpec[0].ModelSpec.Type != modelType || - addResult.Data.AddEmbeddingSpec.EmbeddingSpec[0].ModelSpec.Endpoint != modelEndpoint { - return errors.New("GraphQL addEmbeddingSpec mutation failed") - } - - return nil -} - func GetJWT(t *testing.T, user, role interface{}, metaInfo *testutil.AuthMeta) http.Header { metaInfo.AuthVars = map[string]interface{}{} if user != nil { diff --git a/graphql/e2e/common/error_test.yaml b/graphql/e2e/common/error_test.yaml index b17cb0d4f91..d49ad424783 100644 --- a/graphql/e2e/common/error_test.yaml +++ b/graphql/e2e/common/error_test.yaml @@ -56,12 +56,13 @@ gqlvariables: | { "filter": "type was wrong" } errors: - [ { "message": "Variable type provided AuthorFiltarzzz! is incompatible with expected - type AuthorFilter", - "locations": [{ "line": 2, "column": 23}]}, + [ { "message": "Variable \"$filter\" of type \"AuthorFiltarzzz!\" used in position expecting type \"AuthorFilter\".", "locations": [ { "line": 2, "column": 23 } ] }, + { "message": "Variable type provided AuthorFiltarzzz! is incompatible with expected + type AuthorFilter", + "locations": [{ "line": 2, "column": 23}]}, { "message": "Unknown type \"AuthorFiltarzzz\".", "locations": [ { "line": 1, "column": 1 } ] } ] diff --git a/graphql/e2e/directives/schema_response.json b/graphql/e2e/directives/schema_response.json index 400ae11fbc3..683460f7da3 100644 --- a/graphql/e2e/directives/schema_response.json +++ b/graphql/e2e/directives/schema_response.json @@ -650,8 +650,7 @@ "tokenizer": [ "exact" ], - "upsert": true, - "unique":true + "upsert": true }, { "predicate": "dgraph.type", @@ -862,52 +861,6 @@ { "predicate": "职业", "type": "string" - }, - { - "predicate": "hypermode.embedding.entityType", - "type": "string" - }, - { - "predicate": "hypermode.embedding.id", - "type": "string", - "index": true, - "tokenizer": [ - "exact" - ], - "upsert": true - }, - { - "predicate":"hypermode.embedding.modelSpec", - "type":"uid" - }, - { - "predicate":"hypermode.embedding.predicate", - "type":"string" - }, - { - "predicate":"hypermode.embedding.prompt", - "type":"string" - }, - { - "predicate":"hypermode.embedding.query", - "type":"string" - }, - { - "predicate":"hypermode.model.endpoint", - "type":"string" - }, - { - "predicate": "hypermode.model.id", - "type": "string", - "index": true, - "tokenizer": [ - "exact" - ], - "upsert": true - }, - { - "predicate":"hypermode.model.type", - "type":"string" } ], "types": [ @@ -1691,43 +1644,6 @@ } ], "name": "LibraryMember" - }, - { - "fields": [ - { - "name": "hypermode.embedding.id" - }, - { - "name": "hypermode.embedding.entityType" - }, - { - "name": "hypermode.embedding.predicate" - }, - { - "name": "hypermode.embedding.modelSpec" - }, - { - "name": "hypermode.embedding.query" - }, - { - "name": "hypermode.embedding.prompt" - } - ], - "name": "hypermode.type.EmbeddingSpec" - }, - { - "fields": [ - { - "name": "hypermode.model.id" - }, - { - "name": "hypermode.model.type" - }, - { - "name": "hypermode.model.endpoint" - } - ], - "name": "hypermode.type.ModelSpec" } ] } diff --git a/graphql/e2e/normal/schema_response.json b/graphql/e2e/normal/schema_response.json index fd80364113f..ab1e9eabb3e 100644 --- a/graphql/e2e/normal/schema_response.json +++ b/graphql/e2e/normal/schema_response.json @@ -858,52 +858,6 @@ "trigram" ], "upsert": true - }, - { - "predicate": "hypermode.embedding.entityType", - "type": "string" - }, - { - "predicate": "hypermode.embedding.id", - "type": "string", - "index": true, - "tokenizer": [ - "exact" - ], - "upsert": true - }, - { - "predicate":"hypermode.embedding.modelSpec", - "type":"uid" - }, - { - "predicate":"hypermode.embedding.predicate", - "type":"string" - }, - { - "predicate":"hypermode.embedding.prompt", - "type":"string" - }, - { - "predicate":"hypermode.embedding.query", - "type":"string" - }, - { - "predicate":"hypermode.model.endpoint", - "type":"string" - }, - { - "predicate": "hypermode.model.id", - "type": "string", - "index": true, - "tokenizer": [ - "exact" - ], - "upsert": true - }, - { - "predicate":"hypermode.model.type", - "type":"string" } ], "types": [ @@ -1679,43 +1633,5 @@ } ], "name": "Person" - }, - { - "fields": [ - { - "name": "hypermode.embedding.id" - }, - { - "name": "hypermode.embedding.entityType" - }, - { - "name": "hypermode.embedding.predicate" - }, - { - "name": "hypermode.embedding.modelSpec" - }, - { - "name": "hypermode.embedding.query" - }, - { - "name": "hypermode.embedding.prompt" - } - ], - "name": "hypermode.type.EmbeddingSpec" - }, - { - "fields": [ - { - "name": "hypermode.model.id" - }, - { - "name": "hypermode.model.type" - }, - { - "name": "hypermode.model.endpoint" - } - ], - "name": "hypermode.type.ModelSpec" - } - ] + } ] } diff --git a/graphql/resolve/mutation_test.go b/graphql/resolve/mutation_test.go index 72faab552dd..a4b9903545c 100644 --- a/graphql/resolve/mutation_test.go +++ b/graphql/resolve/mutation_test.go @@ -104,7 +104,7 @@ func mutationValidation(t *testing.T, file string, rewriterFactory func() Mutati }) require.NotNil(t, err) - require.Equal(t, err.Error(), tcase.ValidationError.Error()) + require.Equal(t, tcase.ValidationError.Error(), err.Error()) }) } } diff --git a/graphql/resolve/query_test.yaml b/graphql/resolve/query_test.yaml index c281bc5932a..a8e972f8850 100644 --- a/graphql/resolve/query_test.yaml +++ b/graphql/resolve/query_test.yaml @@ -3353,3 +3353,168 @@ dgraph.uid : uid } } +- name: "query similar_to" + gqlquery: | + query { + querySimilarProductByEmbedding(by: productVector, topK: 1, vector: [0.1, 0.2, 0.3, 0.4, 0.5]) { + id + title + productVector + } + } + + dgquery: |- + query querySimilarProductByEmbedding($search_vector: float32vector = "[0.1,0.2,0.3,0.4,0.5]") { + var(func: similar_to(Product.productVector, 1, $search_vector)) @filter(type(Product)) { + v2 as Product.productVector + distance as math((v2 - $search_vector) dot (v2 - $search_vector)) + } + querySimilarProductByEmbedding(func: uid(distance), orderasc: val(distance)) { + Product.id : Product.id + Product.title : Product.title + Product.productVector : Product.productVector + dgraph.uid : uid + Product.hm_distance : val(distance) + } + } +- name: "query vector using uid" + gqlquery: | + query { + querySimilarProductById(by: productVector, topK: 3, id: "0x1") { + id + title + productVector + } + } + + dgquery: |- + query { + var(func: eq(Product.id, "0x1")) @filter(type(Product)) { + vec as Product.productVector + } + var() { + v1 as max(val(vec)) + } + var(func: similar_to(Product.productVector, 3, val(v1))) { + v2 as Product.productVector + distance as math((v2 - v1) dot (v2 - v1)) + } + querySimilarProductById(func: uid(distance), orderasc: val(distance)) { + Product.id : Product.id + Product.title : Product.title + Product.productVector : Product.productVector + dgraph.uid : uid + Product.hm_distance : val(distance) + } + } + +- name: "query vector by id with cosine distance" + gqlquery: | + query { + querySimilarProjectById(by: description_v, topK: 3, id: "0x1") { + id + title + description_v + } + } + + dgquery: |- + query { + var(func: eq(Project.id, "0x1")) @filter(type(Project)) { + vec as Project.description_v + } + var() { + v1 as max(val(vec)) + } + var(func: similar_to(Project.description_v, 3, val(v1))) { + v2 as Project.description_v + distance as math((v1 dot v2) / ((v1 dot v1) * (v2 dot v2))) + } + querySimilarProjectById(func: uid(distance), orderasc: val(distance)) { + Project.id : Project.id + Project.title : Project.title + Project.description_v : Project.description_v + dgraph.uid : uid + Project.hm_distance : val(distance) + } + } + +- name: "query similar_to with cosine distance" + gqlquery: | + query { + querySimilarProjectByEmbedding(by: description_v, topK: 1, vector: [0.1, 0.2, 0.3, 0.4, 0.5]) { + id + title + description_v + } + } + + dgquery: |- + query querySimilarProjectByEmbedding($search_vector: float32vector = "[0.1,0.2,0.3,0.4,0.5]") { + var(func: similar_to(Project.description_v, 1, $search_vector)) @filter(type(Project)) { + v2 as Project.description_v + distance as math(($search_vector dot v2) / (($search_vector dot $search_vector) * (v2 dot v2))) + } + querySimilarProjectByEmbedding(func: uid(distance), orderasc: val(distance)) { + Project.id : Project.id + Project.title : Project.title + Project.description_v : Project.description_v + dgraph.uid : uid + Project.hm_distance : val(distance) + } + } +- name: "query vector by id with dot product distance" + gqlquery: | + query { + querySimilarProject1ById(by: description_v, topK: 3, id: "0x1") { + id + title + description_v + } + } + + dgquery: |- + query { + var(func: eq(Project1.id, "0x1")) @filter(type(Project1)) { + vec as Project1.description_v + } + var() { + v1 as max(val(vec)) + } + var(func: similar_to(Project1.description_v, 3, val(v1))) { + v2 as Project1.description_v + distance as math(v1 dot v2) + } + querySimilarProject1ById(func: uid(distance), orderasc: val(distance)) { + Project1.id : Project1.id + Project1.title : Project1.title + Project1.description_v : Project1.description_v + dgraph.uid : uid + Project1.hm_distance : val(distance) + } + } + +- name: "query similar_to with dot product distance" + gqlquery: | + query { + querySimilarProject1ByEmbedding(by: description_v, topK: 1, vector: [0.1, 0.2, 0.3, 0.4, 0.5]) { + id + title + description_v + } + } + + dgquery: |- + query querySimilarProject1ByEmbedding($search_vector: float32vector = "[0.1,0.2,0.3,0.4,0.5]") { + var(func: similar_to(Project1.description_v, 1, $search_vector)) @filter(type(Project1)) { + v2 as Project1.description_v + distance as math($search_vector dot v2) + } + querySimilarProject1ByEmbedding(func: uid(distance), orderasc: val(distance)) { + Project1.id : Project1.id + Project1.title : Project1.title + Project1.description_v : Project1.description_v + dgraph.uid : uid + Project1.hm_distance : val(distance) + } + } \ No newline at end of file diff --git a/graphql/resolve/schema.graphql b/graphql/resolve/schema.graphql index e1fd242a6a1..bf79b1abbc9 100644 --- a/graphql/resolve/schema.graphql +++ b/graphql/resolve/schema.graphql @@ -505,3 +505,17 @@ type Product { imageUrl: String productVector: [Float!] @hm_embedding } + +type Project { + id: String! @id + description: String + title: String + description_v: [Float!] @hm_embedding @search(by: ["hnsw(metric: cosine, exponent: 4)"]) +} + +type Project1 { + id: String! @id + description: String + title: String + description_v: [Float!] @hm_embedding @search(by: ["hnsw(metric: dotproduct, exponent: 4)"]) +} diff --git a/graphql/resolve/update_mutation_test.yaml b/graphql/resolve/update_mutation_test.yaml index c38cefe1453..feb0ef85501 100644 --- a/graphql/resolve/update_mutation_test.yaml +++ b/graphql/resolve/update_mutation_test.yaml @@ -1979,7 +1979,6 @@ "Author.name": "Alice" } cond: "@if(gt(len(x), 0))" - - name: "Updating @id field when given values for @id fields doesn't exists" explaination: "We are giving two @id fields title and ISBN in set part of update mutation, @@ -2170,7 +2169,6 @@ "uid": "uid(x)" } cond: "@if(gt(len(x), 0))" - - name: "Updating link to nested field require all the non-null id's to be present in nested field" explaination: "when we update link to nested field then we check if that already exist or not, @@ -2242,7 +2240,6 @@ } } } - dgquery: |- query { LibraryMember_1(func: eq(Member.name, "Alice")) { @@ -2273,7 +2270,6 @@ "uid":"uid(x)" } cond: "@if(gt(len(x), 0))" - - name: "Updating inherited @id field with interface arg -2 " explaination: "For this case we will generate one more existence query for inherited @id field refID. @@ -2331,7 +2327,7 @@ explaination: "If nested object have inherited @id field which have interface argument set, and that field already exist in some other implementing type than we returns error.In below mutation manages is of type LibraryMember but node with given refID already exist in some other - type than LibraryMember" + type than than LibraryMember" gqlmutation: | mutation update($patch: UpdateLibraryManagerInput!) { updateLibraryManager(input: $patch) { @@ -2447,3 +2443,4 @@ "uid": "uid(x)" } cond: "@if(gt(len(x), 0))" + diff --git a/graphql/resolve/validate_mutation_test.yaml b/graphql/resolve/validate_mutation_test.yaml index 713ce8dbce0..abc54a5ef97 100644 --- a/graphql/resolve/validate_mutation_test.yaml +++ b/graphql/resolve/validate_mutation_test.yaml @@ -18,9 +18,9 @@ explanation: "Add mutation expects an array instead of an object" validationerror: { "message": - "input:2: Variable type provided AddAuthorInput! is incompatible with expected - type [AddAuthorInput!]!\ninput:2: Variable \"$auth\" of type \"AddAuthorInput!\" - used in position expecting type \"[AddAuthorInput!]!\".\n" } + "input:2: Variable \"$auth\" of type \"AddAuthorInput!\" used in position expecting + type \"[AddAuthorInput!]!\".\ninput:2: Variable type provided AddAuthorInput! is + incompatible with expected type [AddAuthorInput!]!\n" } - diff --git a/graphql/schema/dgraph_schemagen_test.yml b/graphql/schema/dgraph_schemagen_test.yml index f81ab3b5395..139c305e77c 100644 --- a/graphql/schema/dgraph_schemagen_test.yml +++ b/graphql/schema/dgraph_schemagen_test.yml @@ -218,10 +218,10 @@ schemas: X.dt3: dateTime @index(month) . X.dt4: dateTime @index(day) . X.dt5: dateTime @index(hour) . - X.vf1: vfloat @index(hnsw) . - X.vf2: vfloat @index(hnsw(exponent: "4", metric: "euclidian")) . - X.vf3: vfloat @index(hnsw(metric: "cosine")) . - X.vf4: vfloat @index(hnsw(exponent: "4", metric: "dotproduct")) . + X.vf1: float32vector @index(hnsw) . + X.vf2: float32vector @index(hnsw(exponent: "4", metric: "euclidian")) . + X.vf3: float32vector @index(hnsw(metric: "cosine")) . + X.vf4: float32vector @index(hnsw(exponent: "4", metric: "dotproduct")) . X.e: string @index(hash) . X.e1: string @index(hash) . X.e2: string @index(exact) . diff --git a/query/vector_graphql_test.go b/query/vector_graphql_test.go index a5d139acd5a..a53f1bbe26e 100644 --- a/query/vector_graphql_test.go +++ b/query/vector_graphql_test.go @@ -20,7 +20,6 @@ package query import ( "encoding/json" - "fmt" "testing" "github.com/dgraph-io/dgraph/dgraphtest" @@ -28,173 +27,116 @@ import ( ) const ( - exampleSchema = ` - type Project { - id: ID! - title: String! @search(by: [term]) - grade: String @search(by: [hash]) - category: Category - score: Float - title_v: [Float!] @hm_embedding @search(by: ["hnsw(metric: euclidian, exponent: 4)"]) - } - - type Category { - id: ID! - name: String! - name_v: [Float!] @hm_embedding @search(by: ["hnsw(metric: euclidian, exponent: 4)"]) - }` - - categories = `mutation { - addCategory( - input: [ - { name: "Math & Science", name_v: [0.1, 0.2, 0.3, 0.4, 0.5] } - { name: "Health & Sports", name_v: [0.6, 0.7, 0.8, 0.9, 0.10] } - { name: "History & Civics", name_v: [0.11, 0.12, 0.13, 0.14, 0.15] } - { name: "Literacy & Language", name_v: [0.16, 0.17, 0.18, 0.19, 0.20] } - { name: "Music & The Arts", name_v: [0.21, 0.22, 0.23, 0.24, 0.25] } - ] - ) { - category { - id - name - name_v - } - } - }` + graphQLVectorSchema = ` + type Project { + id: ID! + title: String! @search(by: [exact]) + title_v: [Float!] @hm_embedding @search(by: ["hnsw(metric: euclidian, exponent: 4)"]) + } + ` ) type ProjectInput struct { - Title string `json:"title"` - TitleV []float32 `json:"title_v"` - Category *Category `json:"category"` + Title string `json:"title"` + TitleV []float32 `json:"title_v"` } -type Category struct { - Name string `json:"name"` - NameV []float32 `json:"name_v"` -} +func TestVectorGraphQLAddVectorPredicate(t *testing.T) { + require.NoError(t, client.DropAll()) -func addProject(t *testing.T, project ProjectInput, hc *dgraphtest.HTTPClient) { - - // add category if not exists already - if project.Category == nil { - // here we are querying the category by the vector of the project - queryCategory := fmt.Sprintf(`query { - querySimilarCategoryByEmbedding( - by: name_v - topK: 1 - vector: %v - ) { - name - name_v - } - } - `, project.TitleV) + hc, err := dc.HTTPClient() + require.NoError(t, err) + hc.LoginIntoNamespace("groot", "password", 0) + // add schema + require.NoError(t, hc.UpdateGQLSchema(graphQLVectorSchema)) +} - params := dgraphtest.GraphQLParams{ - Query: queryCategory, - } - response, err := hc.RunGraphqlQuery(params, false) - require.NoError(t, err) +func TestVectorGraphQlMutationAndQuery(t *testing.T) { + require.NoError(t, client.DropAll()) - type CategoryResponse struct { - QuerySimilarCategoryByEmbedding []Category `json:"querySimilarCategoryByEmbedding"` - } + hc, err := dc.HTTPClient() + require.NoError(t, err) + hc.LoginIntoNamespace("groot", "password", 0) - var resp CategoryResponse - err = json.Unmarshal([]byte(string(response)), &resp) - require.NoError(t, err) + // add schema + require.NoError(t, hc.UpdateGQLSchema(graphQLVectorSchema)) - data := resp.QuerySimilarCategoryByEmbedding[0] - project.Category = &Category{Name: data.Name, NameV: data.NameV} + // add project + project := ProjectInput{ + Title: "iCreate with a Mini iPad", + TitleV: []float32{0.1, 0.2, 0.3, 0.4, 0.5}, } - variables := make(map[string]interface{}) - variables["project"] = project - query := ` mutation addProject($project: AddProjectInput!) { addProject(input: [$project]) { project { title - category{ - name - } + title_v } } }` params := dgraphtest.GraphQLParams{ Query: query, - Variables: variables, + Variables: map[string]interface{}{"project": project}, } - _, err := hc.RunGraphqlQuery(params, false) - require.NoError(t, err) -} - -func TestVectorExample(t *testing.T) { - require.NoError(t, client.DropAll()) - - hc, err := dc.HTTPClient() + _, err = hc.RunGraphqlQuery(params, false) require.NoError(t, err) - hc.LoginIntoNamespace("groot", "password", 0) - - // add schema - require.NoError(t, hc.UpdateGQLSchema(exampleSchema)) - - // add categories - query := dgraphtest.GraphQLParams{ - Query: categories, - } - response, err := hc.RunGraphqlQuery(query, false) - require.NoError(t, err) - - // adding project which vector similar to the category math & science - project := ProjectInput{ - Title: "iCreate with a Mini iPad", - TitleV: []float32{0.1, 0.2, 0.3, 0.4, 0.5}, - } - - addProject(t, project, hc) - - // query similar project by embedding - // it should return the project with title "iCreate with a Mini iPad" and category "Math & Science" + // query project queryProduct := `query { - querySimilarProjectByEmbedding( - by: title_v - topK: 1 - vector: [0.1, 0.2, 0.3, 0.4, 0.5] - ) { - id + queryProject(filter: { title: { eq: "iCreate with a Mini iPad" } }) { title title_v - category{ - name - name_v - } } } ` - query = dgraphtest.GraphQLParams{ + params = dgraphtest.GraphQLParams{ Query: queryProduct, } - response, err = hc.RunGraphqlQuery(query, false) + response, err := hc.RunGraphqlQuery(params, false) require.NoError(t, err) type QueryResult struct { - QuerySimilarProjectByEmbedding []ProjectInput `json:"querySimilarProjectByEmbedding"` + QueryProject []ProjectInput `json:"queryProject"` } var resp QueryResult err = json.Unmarshal([]byte(string(response)), &resp) require.NoError(t, err) + require.Equal(t, project.Title, resp.QueryProject[0].Title) + require.Equal(t, project.TitleV, resp.QueryProject[0].TitleV) - // fmt.Println("Project :", string(response)) + // query similar project by embedding + queryProduct = `query { + querySimilarProjectByEmbedding( + by: title_v + topK: 1 + vector: [0.1, 0.2, 0.3, 0.4, 0.5] + ) { + id + title + title_v + } + } + ` + + params = dgraphtest.GraphQLParams{ + Query: queryProduct, + } + response, err = hc.RunGraphqlQuery(params, false) + require.NoError(t, err) + + type QueryResult1 struct { + QuerySimilarProjectByEmbedding []ProjectInput `json:"querySimilarProjectByEmbedding"` + } + + var resp1 QueryResult1 + err = json.Unmarshal([]byte(string(response)), &resp1) + require.NoError(t, err) - fmt.Println("Project Title:", resp.QuerySimilarProjectByEmbedding[0].Title) - fmt.Println("Project Vector:", resp.QuerySimilarProjectByEmbedding[0].TitleV) - fmt.Println("Project Category Name:", resp.QuerySimilarProjectByEmbedding[0].Category.Name) - fmt.Println("Project Category Vector ", resp.QuerySimilarProjectByEmbedding[0].Category.NameV) + require.Equal(t, project.Title, resp1.QuerySimilarProjectByEmbedding[0].Title) + require.Equal(t, project.TitleV, resp1.QuerySimilarProjectByEmbedding[0].TitleV) }