Skip to content

DOC use Algolia for the search bar #29666

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ jobs:
# dependencies as long as we can avoid raising warnings with more recent
# versions of the same dependencies.
- SKLEARN_WARNINGS_AS_ERRORS: '0'
# Test not using Algolia search
- SKLEARN_DOC_USE_ALGOLIA_SEARCH: '0'
steps:
- checkout
- run: ./build_tools/circle/checkout_merge_commit.sh
Expand Down Expand Up @@ -65,6 +67,8 @@ jobs:
# Make sure that we fail if the documentation build generates warnings with
# recent versions of the dependencies.
- SKLEARN_WARNINGS_AS_ERRORS: '1'
# Build the final artifacts with Algolia search enabled
- SKLEARN_DOC_USE_ALGOLIA_SEARCH: '1'
steps:
- checkout
- run: ./build_tools/circle/checkout_merge_commit.sh
Expand Down
37 changes: 37 additions & 0 deletions doc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@
# that need plotly.
pass

# Set the environment variable to use Algolia docsearch to overwrite the default sphinx
# local search; this is used in CI
use_algolia = os.environ.get("SKLEARN_DOC_USE_ALGOLIA_SEARCH", "0") != "0"

# -- General configuration ---------------------------------------------------

# Add any Sphinx extension module names here, as strings. They can be
Expand Down Expand Up @@ -295,6 +299,14 @@
"announcement": None,
}

if use_algolia:
# Remove the sphinx searchbox from the persistent field and add Algolia searchbox
# to the start field; note that Algolia searchbox can only be placed in the start
# field because all other fields have multiple slots to adapt to different screen
# sizes while Algolia can hydrate only one slot
html_theme_options["navbar_start"].append("algolia-searchbox")
html_theme_options["navbar_persistent"] = []

# Add any paths that contain custom themes here, relative to this directory.
# html_theme_path = ["themes"]

Expand Down Expand Up @@ -339,6 +351,9 @@
# template names.
html_additional_pages = {"index": "index.html"}

if use_algolia:
html_additional_pages["algolia-search"] = "algolia-search.html"

# Additional files to copy
# html_extra_path = []

Expand Down Expand Up @@ -382,6 +397,28 @@ def add_js_css_files(app, pagename, templatename, context, doctree):
elif pagename.startswith("modules/generated/"):
app.add_css_file("styles/api.css")

if use_algolia:
# If using Algolia search, load Algolia credentials and index name so that they
# are accessible in JavaScript
app.add_js_file(
None,
body=(
'SKLEARN_ALGOLIA_APP_ID = "WAC7N12TSK";\n'
'SKLEARN_ALGOLIA_API_KEY = "85fcdebd88be36ce665548bbbf328519";\n'
'SKLEARN_ALGOLIA_INDEX_NAME = "scikit-learn";'
),
)
if pagename != "algolia-search":
# For all pages except for search page, load Algolia docsearch CSS and JS to
# enable the search field in the navbar
app.add_js_file(
"https://cdn.jsdelivr.net/npm/@docsearch/js@3.6.1",
loading_method="defer",
)
app.add_js_file("scripts/algolia-searchbox.js", loading_method="defer")
app.add_css_file("https://cdn.jsdelivr.net/npm/@docsearch/css@3.6.1")
app.add_css_file("styles/algolia-searchbox.css")


# If false, no module index is generated.
html_domain_indices = False
Expand Down
146 changes: 146 additions & 0 deletions doc/js/scripts/algolia-search.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/**
* This script is used initialize Algolia DocSearch on the Algolia search page. It will
* hydrate the search page (see `doc/templates/search.html`) and activate the search
* functionalities.
*/

