Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Search/Filtering #230

Draft
wants to merge 2 commits into
base: production
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions constants/page.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const pageSlugFetchLinks = [
'page.handle_override',
'page.title',
'page.parent_pages',
'category.title',
]

// TODO continue to add in fetch links as needed for slices and page content here
Expand Down
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,20 @@
},
"dependencies": {
"@prismicio/client": "^6.3.0",
"aws4": "^1.11.0",
"classnames": "^2.2.6",
"embla-carousel-react": "^7.0.0-rc03",
"moment": "^2.29.4",
"moment-timezone": "^0.5.37",
"next": "^12.0.1",
"normalize.css": "^8.0.1",
"nprogress": "^0.2.0",
"prismic-reactjs": "1.3.4",
"prop-types": "^15.7.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"sass": "^1.38.2"
"sass": "^1.38.2",
"swr": "^1.3.0"
},
"devDependencies": {
"@babel/core": "^7.15.0",
Expand Down
100 changes: 100 additions & 0 deletions pages/api/search-index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { client } from 'lib/prismic'
import { pageSlugFetchLinks } from 'constants/page'
import { linkResolver } from 'lib/resolvers'
import * as prismic from '@prismicio/client'
import * as moment from 'moment-timezone'

/**
* This endpoint is just for testing your data
* Modify pageToIndex() to match your data's fields
*/
export default async function searchIndex(_, res) {
// Feel free to adjust this, doesn't affect indexing
let offset = moment().add(-2, 'hours').valueOf()
const index = await getPagesToIndex(offset)
return res.status(200).json(index)
}

// This function is read hourly to index new changes.
// It only adds up to 100 pages at once, so if you
// bulk add, you'll need to modify this to index all the items
export async function getPagesToIndex(offset) {
const masterRef = await client.getMasterRef()
const ref = masterRef.ref

let today = offset || moment().add(-1.5, 'hours').valueOf()

const pages = await client.getAllByType('page', {
ref,
fetchLinks: pageSlugFetchLinks,
predicates: [
prismic.Predicates.dateAfter('document.last_publication_date', today),
],
})
let index = []
pages.forEach((page) => {
const sampleIndex = pageToIndex(page)
index.push(sampleIndex)
})

return index
}

export function pageToIndex(page) {
const sampleIndex = {
type: 'add',
id: page.id,
fields: {
// Map the page fields here to your CloudSearch
title: page?.data?.title,
url: linkResolver(page),
body: page?.data?.body
?.map((body) => {
let text

if (body?.primary?.richtext) {
body?.primary?.richtext.forEach((rt) => {
if (text === undefined) {
text = ''
}
if (rt.text !== undefined) {
text += rt.text + ' '
}
})
}
return text
})
.filter(function (el) {
return el !== undefined
}),
category: page?.data?.categories
?.map((cat) => {
if (cat?.category) {
return cat?.category?.data?.title
}
})
.filter(function (el) {
return el !== undefined
}),
category_handle: page?.data?.categories
?.map((cat) => {
if (cat?.category) {
return cat?.category?.uid
}
})
.filter(function (el) {
return el !== undefined
}),
category_id: page?.data?.categories
?.map((cat) => {
if (cat?.category) {
return cat?.category?.id
}
})
.filter(function (el) {
return el !== undefined
}),
},
}
return sampleIndex
}
39 changes: 39 additions & 0 deletions pages/api/search.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { client } from 'lib/prismic'
import { pageSlugFetchLinks } from 'constants/page'

/**
* Endpoint to query CloudSearch
* and populate with Prismic documents
*/
export default async function search(req, res) {
const q = req.query.q || ''
const sort = req.query.sort ? `&sort=${req.query.sort}` : ''
const size = req.query.size || 6
const page = req.query.page || 0
const start = page * size
const masterRef = await client.getMasterRef()
const ref = masterRef.ref

// Query documentation starts here: https://docs.aws.amazon.com/cloudsearch/latest/developerguide/searching-compound-queries.html
const response = await fetch(
`https://search-prismic-zi3vcm5qxhe7ua7mhcd4neqequ.us-west-2.cloudsearch.amazonaws.com/2013-01-01/search?q.parser=structured&q=${q}&size=${size}${sort}&start=${start}`
)

const data = await response.json()
let results = []

// Get prismic IDs from search results
if (data?.hits?.hit) {
results = data?.hits?.hit?.map(({ id }) => {
return id
})
}

// Look up all IDs and get prismic docs
const pages = await client.getAllByIDs(results, {
ref,
fetchLinks: pageSlugFetchLinks,
})

return res.status(200).json(pages)
}
65 changes: 65 additions & 0 deletions pages/api/update-search.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import aws4 from 'aws4'
import { getPagesToIndex } from './search-index'
import * as moment from 'moment-timezone'

/**
* This endpoint looks for new posts and updates the search index
* It's a POST that's meant to be run by a cron
*/
export default async function updateSearch(req, res) {
if (req.method === 'POST') {
try {
const { authorization } = req.headers
// Only run this if the env secret matches the header's secret
if (authorization === `Bearer ${process.env.CRON_SECRET}`) {
// Look up modified pages in the last 1.5 hours
const offset = moment().add(-1.5, 'hours').valueOf()
const indexData = await getPagesToIndex(offset)

// nothing to import
if (!indexData || indexData.length === 0) {
return res.status(200).json({ success: true, empty: true })
}

// Store data as a file buffer to upload to aws
var buf = Buffer.from(JSON.stringify(indexData))

// aws4 will sign an options object as you'd pass to http.request, with an AWS service and region
var opts = {
host:
'doc-prismic-zi3vcm5qxhe7ua7mhcd4neqequ.us-west-2.cloudsearch.amazonaws.com',
path: '/2013-01-01/documents/batch',
service: 'cloudsearch',
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
region: 'us-west-2',
body: buf,
}

// aws4.sign() will sign and modify these options
const sign = aws4.sign(opts, {
accessKeyId: process.env.AWS_ACCESS,
secretAccessKey: process.env.AWS_SECRET,
})

const url = 'https://' + opts.host + opts.path
await fetch(url, {
method: 'POST',
headers: sign.headers,
body: buf,
})

res.status(200).json({ success: true })
} else {
res.status(401).json({ success: false })
}
} catch (err) {
res.status(500).json({ statusCode: 500, message: err.message })
}
} else {
res.setHeader('Allow', 'POST')
res.status(405).end('Method Not Allowed')
}
}
144 changes: 144 additions & 0 deletions pages/filter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { client } from 'lib/prismic'
import { useState } from 'react'
import useSWRInfinite from 'swr/infinite'

const fetcher = (url) => fetch(url).then((res) => res.json())
const PAGE_SIZE = 3

export default function Filter({ categories }) {
// Query docs here: https://docs.aws.amazon.com/cloudsearch/latest/developerguide/searching-compound-queries.html
const [filters, setFilters] = useState([])
const [searchTerm, setSearchTerm] = useState('')
// Reductive filter: posts contain all selected categories
// Additive filter: posts contain any of the selected categories
const [reductive, setReductive] = useState(true)

const { data, error, size, setSize } = useSWRInfinite(
(index) => {
let searchQuery =
searchTerm === '' ? `matchall` : `(phrase '${searchTerm}')`
const andOr = reductive ? 'and' : 'or'

if (filters.length > 0) {
let catString = ''
filters.forEach((category) => {
catString += ` category:'${category}'`
})
searchQuery = `(and (phrase '${searchTerm}') (${andOr} ${catString}))`
}
return `/api/search?q=${encodeURI(
searchQuery
)}&size=${PAGE_SIZE}&page=${index}`
},
fetcher,
{ revalidateOnFocus: false }
)

// useSWR boilerplate for load more button
const pages = data ? [].concat(...data) : []
const isLoadingInitialData = !data && !error
const isLoadingMore =
isLoadingInitialData ||
(size > 0 && data && typeof data[size - 1] === 'undefined')
const isEmpty = data?.[0]?.length === 0
const isReachingEnd =
isEmpty || (data && data[data.length - 1]?.length < PAGE_SIZE)

return (
<>
<button
onClick={() => {
setSearchTerm(``)
setFilters([])
}}>
All
</button>
<hr />
<input
type="checkbox"
id="reductive"
checked={reductive}
onChange={(event) => {
setReductive(event.currentTarget.checked)
}}
/>
<label htmlFor="reductive">Reductive</label>
<hr />
<label htmlFor="search">Search:</label>
<input
id="search"
type="text"
onKeyDown={(event) => {
if (event.key === 'Enter') {
setSearchTerm(event.target.value)
}
}}
/>
<hr />
{categories &&
categories.map((category) => {
{
return (
category?.data?.title && (
<div key={category.uid}>
<input
onChange={(event) => {
if (event.currentTarget.checked) {
if (!filters.includes(event.target.value)) {
setFilters((prevFilters) => [
...prevFilters,
category?.data?.title,
])
}
} else {
setFilters((prevFilters) =>
prevFilters.filter(
(item) => item !== category?.data?.title
)
)
}
}}
id={category.uid}
checked={filters.includes(category?.data?.title)}
type="checkbox"
/>
<label htmlFor={category.uid}>{category?.data?.title}</label>
</div>
)
)
}
})}
<hr />
{pages.map((page) => {
return <p key={page.id}>{page.data?.title}</p>
})}
<hr />
<button
disabled={isLoadingMore || isReachingEnd}
onClick={() => setSize(size + 1)}>
{isLoadingMore
? 'loading...'
: isReachingEnd
? 'no more pages'
: 'load more'}
</button>
</>
)
}

export async function getStaticProps({ preview = false, previewData }) {
const masterRef = await client.getMasterRef()
const ref = previewData?.ref || masterRef.ref
// Look up categories
const categories = await client.getAllByType('category', {
ref,
})

return {
props: {
preview,
categories: categories ?? null,
},
revalidate: 60,
}
}
Loading