Skip to content

Instantly share code, notes, and snippets.

@r3wt
Created February 25, 2019 18:10
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save r3wt/fbaf844a9864e1d101fdb08e345853f2 to your computer and use it in GitHub Desktop.
Save r3wt/fbaf844a9864e1d101fdb08e345853f2 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