Add built-in search engine

Complement existing integration of Algolia search engine with
alternative option of built-in search engine based on Fuse.

Setting `engine = 1` in `config.toml` will enable the built-in solution.

Close #635
This commit is contained in:
George Cushen committed 2018-08-12 15:39:55 +01:00
1 parent 3e9f7dab03
commit a11d8843bb
8 files changed
+254 -16

No files matched your search

+8
View File
@@ -47,6 +47,14 @@
version = "3.2.5"
sri = "sha256-X5PoE3KU5l+JcX+w09p/wHl9AzK333C4hJ2I9S5mD4M="
url = "https://cdnjs.cloudflare.com/ajax/libs/fancybox/%s/jquery.fancybox.min.js"
[js.fuse]
version = "3.2.1"
sri = "sha256-VzgmKYmhsGNNN4Ph1kMW+BjoYJM2jV5i4IlFoeZA9XI="
url = "https://cdnjs.cloudflare.com/ajax/libs/fuse.js/%s/fuse.min.js"
[js.mark]
version = "8.11.1"
sri = "sha256-4HLtjeVgH0eIB3aZ9mLYF6E8oU5chNdjU6p6rrXpl9U="
url = "https://cdnjs.cloudflare.com/ajax/libs/mark.js/%s/jquery.mark.min.js"
[js.instantsearch]
version = "2.9.0"
sri = "sha256-cJXigylnJlxvAdfFNHeS+IiMcKWS3Rf/cy3bl9bb0ng="
+3 -2
View File
@@ -306,8 +306,9 @@ enableGitInfo = false
[params.search]
# Search provider:
# 0: No search engine
# 1: Algolia (https://www.algolia.com)
engine = 0
# 1: Built-in (Fuse)
# 2: Algolia (https://www.algolia.com)
engine = 1
# Configuration of Algolia search engine.
# Paste the values from your Algolia dashboard.
+1 -1
View File
@@ -1,7 +1,7 @@
+++
# Search widget.
widget = "search"
active = false
active = true
date = 2018-07-23T00:00:00
title = "Search"
+27
View File
@@ -247,9 +247,32 @@ small,
**************************************************/
#search-box {
position: relative; /* Required for search icon positioning. */
margin-bottom: 0.5rem;
}
#search-box::before {
content: "\f002";
font-family: FontAwesome;
font-size: 1rem;
opacity: 0.25;
line-height: 1rem;
position: absolute;
left: 0.7rem;
top: 0.6rem;
overflow-x: hidden;
}
#search-query {
border: 1px solid #dedede;
border-radius: 1rem;
padding: 1rem 1rem 1rem 2rem; /* Wider left padding for search icon to fit in. */
width: 250px;
line-height: 1rem;
height: 1rem;
font-size: 0.8rem;
}
.search-hit em {
font-style: normal;
background-color: #FFE0B2;
@@ -1620,6 +1643,10 @@ body.dark {
background-color: rgb(68, 71, 90);
}
.dark #search-query {
background-color: rgb(68, 71, 90);
}
.dark .label-default {
color: rgba(255, 255, 255, .68);
background: rgba(255, 255, 255, .2);
+28 -11
View File
@@ -63,20 +63,37 @@
<script>hljs.initHighlightingOnLoad();</script>
{{ end }}
{{/* Algolia search engine. */}}
{{ if ne .Site.Params.search.engine 0 }}
{{/* Configure search engine. */}}
<script>
const search_index_filename = {{ "/search.json" | relURL }};
const i18n = {
'placeholder': {{ i18n "search_placeholder" }},
'no_results': {{ i18n "search_no_results" }}
};
const content_type = {
'post': {{ i18n "posts" }},
'project': {{ i18n "projects" }},
'publication' : {{ i18n "publications" }},
'talk' : {{ i18n "talks" }}
};
</script>
{{ end }}
{{/* Fuse search engine. */}}
{{ if eq .Site.Params.search.engine 1 }}
{{ printf "<script src=\"%s\" integrity=\"%s\" crossorigin=\"anonymous\"></script>" (printf $js.fuse.url $js.fuse.version) $js.fuse.sri | safeHTML }}
{{ printf "<script src=\"%s\" integrity=\"%s\" crossorigin=\"anonymous\"></script>" (printf $js.mark.url $js.mark.version) $js.mark.sri | safeHTML }}
<script src="{{ "/js/search.js" | relURL }}"></script>
{{ end }}
{{/* Algolia search engine. */}}
{{ if eq .Site.Params.search.engine 2 }}
{{ if ($scr.Get "use_cdn") }}
{{ printf "<script src=\"%s\" integrity=\"%s\" crossorigin=\"anonymous\"></script>" (printf $js.instantsearch.url $js.instantsearch.version) $js.instantsearch.sri | safeHTML }}
{{ end }}
<script>
if ( $('#search-box').length ) {
const content_type = {
'post': {{ i18n "posts" }},
'project': {{ i18n "projects" }},
'publication' : {{ i18n "publications" }},
'talk' : {{ i18n "talks" }}
};
function getTemplate(templateName) {
return document.querySelector(`#${templateName}-template`).innerHTML;
}
@@ -108,7 +125,7 @@
container: '#search-box',
autofocus: false,
poweredBy: {{ .Site.Params.search.algolia.show_logo | default false }},
placeholder: {{ i18n "search_placeholder" }}
placeholder: i18n.placeholder
})
);
@@ -118,8 +135,8 @@
container: '#search-hits',
escapeHits: true,
templates: {
empty: '<div class="search-no-results">' + {{ i18n "search_no_results" }} + '</div>',
item: getTemplate('search-hit')
empty: '<div class="search-no-results">' + i18n.no_results + '</div>',
item: getTemplate('search-hit-algolia')
},
cssClasses: {
showmoreButton: 'btn btn-primary btn-outline'
+1 -1
View File
@@ -71,7 +71,7 @@
{{ printf "<link rel=\"stylesheet\" href=\"%s\" integrity=\"%s\" crossorigin=\"anonymous\">" (printf $css.leaflet.url $css.leaflet.version) $css.leaflet.sri | safeHTML }}
{{ end }}
{{ if eq .Site.Params.search.engine 1 }}
{{ if eq .Site.Params.search.engine 2 }}
{{ printf "<link rel=\"stylesheet\" href=\"%s\" integrity=\"%s\" crossorigin=\"anonymous\">" (printf $css.instantsearch.url $css.instantsearch.version) $css.instantsearch.sri | safeHTML }}
{{ printf "<link rel=\"stylesheet\" href=\"%s\" integrity=\"%s\" crossorigin=\"anonymous\">" (printf $css.instantsearchTheme.url $css.instantsearchTheme.version) $css.instantsearchTheme.sri | safeHTML }}
{{ end }}
+21 -1
View File
@@ -10,7 +10,11 @@
{{ with $page.Content }}<p>{{ . | markdownify }}</p>{{ end }}
<div id="search-box">
{{ if eq $.Site.Params.search.engine 1 }}
<input name="q" id="search-query" placeholder="{{ i18n "search_placeholder" }}" autocapitalize="off" autocomplete="off" autocorrect="off" role="textbox" spellcheck="false" type="search">
{{ else }}
<!-- Search box will appear here -->
{{ end }}
</div>
<div id="search-hits">
@@ -20,7 +24,22 @@
</div>
</div>
<script type="text/html" id="search-hit-template">
{{ if eq $.Site.Params.search.engine 1 }}
{{/* Fuse search result template. */}}
<script id="search-hit-fuse-template" type="text/x-template">
<div class="search-hit" id="summary-{{"{{key}}"}}">
<div class="search-hit-content">
<div class="search-hit-name">
{{ printf "<a href=\"%s\">%s</a>" "{{relpermalink}}" "{{title}}" | safeHTML }}
<div class="article-metadata search-hit-type">{{"{{type}}"}}</div>
<p class="search-hit-description">{{"{{snippet}}"}}</p>
</div>
</div>
</div>
</script>
{{ else }}
{{/* Algolia search result template. */}}
<script id="search-hit-algolia-template" type="text/html">
<div class="search-hit">
<div class="search-hit-content">
<div class="search-hit-name">
@@ -31,3 +50,4 @@
</div>
</div>
</script>
{{ end }}
+165
View File
@@ -0,0 +1,165 @@
/*************************************************
* Academic: the website framework for Hugo.
* https://github.com/gcushen/hugo-academic
**************************************************/
/* ---------------------------------------------------------------------------
* Configuration.
* --------------------------------------------------------------------------- */
// Configure Fuse.
let fuseOptions = {
shouldSort: true,
includeMatches: true,
tokenize: true,
threshold: 0.0,
location: 0,
distance: 100,
maxPatternLength: 32,
minMatchCharLength: 2,
keys: [
{name:'title', weight:0.8},
{name:'summary', weight:0.6},
{name:'content', weight:0.5},
{name:'tags', weight:0.3}
]
};
// Configure summary.
let summaryLength = 60;
/* ---------------------------------------------------------------------------
* Functions.
* --------------------------------------------------------------------------- */
// Get query from URI.
function getSearchQuery(name) {
return decodeURIComponent((location.search.split(name + '=')[1] || '').split('&')[0]).replace(/\+/g, ' ');
}
// Set query in URI without reloading the page.
function updateURL(url) {
if (history.pushState) {
window.history.pushState({path:url}, '', url);
}
}
// Pre-process new search query.
function initSearch(force, fuse) {
let query = $("#search-query").val();
// If query deleted, clear results.
if ( query.length < 1)
$('#search-hits').empty();
// Check for timer event (enter key not pressed) and query less than minimum length required.
if (!force && query.length < fuseOptions.minMatchCharLength)
return;
// Do search.
$('#search-hits').empty();
searchAcademic(query, fuse);
let newURL = window.location.protocol + "//" + window.location.host + window.location.pathname + '?q=' + encodeURIComponent(query) + window.location.hash;
updateURL(newURL);
}
// Perform search.
function searchAcademic(query, fuse) {
let results = fuse.search(query);
// console.log({"results": results});
if (results.length > 0) {
parseResults(query, results);
} else {
$('#search-hits').append('<div class="search-no-results">' + i18n.no_results + '</div>');
}
}
// Parse search results.
function parseResults(query, results) {
$.each( results, function(key, value) {
let content = value.item.content;
let snippet = "";
let snippetHighlights = [];
if ( fuseOptions.tokenize ) {
snippetHighlights.push(query);
} else {
$.each( value.matches, function(matchKey, matchValue) {
if (matchValue.key == "content") {
let start = (matchValue.indices[0][0]-summaryLength>0) ? matchValue.indices[0][0]-summaryLength : 0;
let end = (matchValue.indices[0][1]+summaryLength<content.length) ? matchValue.indices[0][1]+summaryLength : content.length;
snippet += content.substring(start, end);
snippetHighlights.push(matchValue.value.substring(matchValue.indices[0][0], matchValue.indices[0][1]-matchValue.indices[0][0]+1));
}
});
}
if (snippet.length < 1) {
snippet += content.substring(0, summaryLength*2);
}
// Load template.
var template = $('#search-hit-fuse-template').html();
// Localize content types.
let content_key = value.item.type;
if (content_key in content_type) {
content_key = content_type[content_key];
}
// Parse template.
let templateData = {
key: key,
title: value.item.title,
type: content_key,
relpermalink: value.item.relpermalink,
snippet: snippet
};
let output = render(template, templateData);
$('#search-hits').append(output);
// Highlight search terms in result.
$.each( snippetHighlights, function(hlKey, hlValue){
$("#summary-"+key).mark(hlValue);
});
});
}
function render(template, data) {
// Replace placeholders with their values.
let key, find, re;
for (key in data) {
find = '\\{\\{\\s*' + key + '\\s*\\}\\}'; // Expect placeholder in the form `{{x}}`.
re = new RegExp(find, 'g');
template = template.replace(re, data[key]);
}
return template;
}
/* ---------------------------------------------------------------------------
* Initialize.
* --------------------------------------------------------------------------- */
// Wait for Fuse to initialize.
$.getJSON( search_index_filename, function( search_index ) {
let fuse = new Fuse(search_index, fuseOptions);
// On page load, check for search query in URL.
if (query = getSearchQuery('q')) {
$("#search-query").val(query);
initSearch(true, fuse);
}
// On search box key up, process query.
$('#search-query').keyup(function (e) {
clearTimeout($.data(this, 'searchTimer')); // Ensure only one timer runs!
if (e.keyCode == 13) {
initSearch(true, fuse);
} else {
$(this).data('searchTimer', setTimeout(function () {
initSearch(false, fuse);
}, 250));
}
});
});