From a6a2ac4fbeec4955fc5f8a1e348b05f750bbcc91 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Wed, 4 Mar 2020 04:00:29 +0100 Subject: [PATCH] search spec starts doing what it should --- backend/src/schema/resolvers/searches.js | 36 ++- backend/src/schema/resolvers/searches.spec.js | 281 +++++++++++------- 2 files changed, 202 insertions(+), 115 deletions(-) diff --git a/backend/src/schema/resolvers/searches.js b/backend/src/schema/resolvers/searches.js index 5e09b062d..f2fbfa668 100644 --- a/backend/src/schema/resolvers/searches.js +++ b/backend/src/schema/resolvers/searches.js @@ -1,22 +1,18 @@ import log from './helpers/databaseLogger' +// see http://lucene.apache.org/core/8_3_1/queryparser/org/apache/lucene/queryparser/classic/package-summary.html#package.description + export default { Query: { findResources: async (_parent, args, context, _resolveInfo) => { const { query, limit } = args const { id: thisUserId } = context.user - // see http://lucene.apache.org/core/8_3_1/queryparser/org/apache/lucene/queryparser/classic/package-summary.html#package.description - const myQuery = query - .replace(/\s+/g, ' ') - .replace(/[[@#:*~\\$|^\]?/"'(){}+?!,.-;]/g, '') - .split(' ') - .map(s => (s.toLowerCase().match(/^(not|and|or)$/) ? '"' + s + '"' : s + '*')) - .join(' ') + const postCypher = ` CALL db.index.fulltext.queryNodes('post_fulltext_search', $query) YIELD node as resource, score MATCH (resource)<-[:WROTE]-(author:User) - WHERE score >= 0.5 + WHERE score >= 0.2 AND NOT ( author.deleted = true OR author.disabled = true OR resource.deleted = true OR resource.disabled = true @@ -53,7 +49,7 @@ export default { thisUserId, }) const userTransactionResponse = transaction.run(userCypher, { - query: myQuery, + query: createUserQuery(query), limit, thisUserId, }) @@ -64,6 +60,8 @@ export default { const [postResults, userResults] = await searchResultPromise log(postResults) log(userResults) + // console.log(postResults.summary.query.parameters) + // console.log(userResults) return [...postResults.records, ...userResults.records].map(r => r.get('resource')) } finally { session.close() @@ -72,16 +70,32 @@ export default { }, } +function createUserQuery(str) { + // match the whole text + const normalizedString = normalizeWhitespace(str) + const escapedString = escapeSpecialCharacters(normalizedString) + const result = normalizedString.includes(' ') ? quoteString(escapedString) : escapedString + // console.log('"' + + '"') + return result +} + function createPostQuery(str) { // match the whole text - // console.log('"' + escapeSpecialCharacters(normalizeWhitespace(str)) + '"') - return '"' + escapeSpecialCharacters(normalizeWhitespace(str)) + '"' + const normalizedString = normalizeWhitespace(str) + const escapedString = escapeSpecialCharacters(normalizedString) + const result = normalizedString.includes(' ') ? quoteString(escapedString) : escapedString + // console.log('"' + + '"') + return result } function normalizeWhitespace(str) { return str.replace(/\s+/g, ' ') } +function quoteString(str) { + return '"' + str + '"' +} + function escapeSpecialCharacters(str) { return str.replace(/(["[\]&|\\{}+!()^~*?:/-])/g, '\\$1') } diff --git a/backend/src/schema/resolvers/searches.spec.js b/backend/src/schema/resolvers/searches.spec.js index 90cd2e013..4ae62ca2c 100644 --- a/backend/src/schema/resolvers/searches.spec.js +++ b/backend/src/schema/resolvers/searches.spec.js @@ -26,7 +26,7 @@ beforeAll(async () => { }) afterAll(async () => { - await cleanDatabase() + // await cleanDatabase() }) const searchQuery = gql` @@ -47,73 +47,149 @@ const searchQuery = gql` } ` +const nothingFound = { data: { findResources: [] } } + +const createExpectedObject = array => { + return { data: { findResources: array } } +} + +const addPostToDB = post => { + return Factory.build('post', { + id: post.id, + title: post.title, + content: post.content, + }) +} + +const addUserToDB = user => { + return Factory.build('user', { + id: user.id, + name: user.name, + slug: user.slug, + }) +} + +const createDataObject = (obj, type) => { + return { __typename: type, ...obj } +} + +const createPostObject = post => { + return createDataObject(post, 'Post') +} + +const createUserObject = user => { + return createDataObject(user, 'User') +} + +// see data at the end of the file + +let user + describe('resolvers', () => { describe('searches', () => { beforeAll(async () => { - const user = await Factory.build('user', { - id: 'a-user', - name: 'John Doe', - slug: 'john-doe', - }) - await Factory.build('post', { - id: 'a-post', - title: 'Beitrag', - content: 'Ein erster Beitrag', - }) + user = await addUserToDB(aUser) + await addPostToDB(aPost) authenticatedUser = await user.toJson() }) + let variables describe('basic searches', () => { it('finds the post', async () => { variables = { query: 'Beitrag' } - const res = await query({ query: searchQuery, variables }) - // console.log(res) - expect(res.data.findResources).toHaveLength(1) + const expected = createExpectedObject([aPost]) + await expect(query({ query: searchQuery, variables })).resolves.toMatchObject(expected) }) it('does not find the post', async () => { variables = { query: 'Unfug' } - const res = await query({ query: searchQuery, variables }) - expect(res.data.findResources).toHaveLength(0) + await expect(query({ query: searchQuery, variables })).resolves.toMatchObject(nothingFound) }) it('finds the user', async () => { variables = { query: 'John' } - const res = await query({ query: searchQuery, variables }) - // console.log(res) - expect(res.data.findResources).toHaveLength(1) + const expected = createExpectedObject([aUser]) + await expect(query({ query: searchQuery, variables })).resolves.toMatchObject(expected) }) it('does not find the user', async () => { variables = { query: 'Unfug' } - const res = await query({ query: searchQuery, variables }) - expect(res.data.findResources).toHaveLength(0) + await expect(query({ query: searchQuery, variables })).resolves.toMatchObject(nothingFound) }) }) describe('more data added', () => { beforeAll(async () => { await Promise.all([ - Factory.build('post', { - id: 'b-post', - title: 'Aufruf', - content: 'Jeder sollte seinen Beitrag leisten.', - }), - Factory.build('post', { - id: 'c-post', - title: 'Die binomischen Formeln', - content: ` -1. binomische Formel: (a + b)² = a² + 2ab + b² + addPostToDB(bPost), + addPostToDB(cPost), + addPostToDB(dPost), + addPostToDB(ePost), + addPostToDB(fPost), + addPostToDB(gPost), + addUserToDB(bUser), + addUserToDB(cUser), + addUserToDB(dUser), + addUserToDB(eUser), + addUserToDB(fUser), + addUserToDB(gUser), + ]) + }) + + it('finds the AK-47', async () => { + variables = { query: 'AK-47' } + const expected = createExpectedObject([gPost]) + await expect(query({ query: searchQuery, variables })).resolves.toMatchObject(expected) + }) + + it('finds more than one post', async () => { + variables = { query: 'Beitrag' } + const expected = createExpectedObject([aPost, bPost]) + await expect(query({ query: searchQuery, variables })).resolves.toMatchObject(expected) + }) + + it('finds more than one user by slug', async () => { + variables = { query: '-maria-' } + const expected = createExpectedObject([dUser, cUser]) + await expect(query({ query: searchQuery, variables })).resolves.toMatchObject(expected) + }) + + it('finds Russian text', async () => { + variables = { query: 'Калашникова' } + const expected = createExpectedObject([gPost]) + await expect(query({ query: searchQuery, variables })).resolves.toMatchObject(expected) + }) + }) + }) +}) + +// data section + +const aPost = createPostObject({ + id: 'a-post', + title: 'Beitrag', + content: 'Ein erster Beitrag', +}) + +const bPost = createPostObject({ + id: 'b-post', + title: 'Aufruf', + content: 'Jeder sollte seinen Beitrag leisten.', +}) + +const cPost = createPostObject({ + id: 'c-post', + title: 'Die binomischen Formeln', + content: `1. binomische Formel: (a + b)² = a² + 2ab + b² 2. binomische Formel: (a - b)² = a² - 2ab + b² -3. binomische Formel: (a + b)(a - b) = a² - b² -`, - }), - Factory.build('post', { - id: 'd-post', - title: 'Der Panther', - content: ` -Sein Blick ist vom Vorübergehn der Stäbe +3. binomische Formel: (a + b)(a - b) = a² - b²`, +}) + +const dPost = createPostObject({ + id: 'd-post', + title: 'Der Panther', + content: `Sein Blick ist vom Vorübergehn der Stäbe so müd geworden, daß er nichts mehr hält. Ihm ist, als ob es tausend Stäbe gäbe und hinter tausend Stäben keine Welt. @@ -126,69 +202,66 @@ in der betäubt ein großer Wille steht. Nur manchmal schiebt der Vorhang der Pupille sich lautlos auf –. Dann geht ein Bild hinein, geht durch der Glieder angespannte Stille – -und hört im Herzen auf zu sein. -`, - }), - Factory.build('post', { - id: 'e-post', - title: 'Typographie', - content: ` -Gelegentlich können sowohl der angeführte Text als auch der Begleitsatz mit Frage- oder Ausrufezeichen enden (§ 91): -Gefällt dir der Roman „Quo vadis?“? Lass doch dieses ewige „Ich will nicht!“! -`, - }), - Factory.build('post', { - id: 'f-post', - title: 'Typographie II', - content: ` -Der Gedankenstrich kann als Auslassungszeichen (Auslassungsstrich) eine längere Pause oder eine Ellipse darstellen: „Du willst doch wohl nicht etwa –“, „Mein Gott, woher nehm ich bloß –?“ -`, - }), - Factory.build('post', { - id: 'g-post', - title: 'AK-47', - content: ` -Vom AK-47 Typ I existiert eine Version mit unter die Waffe klappbarer Schulterstütze, das AKS-47 (russisch Автомат Калашникова складной образца 1947 года, transkr.: Avtomat Kalašnikova skladnoj obrazca 1947 goda, dt. Automat Kalaschnikow klappbar Modell 1947tes Jahr) genannt wird, seltener auch AK-47s. -`, - }), - Factory.build('user', { - id: 'b-user', - name: 'Johnannes der Täufer', - slug: 'johnannes-der-taufer', - }), - Factory.build('user', { - id: 'c-user', - name: 'Rainer Maria Rilke', - slug: 'rainer-maria-rilke', - }), - Factory.build('user', { - id: 'd-user', - name: 'Erich Maria Remarque', - slug: 'erich-maria-remarque', - }), - Factory.build('user', { - id: 'e-user', - name: 'Klaus Dieter', - slug: 'kd', - }), - Factory.build('user', { - id: 'f-user', - name: 'Sluggy', - slug: '_', - }), - Factory.build('user', { - id: 'g-user', - name: 'AKK', - slug: 'akk', - }), - ]) - }) - - it('finds the AK-47', async () => { - variables = { query: 'AK-47' } - const res = await query({ query: searchQuery, variables }) - expect(res.data.findResources).toHaveLength(1) - }) - }) - }) +und hört im Herzen auf zu sein.`, +}) + +const ePost = createPostObject({ + id: 'e-post', + title: 'Typographie', + content: `Gelegentlich können sowohl der angeführte Text als auch der Begleitsatz mit Frage- oder Ausrufezeichen enden (§ 91): +Gefällt dir der Roman „Quo vadis?“? Lass doch dieses ewige „Ich will nicht!“!`, +}) + +const fPost = createPostObject({ + id: 'f-post', + title: 'Typographie II', + content: `Der Gedankenstrich kann als Auslassungszeichen (Auslassungsstrich) eine längere Pause oder eine Ellipse darstellen: „Du willst doch wohl nicht etwa –“, „Mein Gott, woher nehm ich bloß –?“`, +}) + +const gPost = createPostObject({ + id: 'g-post', + title: 'AK-47', + content: `Vom AK-47 Typ I existiert eine Version mit unter die Waffe klappbarer Schulterstütze, das AKS-47 (russisch Автомат Калашникова складной образца 1947 года, transkr.: Avtomat Kalašnikova skladnoj obrazca 1947 goda, dt. Automat Kalaschnikow klappbar Modell 1947tes Jahr) genannt wird, seltener auch AK-47s.`, +}) + +const aUser = createUserObject({ + id: 'a-user', + name: 'John Doe', + slug: 'john-doe', +}) + +const bUser = createUserObject({ + id: 'b-user', + name: 'Johnannes der Täufer', + slug: 'johnannes-der-taufer', +}) + +const cUser = createUserObject({ + id: 'c-user', + name: 'Rainer Maria Rilke', + slug: 'rainer-maria-rilke', +}) + +const dUser = createUserObject({ + id: 'd-user', + name: 'Erich Maria Remarque', + slug: 'erich-maria-remarque', +}) + +const eUser = createUserObject({ + id: 'e-user', + name: 'Klaus Dieter', + slug: 'kd', +}) + +const fUser = createUserObject({ + id: 'f-user', + name: 'Sluggy', + slug: '_', +}) + +const gUser = createUserObject({ + id: 'g-user', + name: 'AKK', + slug: 'akk', })