Skip to content

Instantly share code, notes, and snippets.

@jbelke
Forked from r3wt/GhostSearch.js
Created August 30, 2021 13:53
Show Gist options
  • Save jbelke/6a8a42ffc2ef5d0e725dcd5fdc430548 to your computer and use it in GitHub Desktop.
Save jbelke/6a8a42ffc2ef5d0e725dcd5fdc430548 to your computer and use it in GitHub Desktop.
Search + Archive Implementation for a Ghost Blog
const app = require('express')();
const GhostSearch=require('./GhostSearch');
GhostSearch.start();
app.get('/blog/search',GhostSearch.search);
app.get('/blog/archives',GhostSearch.archive);
app.listen();
// begin config
const ghost_host = '<your host here>';//your blogs domain -- see documentation of tryghost/content-api
const ghost_key = '<your key here>';//key to your integration -- see documentation of tryghost/content-api
const updateInterval = 60000;//how often the index should update given in milliseconds
const requestTimeout = 5000;// maximum time for a search request before it times out given in milliseconds (note this isn't exact, it depends on the load of the event loop)
const resultsPerPage = [8];// allowed values for limit. in my case we only allow 8, but made this configurable so users of ghost can benefit
// end config
const elasticlunr = require('elasticlunr');
var index = elasticlunr(function () {
this.addField('title');
this.addField('meta_title');
this.addField('plaintext');
this.addField('meta_description');
this.addField('excerpt');
this.addField('custom_excerpt');
this.setRef('id');
});
const GhostContentAPI = require('@tryghost/content-api');
const blog = new GhostContentAPI({
host: ghost_host,
key: ghost_key,
version: 'v2'
});
var inprogress = 0,
indexWillUpdate = false,
indexReady = false
function updateIndex() {
return new Promise(updateIndex_impl);//kicks of the update by creating a promise and calling the internal updateIndex_impl
}
// this function accepts the resolve/reject from the promise and implements the logic for waiting until it can update the index, then doing so
// don't have to worry about uncaught promise errors, we just keep recursing
function updateIndex_impl(resolve,reject) {
if(inprogress > 0) {
setTimeout(()=>updateIndex_impl(resolve,reject),22);//just keep passing the original arguments
}else{
indexWillUpdate = true;
console.log('updating index');
return blog.posts.browse({limit:'all',formats:'html,plaintext'}).then(posts=>{
console.log('got posts from blog');
posts.forEach(post=>index.addDoc(post));//If docRef already exist, then update doc. <--taken straight from elasticlunr docs
indexWillUpdate=false;
indexReady = true;
console.log('index updated');
resolve();
})
.catch(err=>{
console.log('failed to update index');
console.log('debug info: indexWillUpdate=%s,indexReady=%s',indexWillUpdate?'true':'false',indexReady?'true':'false');
console.log(err);
return updateIndex_impl(resolve,reject);
})
}
}
function waitForIndex(timeout) {
return new Promise((resolve,reject)=>{
if(indexWillUpdate || !indexReady){
var elapsed = 0;
var _t = setInterval(()=>{
elapsed += 66;
if(!indexWillUpdate && indexReady){
clearInterval(_t);
resolve();
}else{
if(elapsed > timeout){
clearInterval(_t);
reject(new Error('timeout exceeded while waiting for index'));
}
}
},66);
}else{
resolve();
}
})
}
function search(req,res) {
const term = req.query.term || false;
const limit = req.query.limit ? ~~(req.query.limit) : resultsPerPage[0];
const page = (req.query.page ? ~~(req.query.page) : 1) - 1;
if(!term){
return res.status(400).json({posts:null,message:'missing required parameter `term`'});
}
if(resultsPerPage.indexOf(limit) === -1){
return res.status(400).json({posts:null,message:'supplied value for parameter `limit` is invalid'});
}
if(page < 0){
return res.status(400).json({posts:null,message:'supplied value for parameter `page` is invalid'});
}
var didIncr = false;//did not incr, so in case of error we only want to decrease counter of inprogress if didIncr = true
waitForIndex(requestTimeout).then(()=>{
inprogress +=1;
didIncr = true;//if we error after this, we have to decrease the counter
var startIndex = page * limit;
var endIndex = (page + 1) * limit;
var _posts = index.search(term,{
fields: {
title: { boost: 2, bool: 'AND' },
plaintext: { boost: 1, bool: 'AND' }
},
bool:'OR',
expand: true
});
var posts = _posts.map(item=>index.documentStore.getDoc(item.ref)).slice(startIndex,endIndex);
res.status(200).json({ posts,meta:{
pagination:{
page:page+1,
limit,
pages: Math.ceil(_posts.length/limit),
total: _posts.length,
}
} });
inprogress -= 1;
})
.catch(err=>{
console.log(err);
if(didIncr){
inprogress -=1;
}
res.status(500).json({posts:null,message:'500 internal server error'});
});
}
//kick it off
function routine(){
updateIndex().then(()=>setTimeout(routine,updateInterval)).catch(err=>{
console.log(err);
})
}
//export a function to start the Search Index
const start = ()=>routine();
// archive (gets posts by month like 01-2019
function getFirstAndLastDay(month,year) {
var d = new Date();
d.setFullYear(year);
d.setMonth(month-1);
d.setMinutes(0);
d.setSeconds(0);
d.setMilliseconds(0);
d.setHours(0);
var firstDay = new Date(d.getFullYear(), d.getMonth(), 1);
firstDay.setDate(firstDay.getDate());//ghost filter is exclusive, so we need to go back 1 day to capture first day of month
var lastDay = new Date(d.getFullYear(), d.getMonth() + 1, 0);
lastDay.setDate(lastDay.getDate());//same principal here as with firstDay
return {firstDay,lastDay};
}
function checkZero( n ) {
return n < 10 ? ('0'+n) : n;
}
function dateFormat( d ) {
return d.getFullYear()+'-'+checkZero(d.getMonth()+1)+'-'+checkZero(d.getDate());
}
function archive(req,res) {
const _month = req.query.month || false;
const page = (req.query.page ? ~~(req.query.page) : 1) - 1;
if(!_month){
return res.status(400).json({posts:null,message:'missing required parameter `month`'});
}
if(page < 0){
return res.status(400).json({posts:null,message:'supplied value for parameter `page` is invalid'});
}
if(!/^\d{2}\-\d{4}$/.test(_month)){
return res.status(400).json({posts:null,message:'supplied value for parameter `month`'});
}
const [month,year] = _month.split('-').map(v=>~~v);
const {firstDay,lastDay} = getFirstAndLastDay(month,year);
blog.posts.browse({ limit: 4, page,filter:`published_at:<='${dateFormat(lastDay)}' + published_at:>='${dateFormat(firstDay)}'`}).then(results=>{
console.log(results);
res.status(200).json({posts:results,meta:results.meta});
})
}
module.exports = {search,archive,start};

This is designed to be used with express. you could modify it to use with whatever. license is MIT, do as you want with it.

be sure to fill in your configuration information in GhostSearch.js file as shown in the first 6 lines.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment