Search json index (#1998)

* search: Add support for a JSON index

* docs: Document JSON index for search

* docs: Use lazy-loaded JSON index

* Add elasticlunr prefix to search engine format configuration

This will be useful if support for more search libraries are added in the future
This commit is contained in:
Sosthene 2022-10-30 21:55:47 +01:00 committed by Vincent Prouillet
parent 291c93e4ba
commit 7000f787b3
8 changed files with 73 additions and 26 deletions

View File

@ -1,5 +1,18 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum IndexFormat {
ElasticlunrJson,
ElasticlunrJavascript,
}
impl Default for IndexFormat {
fn default() -> IndexFormat {
IndexFormat::ElasticlunrJavascript
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)] #[serde(default)]
pub struct Search { pub struct Search {
@ -15,6 +28,8 @@ pub struct Search {
pub include_description: bool, pub include_description: bool,
/// Include the path of the page in the search index. `false` by default. /// Include the path of the page in the search index. `false` by default.
pub include_path: bool, pub include_path: bool,
/// Foramt of the search index to be produced. Javascript by default
pub index_format: IndexFormat,
} }
impl Default for Search { impl Default for Search {
@ -25,6 +40,7 @@ impl Default for Search {
include_description: false, include_description: false,
include_path: false, include_path: false,
truncate_content_length: None, truncate_content_length: None,
index_format: Default::default(),
} }
} }
} }

View File

@ -5,8 +5,13 @@ mod theme;
use std::path::Path; use std::path::Path;
pub use crate::config::{ pub use crate::config::{
languages::LanguageOptions, link_checker::LinkChecker, link_checker::LinkCheckerLevel, languages::LanguageOptions,
search::Search, slugify::Slugify, taxonomies::TaxonomyConfig, Config, link_checker::LinkChecker,
link_checker::LinkCheckerLevel,
search::{IndexFormat, Search},
slugify::Slugify,
taxonomies::TaxonomyConfig,
Config,
}; };
use errors::Result; use errors::Result;

View File

