Skip to content

Instantly share code, notes, and snippets.

@westc
Last active November 10, 2022 01:53
Show Gist options
  • Select an option

  • Save westc/b64f7fbbbc13fdd22f29b6f2c8449198 to your computer and use it in GitHub Desktop.

Select an option

Save westc/b64f7fbbbc13fdd22f29b6f2c8449198 to your computer and use it in GitHub Desktop.
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>
@B-uny

B-uny commented Jun 4, 2019

Copy link
Copy Markdown

Where could you filter the JSON results? Such as entry.filter(entry => entry.gsx$somedataname.$t.length > 7 );?

@B-uny

B-uny commented Jun 7, 2019

Copy link
Copy Markdown

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.

@masterika

masterika commented Apr 8, 2021

Copy link
Copy Markdown

Thanks for the great code! There is a typo, instead of "matchesTitle * 8 + matchesURL * 4 + matchesDescr * 2 + matchesKeywords * 1" it should be "matchesTitle * 8 + matchesURL * 4 + matchesDescr * 2 + matchesKeywords * 1+score", otherwise search engine only cares about the last word

@masterika

Copy link
Copy Markdown

Something changed about google sheets APIs, I get the following error: The specified GSHEET_JSON_URL does not contain JSON:
https://spreadsheets.google.com/feeds/list/1c2aJmDdLkbjW0ErfUB0sfiQ-pt6zdW5KWZgREWs0zvM/1/public/values?alt=json

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