document.addEventListener("DOMContentLoaded", () => {
let timer;
const timeout = 500; // Debounce search-as-you-type

const searchClient = algoliasearch(
SKLEARN_ALGOLIA_APP_ID,
SKLEARN_ALGOLIA_API_KEY
);

const search = instantsearch({
indexName: SKLEARN_ALGOLIA_INDEX_NAME,
initialUiState: {
[SKLEARN_ALGOLIA_INDEX_NAME]: {
query: new URLSearchParams(window.location.search).get("q") || "",
},
},
searchClient,
});

search.addWidgets([
// The powered-by widget as the heading
instantsearch.widgets.poweredBy({
container: "#docsearch-powered-by-light",
theme: "light",
}),
instantsearch.widgets.poweredBy({
container: "#docsearch-powered-by-dark",
theme: "dark",
}),
// The search input box
instantsearch.widgets.searchBox({
container: "#docsearch-container",
placeholder: "Search the docs ...",
autofocus: true,
// Debounce the search input to avoid making too many requests
queryHook(query, refine) {
clearTimeout(timer);
timer = setTimeout(() => refine(query), timeout);
},
}),
// The search statistics before the list of results
instantsearch.widgets.stats({
container: "#docsearch-stats",
templates: {
text: (data, { html }) => {
if (data.query === "") {
return "";
}

let count;
if (data.hasManyResults) {
count = `${data.nbHits} results`;
} else if (data.hasOneResult) {
count = "1 result";
} else {
count = "no results";
}

const stats = `Search finished, found ${count} matching the search query in ${data.processingTimeMS}ms.`;
return html`
<div class="sk-search-stats-heading">Search Results</div>
<p class="sk-search-stats">${stats}</p>
`;
},
},
}),
// The list of search results
instantsearch.widgets.infiniteHits({
container: "#docsearch-hits",
transformItems: (items, { results }) => {
if (results.query === "") {
return [];
}
return items;
},
templates: {
item: (hit, { html, components }) => {
const hierarchy = Object.entries(hit._highlightResult.hierarchy);
const lastKey = hierarchy[hierarchy.length - 1][0];

const sharedHTML = html`
<a class="sk-search-item-header" href="${hit.url}">
${components.Highlight({
hit,
attribute: `hierarchy.${lastKey}`,
})}
</a>
<div class="sk-search-item-path">
${components.Highlight({ hit, attribute: "hierarchy.lvl0" })}
${hierarchy.slice(1, -1).map(([key, _]) => {
return html`
<span class="sk-search-item-path-divider">»</span>
${components.Highlight({
hit,
attribute: `hierarchy.${key}`,
})}
`;
})}
</div>
`;

if (hit.type === "content") {
return html`
${sharedHTML}
<p class="sk-search-item-context">
${components.Snippet({ hit, attribute: "content" })}
</p>
`;
} else {
return sharedHTML;
}
},
// We have stats widget that can imply "no results"
empty: () => {
return "";
},
},
}),
// Additional configuration of the widgets
instantsearch.widgets.configure({
hitsPerPage: 50,
attributesToSnippet: ["content:60"], // Lengthen snippets to show more context
}),
]);

search.start();

// Apart from the loading indicator in the search form, also show loading information
// at the bottom so when clicking on "load more" we also have some feedback
search.on("render", () => {
const container = document.getElementById("docsearch-loading-indicator");
if (search.status === "stalled") {
container.innerText = "Loading search results...";
container.style.marginTop = "0.4rem";
} else {
container.innerText = "";
container.style.marginTop = "0";
}
});
});
89 changes: 89 additions & 0 deletions doc/js/scripts/algolia-searchbox.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/**
* This script is used initialize Algolia DocSearch on each page. It will hydrate the
* container with ID `docsearch` (see `doc/templates/algolia-searchbox.html`) with the
* Algolia search widget.
*/

document.addEventListener("DOMContentLoaded", () => {
// Figure out how to route to the search page from the current page where we will show
// all search results
const pagename = DOCUMENTATION_OPTIONS.pagename;
let searchPageHref = "./";
for (let i = 0; i < pagename.split("/").length - 1; i++) {
searchPageHref += "../";
}
searchPageHref += "algolia-search.html";

// Function to navigate to the all results page
const navigateToResultsPage = () => {
const link = document.getElementById("sk-search-all-results-link");
if (link !== null) {
// If there is the "see all results" link, just click it
link.click();
return;
}

const inputBox = document.getElementById("docsearch-input");
if (inputBox === null || inputBox.value === "") {
// If we cannot get the input box or the input box is empty, navigate to the
// all results page with no query
window.location.assign(searchPageHref);
return;
}
// Navigate to the all results page with query constructed from the input
const query = new URLSearchParams({ q: inputBox.value });
window.location.assign(`${searchPageHref}?${query}`);
};

// Initialize the Algolia DocSearch widget
docsearch({
container: "#docsearch",
appId: SKLEARN_ALGOLIA_APP_ID,
apiKey: SKLEARN_ALGOLIA_API_KEY,
indexName: SKLEARN_ALGOLIA_INDEX_NAME,
placeholder: "Search the docs ...",
searchParameters: { attributesToHighlight: ["hierarchy.lvl0"] },
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apparently there is a bug that explain why I could not get the desired rendering in the instant box:
algolia/docsearch#2294

// Redirect to the search page with the corresponding query
resultsFooterComponent: ({ state }) => ({
type: "a",
ref: undefined,
constructor: undefined,
key: state.query,
props: {
id: "sk-search-all-results-link",
href: `${searchPageHref}?q=${state.query}`,
children: `Check all results...`,
},
__v: null,
}),
navigator: {
// Hack implementation to navigate to the search page instead of navigating to the
// corresponding search result page; `navigateNewTab` and `navigateNewWindow` are
// still left as the default behavior
navigate: navigateToResultsPage,
},
});

// The navigator API only works when there are search results; there are cases where
// there are no hits, e.g. empty query + no history, in which case we need to manually
// listen to the Enter key
document.addEventListener("keydown", (e) => {
if (
e.key === "Enter" &&
!e.shiftKey &&
!e.ctrlKey &&
!e.metaKey &&
!e.altKey
) {
const container = document.querySelector(
".DocSearch.DocSearch-Container"
);
if (container === null) {
return;
}
e.preventDefault();
e.stopPropagation();
navigateToResultsPage();
}
});
});
Loading
Loading