Skip to main content
Seb's blog

Faire une page de recherche dans Zola

C'est un peu l'équivalent pour Zola du billet sur 11ty et la page de recherche, on va voir comment faire une page de recherche même si ça dépendra beaucoup du thème utilisé.

En effet, contrairement à 11ty et une des raisons pour laquelle j'ai choisi ce dernier au lieu et place de Zola, c'est que tout peut basculer selon le thème choisi, ça va changer les pages étendues, mais aussi les blocks HTML.

On commencera par faire un "template" search.html, contenant ceci (attention comme dit ça dépendra de votre thème, la majeur partie restera pareil mais {% extends "index.html" %}, {% block main_content %} et {% endblock main_content %} devront changer selon le cas):

{% extends "index.html" %}


{% block main_content %}
    <h1>Search</h1>
        <form class="form-search">
          <input id="search" type="text" class="input-large search-query">
        </form>
        <div class="search-results">
          <div class="search-results__items"></div>
        </div>
  <script src="{{ get_url(path="js/search.js", trailing_slash=false) | safe }}"></script>
  <script src="{{ get_url(path="elasticlunr.min.js", trailing_slash=false) | safe }}"></script>
  <script src="{{ get_url(path="search_index.en.js", trailing_slash=false) | safe }}"></script>
{% endblock main_content %}

Ensuite un peu de scripts, on colle ce qui suit dans le fichier search.js (/static/js/search.js):

// https://raw.githubusercontent.com/getzola/zola/master/docs/static/search.js

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 = 100;

    var options = {
      bool: "AND",
      fields: {
        title: {boost: 2},
        body: {boost: 1},
      }
    };
    var currentTerm = "";
    var index = elasticlunr.Index.load(window.searchIndex);

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

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

      currentTerm = term;
      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));
  }


  if (document.readyState === "complete" ||
      (document.readyState !== "loading" && !document.documentElement.doScroll)
  ) {
    initSearch();
  } else {
    document.addEventListener("DOMContentLoaded", initSearch);
  }

Maintenant la page search.md dans votre content:

+++
title = "Search"
path = "search"
template = "search.html"
+++

On pense bien à activer la recherche dans notre config:

build_search_index = true

[search]                # Options specific to elasticlunr search.
include_title = true        # include title of page/section in index
include_description = false # include description of page/section in index
include_content = true      # include rendered content of page/section in index
# Truncate content at nth character. Useful if index size significantly slows down the site.
truncate_content_length = 1000

Et le tour est joué.

Commencer la discussion: Venez écrire un commentaire dans le forum