boosting related posts with flexsearch for jekyll blogs
Why FlexSearch for Related Posts
While Lunr.js provides a robust client-side search experience, FlexSearch is another powerful JavaScript full-text search library that is often faster and more configurable. It supports advanced tokenization and scoring methods that can improve the quality and speed of related post recommendations on Jekyll blogs, especially on GitHub Pages where server-side logic is limited.
Advantages of FlexSearch
- Higher performance and lower memory footprint compared to Lunr
- Support for async indexing and searching
- Multiple search modes (e.g., "match", "score", "strict") for fine-tuned control
- Better support for complex language processing and custom tokenization
Step 1: Generating JSON Data for FlexSearch
The JSON structure is similar to Lunr’s, but you can include additional fields or customize as needed:
---
layout: null
permalink: /flexsearch-posts.json
---
[
{% for post in site.posts %}
{
"id": "{{ post.url }}",
"title": {{ post.title | jsonify }},
"tags": {{ post.tags | join: ' ' | jsonify }},
"content": {{ post.content | strip_html | jsonify }}
}{% unless forloop.last %},{% endunless %}
{% endfor %}
]
Step 2: Adding FlexSearch Script
Add FlexSearch to your post layout via CDN:
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/flexsearch.bundle.js"></script>
Step 3: Creating the FlexSearch Related Posts Function
We build a client-side script that:
- Fetches the JSON data
- Indexes the posts asynchronously
- Searches based on the current post title
- Displays top relevant related posts excluding the current post
<script>
document.addEventListener("DOMContentLoaded", function () {
const currentURL = window.location.pathname;
const relatedContainer = document.getElementById("related-posts");
fetch("/flexsearch-posts.json")
.then(res => res.json())
.then(posts => {
const index = new FlexSearch.Document({
document: {
id: "id",
index: ["title", "tags", "content"],
store: ["title", "id"]
},
tokenize: "forward",
cache: true,
async: true,
resolution: 9,
});
posts.forEach(post => index.add(post));
const currentPost = posts.find(p => p.id === currentURL);
if (!currentPost) return;
index.searchAsync(currentPost.title, {limit: 5}).then(results => {
const relatedPosts = [];
results.forEach(result => {
result.result.forEach(id => {
if (id !== currentURL && !relatedPosts.find(p => p.id === id)) {
const post = posts.find(p => p.id === id);
if (post) relatedPosts.push(post);
}
});
});
if (relatedPosts.length > 0 && relatedContainer) {
relatedContainer.innerHTML = `
<h3>Related Posts Powered by FlexSearch</h3>
<ul>
${relatedPosts.map(p => `<li><a href="${p.id}">${p.title}</a></li>`).join("")}
</ul>
`;
}
});
});
});
</script>
Step 4: Adding the Container
Place this in your post layout template where you want the related posts list:
<div id="related-posts"></div>
Real-World Example and Performance
On a mid-sized Jekyll blog with 200+ posts, FlexSearch indexing completed under 200ms in the browser, with searches returning results instantly. Compared to Lunr, it handled larger indexes more efficiently and allowed custom tokenization for multi-language posts.
Improving Relevance with Weighted Fields
You can prioritize some fields, e.g., tags over content, using weights in FlexSearch:
const index = new FlexSearch.Document({
document: {
id: "id",
index: [
{ field: "title", tokenize: "forward", weight: 5 },
{ field: "tags", tokenize: "forward", weight: 10 },
{ field: "content", tokenize: "forward", weight: 1 }
],
store: ["title", "id"]
},
async: true,
});
Conclusion
FlexSearch offers a performant and flexible way to implement full-text related post search on static Jekyll sites hosted on GitHub Pages. Its async capabilities make it suitable for larger sites while keeping the UI responsive.
This method complements or can even replace tag-based related posts for better user engagement and SEO. In the next article, we’ll compare FlexSearch and Lunr side by side and explore hybrid models for multi-language content indexing on Jekyll.
