Skip to content

Commit

Permalink
added basic and ugly but working search
Browse files Browse the repository at this point in the history
  • Loading branch information
deadbaed committed Jan 1, 2024
1 parent 9a714a8 commit 5aaee87
Show file tree
Hide file tree
Showing 3 changed files with 217 additions and 0 deletions.
8 changes: 8 additions & 0 deletions config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,19 @@ description = "broke my bed now it's dead"
# The URL the site will be built for
base_url = "https://blog.x4m3.rocks"

# css
compile_sass = true

# rss
generate_feed = true
feed_filename = "atom.xml"

# search
default_language = "en"
build_search_index = true
[search]
index_format = "elasticlunr_json"

[markdown]
highlight_code = true
highlight_theme = "gruvbox-light"
Expand Down
199 changes: 199 additions & 0 deletions static/search.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
function debounce(func, wait) {
var timeout;

return function () {
var context = this;
var args = arguments;
clearTimeout(timeout);

timeout = setTimeout(function () {
timeout = null;
func.apply(context, args);
}, wait);
};
}

// Taken from mdbook
// The strategy is as follows:
// First, assign a value to each word in the document:
// Words that correspond to search terms (stemmer aware): 40
// Normal words: 2
// First word in a sentence: 8
// Then use a sliding window with a constant number of words and count the
// sum of the values of the words within the window. Then use the window that got the
// maximum sum. If there are multiple maximas, then get the last one.
// Enclose the terms in <b>.
function makeTeaser(body, terms) {
var TERM_WEIGHT = 40;
var NORMAL_WORD_WEIGHT = 2;
var FIRST_WORD_WEIGHT = 8;
var TEASER_MAX_WORDS = 30;

var stemmedTerms = terms.map(function (w) {
return elasticlunr.stemmer(w.toLowerCase());
});
var termFound = false;
var index = 0;
var weighted = []; // contains elements of ["word", weight, index_in_document]

// split in sentences, then words
var sentences = body.toLowerCase().split(". ");

for (var i in sentences) {
var words = sentences[i].split(" ");
var value = FIRST_WORD_WEIGHT;

for (var j in words) {
var word = words[j];

if (word.length > 0) {
for (var k in stemmedTerms) {
if (elasticlunr.stemmer(word).startsWith(stemmedTerms[k])) {
value = TERM_WEIGHT;
termFound = true;
}
}
weighted.push([word, value, index]);
value = NORMAL_WORD_WEIGHT;
}

index += word.length;
index += 1; // ' ' or '.' if last word in sentence
}

index += 1; // because we split at a two-char boundary '. '
}

if (weighted.length === 0) {
return body;
}

var windowWeights = [];
var windowSize = Math.min(weighted.length, TEASER_MAX_WORDS);
// We add a window with all the weights first
var curSum = 0;
for (var i = 0; i < windowSize; i++) {
curSum += weighted[i][1];
}
windowWeights.push(curSum);

for (var i = 0; i < weighted.length - windowSize; i++) {
curSum -= weighted[i][1];
curSum += weighted[i + windowSize][1];
windowWeights.push(curSum);
}

// If we didn't find the term, just pick the first window
var maxSumIndex = 0;
if (termFound) {
var maxFound = 0;
// backwards
for (var i = windowWeights.length - 1; i >= 0; i--) {
if (windowWeights[i] > maxFound) {
maxFound = windowWeights[i];
maxSumIndex = i;
}
}
}

var teaser = [];
var startIndex = weighted[maxSumIndex][2];
for (var i = maxSumIndex; i < maxSumIndex + windowSize; i++) {
var word = weighted[i];
if (startIndex < word[2]) {
// missing text from index to start of `word`
teaser.push(body.substring(startIndex, word[2]));
startIndex = word[2];
}

// add <em/> around search terms
if (word[1] === TERM_WEIGHT) {
teaser.push("<b>");
}
startIndex = word[2] + word[0].length;
teaser.push(body.substring(word[2], startIndex));

if (word[1] === TERM_WEIGHT) {
teaser.push("</b>");
}
}
teaser.push("…");
return teaser.join("");
}

function formatSearchResultItem(item, terms) {
return '<div class="search-results__item">'
+ `<a href="${item.ref}">${item.doc.title}</a>`
+ `<div>${makeTeaser(item.doc.body, terms)}</div>`
+ '</div>';
}

function initSearch() {
var $searchInput = document.getElementById("search");
var $searchResults = document.querySelector(".search-results");
var $searchResultsItems = document.querySelector(".search-results__items");
var MAX_ITEMS = 10;

var options = {
bool: "AND",
fields: {
title: {boost: 2},
body: {boost: 1},
}
};
var currentTerm = "";
var index;

var initIndex = async function () {
if (index === undefined) {
index = fetch("/search_index.en.json")
.then(
async function(response) {
return await elasticlunr.Index.load(await response.json());
}
);
}
let res = await index;
return res;
}

$searchInput.addEventListener("keyup", debounce(async function() {
var term = $searchInput.value.trim();
if (term === currentTerm) {
return;
}
$searchResults.style.display = term === "" ? "none" : "block";
$searchResultsItems.innerHTML = "";
currentTerm = term;
if (term === "") {
return;
}

var results = (await initIndex()).search(term, options);
if (results.length === 0) {
$searchResults.style.display = "none";
return;
}

for (var i = 0; i < Math.min(results.length, MAX_ITEMS); i++) {
var item = document.createElement("li");
item.innerHTML = formatSearchResultItem(results[i], term.split(" "));
$searchResultsItems.appendChild(item);
}
}, 150));

window.addEventListener('click', function(e) {
if ($searchResults.style.display == "block" && !$searchResults.contains(e.target)) {
$searchResults.style.display = "none";
}
});
}


if (document.readyState === "complete" ||
(document.readyState !== "loading" && !document.documentElement.doScroll)
) {
initSearch();
} else {
document.addEventListener("DOMContentLoaded", initSearch);
}
10 changes: 10 additions & 0 deletions templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@ <h3 style="margin-top:0;">
<a href="{{ config.base_url | safe }}" title="Home">{{ config.title }}</a>
<br /><small>{{ config.description }}</small>
</h3>

<div class="search-container">
<input type="text" id="search" placeholder="Search..." style="width: 100%;">
<div class="search-results">
<div class="search-results__items"></div>
</div>
</div>
</header>
<hr />
{% endblock header %}
Expand Down Expand Up @@ -92,6 +99,9 @@ <h3 style="margin-top:0;">
</main>
{% block after_main %}
{% endblock after_main %}

<script type="text/javascript" src="{{ get_url(path="elasticlunr.min.js") }}"></script>
<script type="text/javascript" src="{{ get_url(path="search.js") }}"></script>
</body>
</html>

0 comments on commit 5aaee87

Please sign in to comment.