A simple search engine that only uses JavaScript and Google sheets. NO DATABASE REQUIRED!
<!DOCTYPE html> | |
<html> | |
<head> | |
<title>Custom Search Engine Using Google Sheets</title> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"></script> | |
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.3/js/bootstrap.min.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.3/vue.min.js"></script> | |
<script type="text/javascript"> | |
$(function() { | |
var GSHEET_JSON_URL = 'https://spreadsheets.google.com/feeds/list/1c2aJmDdLkbjW0ErfUB0sfiQ-pt6zdW5KWZgREWs0zvM/1/public/values?alt=json'; | |
var vueSearch = new Vue({ | |
el: '#vueSearch', | |
data: { | |
search: '', | |
links: [], | |
pages: [], | |
pageNumber: 1, | |
results: [], | |
perPage: 10, | |
loading: true, | |
error: null | |
}, | |
methods: { | |
changePage: function(number) { | |
location.href = '#vueSearch'; | |
this.pageNumber = number; | |
this.updateResults(); | |
}, | |
updateResults: function() { | |
var self = this; | |
// Clear all previous paging data | |
self.pages.splice(0, Infinity); | |
// Get an array of search terms as regexes | |
var searchTerms = []; | |
self.search.replace( | |
/([+\-])?(?:"([^"]+)"|(\/((?:[^\/]|\\.)+)\/(i?)|[\S]+))/g, | |
function(m, must, quoted, nonQuoted, rgxText, rgxOpts) { | |
var realRgxText = rgxText; | |
if (rgxText) { | |
try { | |
new RegExp(rgxText); | |
} | |
catch (e) { | |
rgxText = null; | |
} | |
} | |
rgxText = rgxText || ( | |
((quoted ? /^\b/.test(quoted) : !realRgxText) ? '\\b' : '') | |
+ YourJS.quoteRegExp(quoted || nonQuoted) | |
+ ((quoted && /\b$/.test(quoted)) ? '\\b' : '') | |
); | |
searchTerms.push({ | |
type: realRgxText ? 'regex' : 'string', | |
rgx: new RegExp(rgxText, (realRgxText ? rgxOpts : 'i') + 'g'), | |
must: must == '+' | |
? true | |
: must == '-' | |
? false | |
: undefined | |
}); | |
} | |
); | |
// Get the resulting list of links based on the scores. | |
var results = self.links | |
.reduce(function(scores, link, linkIndex) { | |
var score = searchTerms.reduce(function(score, searchTerm) { | |
if (score !== false) { | |
var matchesTitle = (link.title.match(searchTerm.rgx) || '').length; | |
var matchesURL = (link.url.match(searchTerm.rgx) || '').length; | |
var matchesDescr = ((link.description || '').match(searchTerm.rgx) || '').length; | |
var matchesKeywords = ((link.keywords || '').match(searchTerm.rgx) || '').length; | |
var matchesAny = !!(matchesTitle || matchesURL || matchesDescr || matchesKeywords); | |
return searchTerm.must !== !matchesAny | |
? searchTerm.must !== false | |
? matchesTitle * 8 + matchesURL * 4 + matchesDescr * 2 + matchesKeywords * 1 | |
: 1 | |
: false; | |
} | |
return score; | |
}, searchTerms.length ? 0 : 1); | |
if (score) { | |
scores.push({ link: link, score: score, index: linkIndex }); | |
} | |
return scores; | |
}, []) | |
.sort(function(a, b) { return (b.score - a.score) || (b.index - a.index); }) | |
.map(function(score) { return score.link; }); | |
// Setup the paging data | |
var pageCount = Math.max(1, Math.ceil(results.length / self.perPage)); | |
var pageNumber = self.pageNumber = YourJS.clamp(self.pageNumber, 1, pageCount); | |
if (pageNumber > 1) { | |
self.pages.push(self.getPageObject(1, results)); | |
} | |
if (pageNumber > 2) { | |
self.pages.push(self.getPageObject(pageNumber - 1, results)); | |
} | |
self.pages.push(self.getPageObject(pageNumber, results)); | |
if (pageNumber + 1 < pageCount) { | |
self.pages.push(self.getPageObject(pageNumber + 1, results)); | |
} | |
if (pageNumber < pageCount) { | |
self.pages.push(self.getPageObject(pageCount, results)); | |
} | |
// Set self.results to only the results that are shown in the current page. | |
self.results = results.slice((pageNumber - 1) * self.perPage, pageNumber * self.perPage).map(function(link) { | |
link = jQuery.extend({}, link); | |
return { | |
title: self.markTerms(link.title, searchTerms), | |
description: self.markTerms(link.description, searchTerms), | |
url: link.url | |
}; | |
}); | |
}, | |
markTerms: function(strIn, terms) { | |
return terms | |
.reduce(function(str, term) { | |
return (YourJS.matchAll(strIn, term.rgx) || []).reduce(function(str, termMatch) { | |
return str.replace( | |
new RegExp('((?:[\\x01\\x02]*[^\\x01\\x02]){' + termMatch.index + '})((?:[\\x01\\x02]*[^\\x01\\x02]){' + (termMatch[0].length) + '})'), | |
'$1\x01$2\x02' | |
); | |
}, str); | |
}, strIn) | |
.replace(/\x01/g, '<div class="highlight">').replace(/\x02/g, '</div>'); | |
}, | |
getPageObject: function(pageNumber, results) { | |
return { | |
text: pageNumber, | |
number: pageNumber, | |
caption: Math.min(results.length, 1 + (pageNumber - 1) * this.perPage) + ' to ' + Math.min(results.length, pageNumber * this.perPage) | |
}; | |
} | |
}, | |
watch: { | |
search: function(newValue, oldValue) { | |
this.pageNumber = 1; | |
this.updateResults(); | |
} | |
}, | |
mounted: function () { | |
$.ajax({ | |
dataType: "json", | |
url: GSHEET_JSON_URL + '&cache-buster=' + Math.random(), | |
success: function(data) { | |
if (data && data.feed && data.feed.entry && data.feed.entry[0]) { | |
var missing = ['url', 'title', 'description', 'keywords'].filter(function(name) { | |
return !data.feed.entry[0]['gsx$' + name]; | |
}); | |
if (missing[0]) { | |
vueSearch.error = 'You must add the "`' + missing.join('`", "`').replace(/, (?!.*,)/, ', and ') + '`" field' + (missing[1] ? 's' : '') + '.'; | |
return; | |
} | |
} | |
else { | |
vueSearch.error = 'The specified GSHEET_JSON_URL is invalid:<br>`' + GSHEET_JSON_URL + '`'; | |
return; | |
} | |
data.feed.entry.forEach(function(entry) { | |
vueSearch.links.push({ | |
url: entry.gsx$url.$t, | |
title: entry.gsx$title.$t, | |
description: entry.gsx$description.$t, | |
keywords: entry.gsx$keywords.$t | |
}); | |
}); | |
vueSearch.loading = false; | |
vueSearch.updateResults(); | |
}, | |
error: function() { | |
vueSearch.error = 'The specified GSHEET_JSON_URL does not contain JSON:<br>`' + GSHEET_JSON_URL + '`'; | |
vueSearch.loading = false; | |
} | |
}); | |
// Set focus to search box | |
$('#txtQuery').select(); | |
} | |
}); | |
}); | |
/* | |
YourJS - Your Very Own JS Library | |
http://yourjs.com | |
Copyright (c) 2015-2017 Christopher West | |
Licensed under the MIT license. | |
*/ | |
(function(h,c,k){function l(a,b){return function(){return a[b].apply(a,arguments)}}var g=this,m;(function(a,b){m=function(){b||(b=1,g[c]=a);return d}})(g[c]);var n=l([].slice,"call");var d={alias:l,clamp:function(a,b,f){return a<b?b:a>f?f:a},info:function(){return{name:c,version:h,toString:d.toString}},matchAll:function(a,b,f){var c=0,d=[];a.replace(b,function(e){e=n(arguments,0,-1);e.index=e.pop();e.input=a;e.source=b;f&&(e=f(e,++c));e!==k&&d.push(e)});return d.length?d:null},noConflict:m,quoteRegExp:function(a, | |
b){var c=a.replace(/[[\](){}.+*^$|\\?-]/g,"\\$&");return""===b||b?new RegExp(c,!0===b?"":b):c},slice:n,toString:function(){return"YourJS v"+h+" ("+c+")"}};[].forEach(function(a){a()});"undefined"!==typeof exports?("undefined"!==typeof module&&module.exports&&(exports=module.exports=d),(exports[c]=d)[c]=k):g[c]=d})("2.2.0","YourJS"); | |
</script> | |
<link rel="stylesheet" type="text/css" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.3/css/bootstrap.min.css"> | |
<style type="text/css"> | |
body { | |
padding: 10px; | |
} | |
[v-cloak] { display: none; } | |
div.highlight { | |
display: inline; | |
box-shadow: 0 0 0.5em red; | |
padding: 0.125em; | |
border-radius: 0.5em; | |
} | |
.btn { | |
background-image: linear-gradient(to bottom, rgba(255,255,255,0.5), rgba(255,255,255,0.2) 49%, rgba(0,0,0,0.15) 51%, rgba(0,0,0,0.05)); | |
background-repeat: repeat-x; | |
} | |
</style> | |
</head> | |
<body> | |
<h1>Custom Search Engine Using Google Sheets</h1> | |
<p class="lead">A simple search engine built just JavaScript and <a href="https://docs.google.com/spreadsheets/d/1c2aJmDdLkbjW0ErfUB0sfiQ-pt6zdW5KWZgREWs0zvM/edit?usp=sharing" target="_blank">this Google sheet document</a>. The full version of this search can be found at <a href="http://gotochriswest.com/search">GoToChrisWest.com</a>.</p> | |
<div id="vueSearch" v-cloak> | |
<div class="input-group mb-3"> | |
<div class="input-group-prepend"> | |
<label class="input-group-text" for="txtQuery">Search Terms</label> | |
</div> | |
<input type="text" id="txtQuery" v-model="search" class="form-control" placeholder="whatever you want to search for..."> | |
</div> | |
<div v-for="(link, linkIndex) in results" v-bind:style="linkIndex + 1 < results.length ? 'margin-bottom: 1em;' : ''"> | |
<div style="font-size: 1.25em;"><a v-bind:href="link.url" target="_blank" v-html="link.title"></a></div> | |
<div v-html="link.description"></div> | |
<div style="color: #080;">{{ link.url }}</div> | |
</div> | |
<div v-if="error" class="alert alert-danger text-center lead" v-html="error.replace(/`([^`]+)`/g, '<code>$1</code>')">{{error}}</div> | |
<div v-else-if="loading" class="text-center lead">Loading search results from Google Sheets...</div> | |
<div v-else-if="!results.length" class="text-center lead"> | |
No results were found for <code>{{ search }}</code>. | |
</div> | |
<div class="text-center"> | |
<div class="d-inline-block"> | |
<div class="input-group mb-3" v-if="pages.length"> | |
<div class="input-group-prepend"> | |
<span class="input-group-text" for="txtQuery">Pages</span> | |
</div> | |
<div class="input-group-append"> | |
<button v-for="page in pages" v-bind:class="'btn btn-outline-secondary' + (page.number == pageNumber ? ' active' : '')" type="button" style="float: initial;" v-on:click="changePage(page.number)" v-bind:title="page.caption">{{ page.text }}</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</body> | |
</html> |
This comment has been minimized.
This comment has been minimized.
Figured out my own question for how to filter. You can preview it here on stack overflow if anyone ever needs it. I may build a fork if I ever get the time. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This comment has been minimized.
Where could you filter the JSON results? Such as
entry.filter(entry => entry.gsx$somedataname.$t.length > 7 );
?