From d8ec69f78cca0a04c0ef9643a731552d7aa7af29 Mon Sep 17 00:00:00 2001 From: Marcin Antas Date: Tue, 20 Apr 2021 14:05:06 +0200 Subject: [PATCH] gh-20 Add support for nearObject argument --- docker-compose.yml | 7 ++- graphql/explorer.js | 14 ++++++ graphql/getter.js | 15 ++++++ graphql/getter.test.js | 100 ++++++++++++++++++++++++++++++++++++++++ graphql/journey.test.js | 13 ++++++ graphql/nearObject.js | 77 +++++++++++++++++++++++++++++++ schema/journey.test.js | 52 +++++++++++++++++++-- 7 files changed, 271 insertions(+), 7 deletions(-) create mode 100644 graphql/nearObject.js diff --git a/docker-compose.yml b/docker-compose.yml index 779bcb2..7fc3833 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ version: '3.4' services: weaviate: - image: semitechnologies/weaviate:1.0.0-rc1 + image: semitechnologies/weaviate:1.2.1 restart: on-failure:0 ports: - "8080:8080" @@ -11,11 +11,14 @@ services: AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED: 'true' PERSISTENCE_DATA_PATH: "./weaviate-data" DEFAULT_VECTORIZER_MODULE: text2vec-contextionary + ENABLE_MODULES: text2vec-contextionary contextionary: - image: semitechnologies/contextionary:en0.16.0-v0.4.21 + image: semitechnologies/contextionary:en0.16.0-v1.0.2 ports: - "9999:9999" environment: OCCURRENCE_WEIGHT_LINEAR_FACTOR: 0.75 EXTENSIONS_STORAGE_MODE: weaviate EXTENSIONS_STORAGE_ORIGIN: http://weaviate:8080 + NEIGHBOR_OCCURRENCE_IGNORE_PERCENTILE: 5 + ENABLE_COMPOUND_SPLITTING: 'false' diff --git a/graphql/explorer.js b/graphql/explorer.js index aeacc4a..0316101 100644 --- a/graphql/explorer.js +++ b/graphql/explorer.js @@ -1,5 +1,6 @@ import NearText from "./nearText"; import NearVector from "./nearVector"; +import NearObject from "./nearObject"; import { DEFAULT_KIND, validateKind } from "../kinds"; export default class Explorer { @@ -28,6 +29,15 @@ export default class Explorer { return this; }; + withNearObject = (nearObjectObj) => { + try { + this.nearObjectString = new NearObject(nearObjectObj).toString(); + } catch (e) { + this.errors = [...this.errors, e]; + } + return this; + }; + withNearVector = (nearVectorObj) => { try { this.nearVectorString = new NearVector(nearVectorObj).toString(); @@ -85,6 +95,10 @@ export default class Explorer { args = [...args, `nearText:${this.nearTextString}`]; } + if (this.nearObjectString) { + args = [...args, `nearObject:${this.nearObjectString}`]; + } + if (this.nearVectorString) { args = [...args, `nearVector:${this.nearVectorString}`]; } diff --git a/graphql/getter.js b/graphql/getter.js index 7528fe1..4d7c74b 100644 --- a/graphql/getter.js +++ b/graphql/getter.js @@ -1,6 +1,7 @@ import Where from "./where"; import NearText from "./nearText"; import NearVector from "./nearVector"; +import NearObject from "./nearObject"; import Group from "./group"; export default class Getter { @@ -47,6 +48,15 @@ export default class Getter { return this; }; + withNearObject = (nearObjectObj) => { + try { + this.nearObjectString = new NearObject(nearObjectObj).toString(); + } catch (e) { + this.errors = [...this.errors, e]; + } + return this; + }; + withNearVector = (nearVectorObj) => { try { this.nearVectorString = new NearVector(nearVectorObj).toString(); @@ -92,6 +102,7 @@ export default class Getter { if ( this.whereString || this.nearTextString || + this.nearObjectString || this.nearVectorString || this.limit || this.groupString @@ -106,6 +117,10 @@ export default class Getter { args = [...args, `nearText:${this.nearTextString}`]; } + if (this.nearObjectString) { + args = [...args, `nearObject:${this.nearObjectString}`]; + } + if (this.nearVectorString) { args = [...args, `nearVector:${this.nearVectorString}`]; } diff --git a/graphql/getter.test.js b/graphql/getter.test.js index 605d743..98fb474 100644 --- a/graphql/getter.test.js +++ b/graphql/getter.test.js @@ -370,3 +370,103 @@ describe("nearVector searchers", () => { }); }); }); + +describe("nearObject searchers", () => { + test("a query with a valid nearObject with id", () => { + const mockClient = { + query: jest.fn(), + }; + + const expectedQuery = + `{Get{Person` + `(nearObject:{id:"some-uuid"})` + `{name}}}`; + + new Getter(mockClient) + .withClassName("Person") + .withFields("name") + .withNearObject({ id: "some-uuid" }) + .do(); + + expect(mockClient.query).toHaveBeenCalledWith(expectedQuery); + }); + + test("a query with a valid nearObject with beacon", () => { + const mockClient = { + query: jest.fn(), + }; + + const expectedQuery = + `{Get{Person` + `(nearObject:{beacon:"weaviate/some-uuid"})` + `{name}}}`; + + new Getter(mockClient) + .withClassName("Person") + .withFields("name") + .withNearObject({ beacon: "weaviate/some-uuid" }) + .do(); + + expect(mockClient.query).toHaveBeenCalledWith(expectedQuery); + }); + + test("a query with a valid nearObject with all params", () => { + const mockClient = { + query: jest.fn(), + }; + + const expectedQuery = + `{Get{Person` + `(nearObject:{id:"some-uuid",beacon:"weaviate/some-uuid",certainty:0.7})` + `{name}}}`; + + new Getter(mockClient) + .withClassName("Person") + .withFields("name") + .withNearObject({ + id: "some-uuid", + beacon: "weaviate/some-uuid", + certainty: 0.7 + }) + .do(); + + expect(mockClient.query).toHaveBeenCalledWith(expectedQuery); + }); + + describe("queries with invalid nearObject searchers", () => { + const mockClient = { + query: jest.fn(), + }; + + const tests = [ + { + title: "an empty nearObject", + nearObject: {}, + msg: "nearObject filter: id or beacon needs to be set", + }, + { + title: "id of wrong type", + nearObject: { id: {} }, + msg: "nearObject filter: id must be a string", + }, + { + title: "beacon of wrong type", + nearObject: { beacon: {} }, + msg: "nearObject filter: beacon must be a string", + }, + { + title: "certainty of wrong type", + nearObject: { id: "foo", certainty: "foo" }, + msg: "nearObject filter: certainty must be a number", + } + ]; + + tests.forEach((t) => { + test(t.title, () => { + new Getter(mockClient) + .withClassName("Person") + .withFields("name") + .withNearObject(t.nearObject) + .do() + .then(() => fail("it should have error'd")) + .catch((e) => { + expect(e.toString()).toContain(t.msg); + }); + }); + }); + }); +}); diff --git a/graphql/journey.test.js b/graphql/journey.test.js index 9a5857b..8061802 100644 --- a/graphql/journey.test.js +++ b/graphql/journey.test.js @@ -120,6 +120,18 @@ describe("the graphql journey", () => { .catch((e) => fail("it should not have error'd" + e)); }); + test("graphql explore with nearObject field", () => { + return client.graphql + .explore() + .withNearObject({ id: "abefd256-8574-442b-9293-9205193737ee" }) + .withFields("beacon certainty className") + .do() + .then((res) => { + expect(res.data.Explore.length).toBeGreaterThan(0); + }) + .catch((e) => fail("it should not have error'd" + e)); + }); + it("tears down and cleans up", () => { return Promise.all([ client.schema.classDeleter().withClassName("Article").do(), @@ -150,6 +162,7 @@ const setup = async (client) => { const toImport = [ { + id: "abefd256-8574-442b-9293-9205193737ee", class: "Article", properties: { wordCount: 60, diff --git a/graphql/nearObject.js b/graphql/nearObject.js new file mode 100644 index 0000000..1a9dd04 --- /dev/null +++ b/graphql/nearObject.js @@ -0,0 +1,77 @@ +export default class GraphQLNearObject { + constructor(nearObjectObj) { + this.source = nearObjectObj; + } + + toString(wrap = true) { + this.parse(); + this.validate(); + + let args = []; + + if (this.id) { + args = [...args, `id:${JSON.stringify(this.id)}`]; + } + + if (this.beacon) { + args = [...args, `beacon:${JSON.stringify(this.beacon)}`]; + } + + if (this.certainty) { + args = [...args, `certainty:${this.certainty}`]; + } + + if (!wrap) { + return `${args.join(",")}`; + } + return `{${args.join(",")}}`; + } + + validate() { + if (!this.id && !this.beacon) { + throw new Error("nearObject filter: id or beacon needs to be set"); + } + } + + parse() { + for (let key in this.source) { + switch (key) { + case "id": + this.parseID(this.source[key]); + break; + case "beacon": + this.parseBeacon(this.source[key]); + break; + case "certainty": + this.parseCertainty(this.source[key]); + break; + default: + throw new Error("nearObject filter: unrecognized key '" + key + "'"); + } + } + } + + parseID(id) { + if (typeof id !== "string") { + throw new Error("nearObject filter: id must be a string"); + } + + this.id = id; + } + + parseBeacon(beacon) { + if (typeof beacon !== "string") { + throw new Error("nearObject filter: beacon must be a string"); + } + + this.beacon = beacon; + } + + parseCertainty(cert) { + if (typeof cert !== "number") { + throw new Error("nearObject filter: certainty must be a number"); + } + + this.certainty = cert; + } +} diff --git a/schema/journey.test.js b/schema/journey.test.js index f10ccce..9adc49d 100644 --- a/schema/journey.test.js +++ b/schema/journey.test.js @@ -8,15 +8,36 @@ describe("schema", () => { it("creates a thing class (implicitly)", () => { const classObj = { - class: "MyThingClass", + class: 'MyThingClass', properties: [ { dataType: ["string"], - name: "stringProp", - }, + name: 'stringProp', + moduleConfig: { + 'text2vec-contextionary': { + skip: false, + vectorizePropertyName: false + } + } + } ], - vectorIndexType: "hnsw", - vectorizer: "text2vec-contextionary", + vectorIndexType: 'hnsw', + vectorizer: 'text2vec-contextionary', + vectorIndexConfig: { + cleanupIntervalSeconds: 300, + maxConnections: 64, + efConstruction: 128, + vectorCacheMaxObjects: 500000 + }, + invertedIndexConfig: { + cleanupIntervalSeconds: 60 + }, + moduleConfig: { + 'text2vec-contextionary': + { + vectorizeClassName: true + } + } }; return client.schema @@ -58,6 +79,12 @@ describe("schema", () => { { dataType: ["string"], name: "stringProp", + moduleConfig: { + 'text2vec-contextionary': { + skip: false, + vectorizePropertyName: false + } + } }, { dataType: ["string"], @@ -66,6 +93,21 @@ describe("schema", () => { ], vectorIndexType: "hnsw", vectorizer: "text2vec-contextionary", + vectorIndexConfig: { + cleanupIntervalSeconds: 300, + maxConnections: 64, + efConstruction: 128, + vectorCacheMaxObjects: 500000 + }, + invertedIndexConfig: { + cleanupIntervalSeconds: 60 + }, + moduleConfig: { + 'text2vec-contextionary': + { + vectorizeClassName: true + } + } }, ], });