From e8492b59f442a946fb4fcc3703ff0c5b33c67d0f Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Thu, 2 Apr 2020 00:36:26 +0200 Subject: [PATCH] feat: add pagination for search page - it wasn't really making sense to have one query for all users/posts, future hashtags, because we change the first/offset when the user paginates, which would unneccesarily refetch all other resources. - the solution was to separate them into their own queries and only refetch when the user wants to paginate the resources. --- backend/src/db/factories.js | 2 +- backend/src/db/seed.js | 19 +++- .../src/middleware/permissionsMiddleware.js | 2 + backend/src/schema/resolvers/searches.js | 83 ++++++++++++++++ backend/src/schema/types/type/Search.gql | 2 + .../features/SearchResults/SearchResults.vue | 96 ++++++++++++++++--- webapp/graphql/Search.js | 39 +++++++- 7 files changed, 224 insertions(+), 19 deletions(-) diff --git a/backend/src/db/factories.js b/backend/src/db/factories.js index 010ef67ad..e0280bed4 100644 --- a/backend/src/db/factories.js +++ b/backend/src/db/factories.js @@ -64,7 +64,7 @@ Factory.define('basicUser') password: '1234', role: 'user', about: faker.lorem.paragraph, - termsAndConditionsAgreedVersion: '0.0.1', + termsAndConditionsAgreedVersion: '0.0.4', termsAndConditionsAgreedAt: '2019-08-01T10:47:19.212Z', allowEmbedIframes: false, showShoutsPublicly: false, diff --git a/backend/src/db/seed.js b/backend/src/db/seed.js index 953f80b55..782d52d0c 100644 --- a/backend/src/db/seed.js +++ b/backend/src/db/seed.js @@ -929,7 +929,24 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] ]) await Promise.all([...Array(30).keys()].map(() => Factory.build('user'))) - + await Promise.all( + [...Array(30).keys()].map(index => Factory.build('user', { name: `Jenny${index}` })), + ) + await Promise.all( + [...Array(30).keys()].map(() => + Factory.build( + 'post', + { content: `Jenny ${faker.lorem.sentence()}` }, + { + categoryIds: ['cat1'], + author: jennyRostock, + image: Factory.build('image', { + url: faker.image.unsplash.objects(), + }), + }, + ), + ), + ) await Promise.all( [...Array(30).keys()].map(() => Factory.build( diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index dcb6f8973..e9807c3c2 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -87,6 +87,8 @@ export default shield( findPosts: allow, findUsers: allow, searchResults: allow, + searchPosts: allow, + searchUsers: allow, embed: allow, Category: allow, Tag: allow, diff --git a/backend/src/schema/resolvers/searches.js b/backend/src/schema/resolvers/searches.js index 0cf5c4ae8..eff8cd082 100644 --- a/backend/src/schema/resolvers/searches.js +++ b/backend/src/schema/resolvers/searches.js @@ -5,6 +5,89 @@ import { queryString } from './searches/queryString' export default { Query: { + searchPosts: async (_parent, args, context, _resolveInfo) => { + const { query, postsOffset, firstPosts } = args + const { id: userId } = context.user + + const postCypher = ` + CALL db.index.fulltext.queryNodes('post_fulltext_search', $query) + YIELD node as posts, score + MATCH (posts)<-[:WROTE]-(author:User) + WHERE score >= 0.0 + AND NOT ( + author.deleted = true OR author.disabled = true + OR posts.deleted = true OR posts.disabled = true + OR (:User {id: $userId})-[:MUTED]->(author) + ) + WITH posts, author, + [(posts)<-[:COMMENTS]-(comment:Comment) | comment] as comments, + [(posts)<-[:SHOUTED]-(user:User) | user] as shouter + RETURN posts { + .*, + __typename: labels(posts)[0], + author: properties(author), + commentsCount: toString(size(comments)), + shoutedCount: toString(size(shouter)) + } + SKIP $postsOffset + LIMIT $firstPosts + ` + + const myQuery = queryString(query) + + const session = context.driver.session() + const searchResultPromise = session.readTransaction(async transaction => { + const postTransactionResponse = await transaction.run(postCypher, { + query: myQuery, + postsOffset, + firstPosts, + userId, + }) + return postTransactionResponse + }) + try { + const postResults = await searchResultPromise + log(postResults) + return postResults.records.map(record => record.get('posts')) + } finally { + session.close() + } + }, + searchUsers: async (_parent, args, context, _resolveInfo) => { + const { query, usersOffset, firstUsers } = args + const { id: userId } = context.user + + const userCypher = ` + CALL db.index.fulltext.queryNodes('user_fulltext_search', $query) + YIELD node as users, score + MATCH (users) + WHERE score >= 0.0 + AND NOT (users.deleted = true OR users.disabled = true) + RETURN users {.*, __typename: labels(users)[0]} + SKIP $usersOffset + LIMIT $firstUsers + ` + const myQuery = queryString(query) + + const session = context.driver.session() + const searchResultPromise = session.readTransaction(async transaction => { + const userTransactionResponse = await transaction.run(userCypher, { + query: myQuery, + usersOffset, + firstUsers, + userId, + }) + return userTransactionResponse + }) + + try { + const userResults = await searchResultPromise + log(userResults) + return userResults.records.map(record => record.get('users')) + } finally { + session.close() + } + }, searchResults: async (_parent, args, context, _resolveInfo) => { const { query, limit } = args const { id: thisUserId } = context.user diff --git a/backend/src/schema/types/type/Search.gql b/backend/src/schema/types/type/Search.gql index 9b8613ff8..effd7246c 100644 --- a/backend/src/schema/types/type/Search.gql +++ b/backend/src/schema/types/type/Search.gql @@ -1,5 +1,7 @@ union SearchResult = Post | User type Query { + searchPosts(query: String!, firstPosts: Int, postsOffset: Int): [Post]! + searchUsers(query: String!, firstUsers: Int, usersOffset: Int): [User]! searchResults(query: String!, limit: Int = 5): [SearchResult]! } diff --git a/webapp/components/_new/features/SearchResults/SearchResults.vue b/webapp/components/_new/features/SearchResults/SearchResults.vue index e32970cf6..3b0c2d08a 100644 --- a/webapp/components/_new/features/SearchResults/SearchResults.vue +++ b/webapp/components/_new/features/SearchResults/SearchResults.vue @@ -9,38 +9,54 @@ icon="tasks" :message="$t('search.no-results', { search })" /> - - - - - +