stacker.news/api/resolvers/search.js

198 lines
5.5 KiB
JavaScript
Raw Normal View History

2022-10-20 22:44:44 +00:00
import { decodeCursor, LIMIT, nextCursorEncoded } from '../../lib/cursor'
import { getItem } from './item'
export default {
Query: {
search: async (parent, { q: query, sub, cursor, sort, what, when }, { me, models, search }) => {
const decodedCursor = decodeCursor(cursor)
let sitems
2022-10-25 22:30:54 +00:00
if (!query) {
return {
items: [],
cursor: null
}
}
2022-10-24 21:32:16 +00:00
const whatArr = []
switch (what) {
case 'posts':
whatArr.push({ bool: { must_not: { exists: { field: 'parentId' } } } })
break
case 'comments':
whatArr.push({ bool: { must: { exists: { field: 'parentId' } } } })
break
default:
break
}
const queryArr = query.trim().split(/\s+/)
const url = queryArr.find(word => word.startsWith('url:'))
const nym = queryArr.find(word => word.startsWith('nym:'))
query = queryArr.filter(word => !word.startsWith('url:') && !word.startsWith('nym:')).join(' ')
if (url) {
whatArr.push({ wildcard: { url: `*${url.slice(4).toLowerCase()}*` } })
}
if (nym) {
whatArr.push({ wildcard: { 'user.name': `*${nym.slice(4).toLowerCase()}*` } })
}
2022-10-20 22:44:44 +00:00
const sortArr = []
switch (sort) {
case 'recent':
sortArr.push({ createdAt: 'desc' })
break
case 'comments':
sortArr.push({ ncomments: 'desc' })
break
case 'sats':
sortArr.push({ sats: 'desc' })
break
case 'votes':
sortArr.push({ upvotes: 'desc' })
break
default:
break
}
sortArr.push('_score')
2022-10-24 21:32:16 +00:00
if (query.length) {
whatArr.push({
bool: {
should: [
{
// all terms are matched in fields
multi_match: {
query,
type: 'most_fields',
fields: ['title^20', 'text'],
minimum_should_match: '100%',
boost: 400
}
},
{
// all terms are matched in fields
multi_match: {
query,
type: 'most_fields',
fields: ['title^20', 'text'],
fuzziness: 'AUTO',
prefix_length: 3,
minimum_should_match: '100%',
boost: 20
}
},
{
// only some terms must match unless we're sorting
multi_match: {
query,
type: 'most_fields',
fields: ['title^20', 'text'],
fuzziness: 'AUTO',
prefix_length: 3,
minimum_should_match: sortArr.length > 1 ? '100%' : '60%'
}
}
]
}
})
2022-10-20 22:44:44 +00:00
}
let whenGte
switch (when) {
case 'day':
whenGte = 'now-1d'
break
case 'week':
whenGte = 'now-7d'
break
case 'month':
whenGte = 'now-30d'
break
case 'year':
whenGte = 'now-365d'
break
default:
break
}
try {
sitems = await search.search({
index: 'item',
size: LIMIT,
from: decodedCursor.offset,
body: {
query: {
bool: {
must: [
...whatArr,
sub
? { match: { 'sub.name': sub } }
: { bool: { must_not: { exists: { field: 'sub.name' } } } },
me
? {
bool: {
should: [
{ match: { status: 'ACTIVE' } },
{ match: { status: 'NOSATS' } },
{ match: { userId: me.id } }
]
}
}
: {
bool: {
should: [
{ match: { status: 'ACTIVE' } },
{ match: { status: 'NOSATS' } }
]
}
2022-10-24 21:32:16 +00:00
}
2022-10-20 22:44:44 +00:00
],
filter: {
range: {
createdAt: {
lte: decodedCursor.time,
gte: whenGte
}
}
}
}
},
sort: sortArr,
highlight: {
fields: {
title: { number_of_fragments: 0, pre_tags: [':high['], post_tags: [']'] },
text: { number_of_fragments: 0, pre_tags: [':high['], post_tags: [']'] }
}
}
}
})
} catch (e) {
console.log(e)
return {
cursor: null,
items: []
}
}
// return highlights
const items = sitems.body.hits.hits.map(async e => {
// this is super inefficient but will suffice until we do something more generic
const item = await getItem(parent, { id: e._source.id }, { me, models })
2022-10-24 21:32:16 +00:00
item.searchTitle = (e.highlight?.title && e.highlight.title[0]) || item.title
item.searchText = (e.highlight?.text && e.highlight.text[0]) || item.text
2022-10-20 22:44:44 +00:00
return item
})
return {
cursor: items.length === LIMIT ? nextCursorEncoded(decodedCursor) : null,
items
}
}
}
}