From e4d150413b1e6ff8aad61a1e77d38b42a1b5823c Mon Sep 17 00:00:00 2001 From: keyan Date: Thu, 20 Oct 2022 17:44:44 -0500 Subject: [PATCH] search filters --- api/resolvers/index.js | 3 +- api/resolvers/item.js | 117 ----------------------- api/resolvers/search.js | 176 +++++++++++++++++++++++++++++++++++ api/typeDefs/item.js | 2 +- components/form.js | 21 ++++- components/search.js | 124 +++++++++++++++--------- components/search.module.css | 13 ++- fragments/items.js | 4 +- lib/apollo.js | 2 +- pages/search.js | 3 +- styles/globals.scss | 1 - 11 files changed, 293 insertions(+), 173 deletions(-) create mode 100644 api/resolvers/search.js diff --git a/api/resolvers/index.js b/api/resolvers/index.js index a91ff1a1..d2bb87d9 100644 --- a/api/resolvers/index.js +++ b/api/resolvers/index.js @@ -8,7 +8,8 @@ import invite from './invite' import sub from './sub' import upload from './upload' import growth from './growth' +import search from './search' import { GraphQLJSONObject } from 'graphql-type-json' export default [user, item, message, wallet, lnurl, notifications, invite, sub, - upload, growth, { JSONObject: GraphQLJSONObject }] + upload, growth, search, { JSONObject: GraphQLJSONObject }] diff --git a/api/resolvers/item.js b/api/resolvers/item.js index b34d6378..11ba10c8 100644 --- a/api/resolvers/item.js +++ b/api/resolvers/item.js @@ -435,123 +435,6 @@ export default { comments: async (parent, { id, sort }, { me, models }) => { return comments(me, models, id, sort) }, - search: async (parent, { q: query, sub, cursor }, { me, models, search }) => { - const decodedCursor = decodeCursor(cursor) - let sitems - - try { - sitems = await search.search({ - index: 'item', - size: LIMIT, - from: decodedCursor.offset, - body: { - query: { - bool: { - must: [ - 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' } } - ] - } - }, - { - 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 - multi_match: { - query, - type: 'most_fields', - fields: ['title^20', 'text'], - fuzziness: 'AUTO', - prefix_length: 3, - minimum_should_match: '60%' - } - } - // TODO: add wildcard matches for - // user.name and url - ] - } - } - ], - filter: { - range: { - createdAt: { - lte: decodedCursor.time - } - } - } - } - }, - 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 }) - - item.searchTitle = (e.highlight.title && e.highlight.title[0]) || item.title - item.searchText = (e.highlight.text && e.highlight.text[0]) || item.text - - return item - }) - - return { - cursor: items.length === LIMIT ? nextCursorEncoded(decodedCursor) : null, - items - } - }, auctionPosition: async (parent, { id, sub, bid }, { models, me }) => { const createdAt = id ? (await getItem(parent, { id }, { models, me })).createdAt : new Date() let where diff --git a/api/resolvers/search.js b/api/resolvers/search.js new file mode 100644 index 00000000..d9eaf714 --- /dev/null +++ b/api/resolvers/search.js @@ -0,0 +1,176 @@ +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 + + 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') + + 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 + } + + 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' } } + ] + } + }, + { + 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%' + } + } + // TODO: add wildcard matches for + // user.name and url + ] + } + } + ], + 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 }) + + item.searchTitle = (e.highlight.title && e.highlight.title[0]) || item.title + item.searchText = (e.highlight.text && e.highlight.text[0]) || item.text + + return item + }) + + return { + cursor: items.length === LIMIT ? nextCursorEncoded(decodedCursor) : null, + items + } + } + } +} diff --git a/api/typeDefs/item.js b/api/typeDefs/item.js index 2ee07a68..17aef11a 100644 --- a/api/typeDefs/item.js +++ b/api/typeDefs/item.js @@ -9,7 +9,7 @@ export default gql` pageTitle(url: String!): String dupes(url: String!): [Item!] allItems(cursor: String): Items - search(q: String, sub: String, cursor: String): Items + search(q: String, sub: String, cursor: String, what: String, sort: String, when: String): Items auctionPosition(sub: String, id: ID, bid: Int!): Int! itemRepetition(parentId: ID): Int! outlawedItems(cursor: String): Items diff --git a/components/form.js b/components/form.js index 97d67b53..170bc91b 100644 --- a/components/form.js +++ b/components/form.js @@ -193,11 +193,14 @@ function InputInner ({ {(clear && field.value) && )} + {filter && +
+ search({ ...formik?.values, sort: e.target.value })} + name='sort' + size='sm' + items={['match', 'recent', 'comments', 'sats', 'votes']} + /> + for + { + setSearching(true) + setQ(e.target.value?.trim()) + }} + /> + {q || atBottom || router.query.q + ? ( + + + + ) + : ( + )} +
) : ( - )} -
+
) } diff --git a/components/search.module.css b/components/search.module.css index 461795c9..e180a5fd 100644 --- a/components/search.module.css +++ b/components/search.module.css @@ -3,7 +3,6 @@ bottom: 0; left: 0; right: 0; - height: 88px; } .searchContainer { @@ -11,6 +10,10 @@ position: relative; } +.leaveRoom { + height: 130px !important; +} + .searchSection.solid { pointer-events: auto; background: var(--theme-body); @@ -30,18 +33,20 @@ align-items: center; justify-content: center; display: flex; + left: auto !important; } -.active { +.formActive { pointer-events: auto; bottom: 18px; right: 18px; + left: 18px; position: absolute; } -form.active { - left: 18px; +form>.active { display: flex; + pointer-events: auto; flex-flow: row wrap; align-items: center; } diff --git a/fragments/items.js b/fragments/items.js index 2737f9e0..15f9f0e8 100644 --- a/fragments/items.js +++ b/fragments/items.js @@ -175,8 +175,8 @@ export const ITEM_WITH_COMMENTS = gql` export const ITEM_SEARCH = gql` ${ITEM_FIELDS} - query Search($q: String, $cursor: String) { - search(q: $q, cursor: $cursor) { + query Search($q: String, $cursor: String, $sort: String, $what: String, $when: String) { + search(q: $q, cursor: $cursor, sort: $sort, what: $what, when: $when) { cursor items { ...ItemFields diff --git a/lib/apollo.js b/lib/apollo.js index 465d0a71..ac34db37 100644 --- a/lib/apollo.js +++ b/lib/apollo.js @@ -92,7 +92,7 @@ export default function getApolloClient () { } }, search: { - keyArgs: ['q'], + keyArgs: ['q', 'sub', 'sort', 'what', 'when'], merge (existing, incoming) { if (isFirstPage(incoming.cursor, existing?.items)) { return incoming diff --git a/pages/search.js b/pages/search.js index 8ae388b3..575ecc11 100644 --- a/pages/search.js +++ b/pages/search.js @@ -15,7 +15,8 @@ export default function Index ({ data: { search: { items, cursor } } }) { {router.query?.q && } ) diff --git a/styles/globals.scss b/styles/globals.scss index 3d65842f..c48a190d 100644 --- a/styles/globals.scss +++ b/styles/globals.scss @@ -282,7 +282,6 @@ footer { @media screen and (max-width: 767px) { input, - select, textarea, .form-control, .form-control:focus,