@ -15,7 +15,7 @@ use libs::rayon::prelude::*;
use libs::tera::{Context, Tera}; use libs::tera::{Context, Tera};
use libs::walkdir::{DirEntry, WalkDir}; use libs::walkdir::{DirEntry, WalkDir};
use config::{get_config, Config}; use config::{get_config, Config, IndexFormat};
use content::{Library, Page, Paginator, Section, Taxonomy}; use content::{Library, Page, Paginator, Section, Taxonomy};
use errors::{anyhow, bail, Context as ErrorContext, Result}; use errors::{anyhow, bail, Context as ErrorContext, Result};
use libs::relative_path::RelativePathBuf; use libs::relative_path::RelativePathBuf;
@ -764,32 +764,36 @@ impl Site {
Ok(()) Ok(())
} }
fn index_for_lang(&self, lang: &str) -> Result<()> {
let index_json = search::build_index(
&self.config.default_language,
&self.library.read().unwrap(),
&self.config,
)?;
let (path, content) = match &self.config.search.index_format {
IndexFormat::ElasticlunrJson => {
let path = self.output_path.join(&format!("search_index.{}.json", lang));
(path, index_json)
}
IndexFormat::ElasticlunrJavascript => {
let path = self.output_path.join(&format!("search_index.{}.js", lang));
let content = format!("window.searchIndex = {};", index_json);
(path, content)
}
};
create_file(&path, &content)
}
pub fn build_search_index(&self) -> Result<()> { pub fn build_search_index(&self) -> Result<()> {
ensure_directory_exists(&self.output_path)?; ensure_directory_exists(&self.output_path)?;
// TODO: add those to the SITE_CONTENT map // TODO: add those to the SITE_CONTENT map
// index first // index first
create_file( self.index_for_lang(&self.config.default_language)?;
&self.output_path.join(&format!("search_index.{}.js", self.config.default_language)),
&format!(
"window.searchIndex = {};",
search::build_index(
&self.config.default_language,
&self.library.read().unwrap(),
&self.config
)?
),
)?;
for (code, language) in &self.config.other_languages() { for (code, language) in &self.config.other_languages() {
if code != &self.config.default_language && language.build_search_index { if code != &self.config.default_language && language.build_search_index {
create_file( self.index_for_lang(code)?;
&self.output_path.join(&format!("search_index.{}.js", &code)),
&format!(
"window.searchIndex = {};",
search::build_index(code, &self.library.read().unwrap(), &self.config)?
),
)?;
} }
} }

View File

@ -5,6 +5,9 @@ description = "Everything you need to make a static site engine in one binary."
compile_sass = true compile_sass = true
build_search_index = true build_search_index = true
[search]
index_format = "elasticlunr_json"
[markdown] [markdown]
highlight_code = true highlight_code = true
highlight_theme = "kronuz" highlight_theme = "kronuz"

View File

@ -17,6 +17,9 @@ After `zola build` or `zola serve`, you should see two files in your public dire
- `search_index.${default_language}.js`: so `search_index.en.js` for a default setup - `search_index.${default_language}.js`: so `search_index.en.js` for a default setup
- `elasticlunr.min.js` - `elasticlunr.min.js`
If you set `index_format = "elasticlunr_json"` in your `config.toml`, a `search_index.${default_language}.json` is generated
instead of the default `search_index.${default_language}.js`.
As each site will be different, Zola makes no assumptions about your search function and doesn't provide As each site will be different, Zola makes no assumptions about your search function and doesn't provide
the JavaScript/CSS code to do an actual search and display results. You can look at how this site the JavaScript/CSS code to do an actual search and display results. You can look at how this site
implements it to get an idea: [search.js](https://github.com/getzola/zola/tree/master/docs/static/search.js). implements it to get an idea: [search.js](https://github.com/getzola/zola/tree/master/docs/static/search.js).

View File

@ -160,6 +160,10 @@ include_content = true
# become too big to load on the site. Defaults to not being set. # become too big to load on the site. Defaults to not being set.
# truncate_content_length = 100 # truncate_content_length = 100
# Wether to produce the search index as a javascript file or as a JSON file
# Accepted value "elasticlunr_javascript" or "elasticlunr_json"
index_format = "elasticlunr_javascript"
# Optional translation object for the default language # Optional translation object for the default language
# Example: # Example:
# default_language = "fr" # default_language = "fr"

21
docs/static/search.js vendored
View File

@ -142,11 +142,24 @@ function initSearch() {
} }
}; };
var currentTerm = ""; var currentTerm = "";
var index = elasticlunr.Index.load(window.searchIndex); 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(function() { $searchInput.addEventListener("keyup", debounce(async function() {
var term = $searchInput.value.trim(); var term = $searchInput.value.trim();
if (term === currentTerm || !index) { if (term === currentTerm) {
return; return;
} }
$searchResults.style.display = term === "" ? "none" : "block"; $searchResults.style.display = term === "" ? "none" : "block";
@ -156,7 +169,7 @@ function initSearch() {
return; return;
} }
var results = index.search(term, options); var results = (await initIndex()).search(term, options);
if (results.length === 0) { if (results.length === 0) {
$searchResults.style.display = "none"; $searchResults.style.display = "none";
return; return;

View File

@ -103,7 +103,6 @@
</footer> </footer>
<script type="text/javascript" src="{{ get_url(path="elasticlunr.min.js") }}"></script> <script type="text/javascript" src="{{ get_url(path="elasticlunr.min.js") }}"></script>
<script type="text/javascript" src="{{ get_url(path="search_index.en.js") }}"></script>
<script type="text/javascript" src="{{ get_url(path="search.js") }}"></script> <script type="text/javascript" src="{{ get_url(path="search.js") }}"></script>
</body> </body>
</html> </html>