From 760b6b6e10309d0aef7c39b9c0d5b6011aa9bf45 Mon Sep 17 00:00:00 2001 From: keyan Date: Wed, 26 Oct 2022 17:46:01 -0500 Subject: [PATCH] related items --- api/resolvers/search.js | 88 +++++++++++++++++++++++++++++++++++++ api/typeDefs/item.js | 1 + components/item-full.js | 7 ++- components/related.js | 38 ++++++++++++++++ fragments/items.js | 27 ++++++++++++ lib/apollo.js | 13 ++++++ pages/items/[id]/related.js | 28 ++++++++++++ 7 files changed, 201 insertions(+), 1 deletion(-) create mode 100644 components/related.js create mode 100644 pages/items/[id]/related.js diff --git a/api/resolvers/search.js b/api/resolvers/search.js index 8dafc8e0..cc4f705b 100644 --- a/api/resolvers/search.js +++ b/api/resolvers/search.js @@ -1,8 +1,96 @@ import { decodeCursor, LIMIT, nextCursorEncoded } from '../../lib/cursor' import { getItem } from './item' +const STOP_WORDS = ['a', 'an', 'and', 'are', 'as', 'at', 'be', 'but', + 'by', 'for', 'if', 'in', 'into', 'is', 'it', 'no', 'not', + 'of', 'on', 'or', 'such', 'that', 'the', 'their', 'then', + 'there', 'these', 'they', 'this', 'to', 'was', 'will', + 'with', 'bitcoin', 'page', 'adds', 'how', 'why', 'what', + 'works', 'now', 'available', 'breaking', 'app', 'powered', + 'just', 'dev', 'using', 'crypto', 'has', 'my', 'i', 'apps', + 'really', 'new', 'era', 'application', 'best', 'year', + 'latest', 'still', 'few', 'crypto', 'keep', 'public', 'current', + 'levels', 'from', 'cryptocurrencies', 'confirmed', 'news', 'network', + 'about', 'sources', 'vote', 'considerations', 'hope', + 'keep', 'keeps', 'including', 'we', 'brings', "don't", 'do', + 'interesting', 'us'] + export default { Query: { + related: async (parent, { title, id, cursor, limit }, { me, models, search }) => { + const decodedCursor = decodeCursor(cursor) + if (!title || title.trim().split(/\s+/).length < 1) { + if (id) { + const item = await getItem(parent, { id }, { me, models }) + title = item?.title + } + if (!title) { + return { + items: [], + cursor: null + } + } + } + + const mustNot = [] + if (id) { + mustNot.push({ term: { id } }) + } + + let items = await search.search({ + index: 'item', + size: limit || LIMIT, + from: decodedCursor.offset, + body: { + query: { + bool: { + should: [ + { + more_like_this: { + fields: ['title'], + like: title, + min_term_freq: 1, + min_doc_freq: 1, + max_query_terms: 25, + min_word_length: 2, + minimum_should_match: '50%', + stop_words: STOP_WORDS, + boost: 400 + } + }, + { + more_like_this: { + fields: ['title'], + like: title, + min_term_freq: 1, + min_doc_freq: 1, + min_word_length: 2, + max_query_terms: 25, + minimum_should_match: '30%', + stop_words: STOP_WORDS + } + } + ], + must_not: [{ exists: { field: 'parentId' } }, ...mustNot], + filter: { + range: { sats: { gte: 10 } } + } + } + }, + sort: ['_score', { sats: 'desc' }] + } + }) + + items = items.body.hits.hits.map(async e => { + // this is super inefficient but will suffice until we do something more generic + return await getItem(parent, { id: e._source.id }, { me, models }) + }) + + return { + cursor: items.length === (limit || LIMIT) ? nextCursorEncoded(decodedCursor) : null, + items + } + }, search: async (parent, { q: query, sub, cursor, sort, what, when }, { me, models, search }) => { const decodedCursor = decodeCursor(cursor) let sitems diff --git a/api/typeDefs/item.js b/api/typeDefs/item.js index 73bc9942..cf2f363f 100644 --- a/api/typeDefs/item.js +++ b/api/typeDefs/item.js @@ -8,6 +8,7 @@ export default gql` comments(id: ID!, sort: String): [Item!]! pageTitle(url: String!): String dupes(url: String!): [Item!] + related(cursor: String, title: String, id: ID, limit: Int): Items allItems(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! diff --git a/components/item-full.js b/components/item-full.js index 14900764..e57e59ea 100644 --- a/components/item-full.js +++ b/components/item-full.js @@ -14,6 +14,7 @@ import useDarkMode from 'use-dark-mode' import { useEffect, useState } from 'react' import Poll from './poll' import { commentsViewed } from '../lib/new-comments' +import Related from './related' function BioItem ({ item, handleClick }) { const me = useMe() @@ -90,7 +91,11 @@ function TopLevelItem ({ item, noReply, ...props }) { {item.text && } {item.url && } {item.poll && } - {!noReply && } + {!noReply && + <> + + {!item.position && !item.isJob && !item.parentId && } + } ) } diff --git a/components/related.js b/components/related.js new file mode 100644 index 00000000..2b0872d0 --- /dev/null +++ b/components/related.js @@ -0,0 +1,38 @@ +import { useQuery } from '@apollo/client' +import Link from 'next/link' +import { RELATED_ITEMS } from '../fragments/items' +import AccordianItem from './accordian-item' +import Item, { ItemSkeleton } from './item' +import styles from './items.module.css' + +export default function Related ({ title, itemId }) { + const emptyItems = new Array(5).fill(null) + const { data, loading } = useQuery(RELATED_ITEMS, { + fetchPolicy: 'cache-first', + variables: { title, id: itemId, limit: 5 } + }) + + let items, cursor + if (data) { + ({ related: { items, cursor } } = data) + } + + return ( + related} + body={ + <> +
+ {loading + ? emptyItems.map((_, i) => ) + : (items?.length + ? items.map(item => ) + :
EMPTY
+ )} +
+ {cursor && itemId && view all related} + + } + /> + ) +} diff --git a/fragments/items.js b/fragments/items.js index 8a648d3a..91fdc26c 100644 --- a/fragments/items.js +++ b/fragments/items.js @@ -204,3 +204,30 @@ export const ITEM_SEARCH = gql` } } ` + +export const RELATED_ITEMS = gql` + ${ITEM_FIELDS} + query Related($title: String, $id: ID, $cursor: String, $limit: Int) { + related(title: $title, id: $id, cursor: $cursor, limit: $limit) { + cursor + items { + ...ItemFields + } + } + } +` + +export const RELATED_ITEMS_WITH_ITEM = gql` + ${ITEM_FIELDS} + query Related($title: String, $id: ID, $cursor: String, $limit: Int) { + item(id: $id) { + ...ItemFields + } + related(title: $title, id: $id, cursor: $cursor, limit: $limit) { + cursor + items { + ...ItemFields + } + } + } +` diff --git a/lib/apollo.js b/lib/apollo.js index f60a1898..5829c368 100644 --- a/lib/apollo.js +++ b/lib/apollo.js @@ -79,6 +79,19 @@ export default function getApolloClient () { } } }, + related: { + keyArgs: ['id', 'title', 'limit'], + merge (existing, incoming) { + if (isFirstPage(incoming.cursor, existing?.items)) { + return incoming + } + + return { + cursor: incoming.cursor, + items: [...(existing?.items || []), ...incoming.items] + } + } + }, outlawedItems: { keyArgs: [], merge (existing, incoming) { diff --git a/pages/items/[id]/related.js b/pages/items/[id]/related.js new file mode 100644 index 00000000..4d06e3e8 --- /dev/null +++ b/pages/items/[id]/related.js @@ -0,0 +1,28 @@ +// ssr the related query with an adequate limit +// need to use a cursor on related +import { RELATED_ITEMS, RELATED_ITEMS_WITH_ITEM } from '../../../fragments/items' +import { getGetServerSideProps } from '../../../api/ssrApollo' +import Items from '../../../components/items' +import Layout from '../../../components/layout' +import { useRouter } from 'next/router' +import Item from '../../../components/item' + +export const getServerSideProps = getGetServerSideProps(RELATED_ITEMS_WITH_ITEM, null, + data => !data.item) + +export default function Related ({ data: { item, related: { items, cursor } } }) { + const router = useRouter() + + return ( + + +
related
+ data.related} + variables={{ id: router.query.id }} + /> +
+ ) +}