203 lines
5.5 KiB
JavaScript

import log from './helpers/databaseLogger'
import { queryString } from './searches/queryString'
// see http://lucene.apache.org/core/8_3_1/queryparser/org/apache/lucene/queryparser/classic/package-summary.html#package.description
const cypherTemplate = (setup) => `
CALL db.index.fulltext.queryNodes('${setup.fulltextIndex}', $query)
YIELD node AS resource, score
${setup.match}
${setup.whereClause}
${setup.withClause}
RETURN
${setup.returnClause}
AS result
SKIP $skip
${setup.limit}
`
const simpleWhereClause =
'WHERE score >= 0.0 AND NOT (resource.deleted = true OR resource.disabled = true)'
const postWhereClause = `WHERE score >= 0.0
AND NOT (
author.deleted = true OR author.disabled = true
OR resource.deleted = true OR resource.disabled = true
OR (:User {id: $userId})-[:MUTED]->(author)
)`
const searchPostsSetup = {
fulltextIndex: 'post_fulltext_search',
match: 'MATCH (resource)<-[:WROTE]-(author:User)',
whereClause: postWhereClause,
withClause: `WITH resource, author,
[(resource)<-[:COMMENTS]-(comment:Comment) | comment] AS comments,
[(resource)<-[:SHOUTED]-(user:User) | user] AS shouter`,
returnClause: `resource {
.*,
__typename: labels(resource)[0],
author: properties(author),
commentsCount: toString(size(comments)),
shoutedCount: toString(size(shouter))
}`,
limit: 'LIMIT $limit',
}
const searchUsersSetup = {
fulltextIndex: 'user_fulltext_search',
match: 'MATCH (resource)',
whereClause: simpleWhereClause,
withClause: '',
returnClause: 'resource {.*, __typename: labels(resource)[0]}',
limit: 'LIMIT $limit',
}
const searchHashtagsSetup = {
fulltextIndex: 'tag_fulltext_search',
match: 'MATCH (resource)',
whereClause: simpleWhereClause,
withClause: '',
returnClause: 'resource {.*, __typename: labels(resource)[0]}',
limit: 'LIMIT $limit',
}
const countSetup = {
returnClause: 'toString(size(collect(resource)))',
limit: '',
}
const countUsersSetup = {
...searchUsersSetup,
...countSetup,
}
const countPostsSetup = {
...searchPostsSetup,
...countSetup,
}
const countHashtagsSetup = {
...searchHashtagsSetup,
...countSetup,
}
const searchResultPromise = async (session, setup, params) => {
return session.readTransaction(async (transaction) => {
return transaction.run(cypherTemplate(setup), params)
})
}
const searchResultCallback = (result) => {
return result.records.map((r) => r.get('result'))
}
const countResultCallback = (result) => {
return result.records[0].get('result')
}
const getSearchResults = async (context, setup, params, resultCallback = searchResultCallback) => {
const session = context.driver.session()
try {
const results = await searchResultPromise(session, setup, params)
log(results)
return resultCallback(results)
} finally {
session.close()
}
}
const multiSearchMap = [
{ symbol: '!', setup: searchPostsSetup, resultName: 'posts' },
{ symbol: '@', setup: searchUsersSetup, resultName: 'users' },
{ symbol: '#', setup: searchHashtagsSetup, resultName: 'hashtags' },
]
export default {
Query: {
searchPosts: async (_parent, args, context, _resolveInfo) => {
const { query, postsOffset, firstPosts } = args
const { id: userId } = context.user
return {
postCount: getSearchResults(
context,
countPostsSetup,
{
query: queryString(query),
skip: 0,
userId,
},
countResultCallback,
),
posts: getSearchResults(context, searchPostsSetup, {
query: queryString(query),
skip: postsOffset,
limit: firstPosts,
userId,
}),
}
},
searchUsers: async (_parent, args, context, _resolveInfo) => {
const { query, usersOffset, firstUsers } = args
return {
userCount: getSearchResults(
context,
countUsersSetup,
{
query: queryString(query),
skip: 0,
},
countResultCallback,
),
users: getSearchResults(context, searchUsersSetup, {
query: queryString(query),
skip: usersOffset,
limit: firstUsers,
}),
}
},
searchHashtags: async (_parent, args, context, _resolveInfo) => {
const { query, hashtagsOffset, firstHashtags } = args
return {
hashtagCount: getSearchResults(
context,
countHashtagsSetup,
{
query: queryString(query),
skip: 0,
},
countResultCallback,
),
hashtags: getSearchResults(context, searchHashtagsSetup, {
query: queryString(query),
skip: hashtagsOffset,
limit: firstHashtags,
}),
}
},
searchResults: async (_parent, args, context, _resolveInfo) => {
const { query, limit } = args
const { id: userId } = context.user
const searchType = query.replace(/^([!@#]?).*$/, '$1')
const searchString = query.replace(/^([!@#])/, '')
const params = {
query: queryString(searchString),
skip: 0,
limit,
userId,
}
if (searchType === '')
return [
...(await getSearchResults(context, searchPostsSetup, params)),
...(await getSearchResults(context, searchUsersSetup, params)),
...(await getSearchResults(context, searchHashtagsSetup, params)),
]
params.limit = 15
const type = multiSearchMap.find((obj) => obj.symbol === searchType)
return getSearchResults(context, type.setup, params)
},
},
}