* [PATCH] docs: add advanced search for kernel documentation
@ 2026-03-21 18:15 Rito Rhymes
2026-03-21 22:53 ` Randy Dunlap
` (2 more replies)
0 siblings, 3 replies; 14+ messages in thread
From: Rito Rhymes @ 2026-03-21 18:15 UTC (permalink / raw)
To: Jonathan Corbet, Mauro Carvalho Chehab, linux-doc
Cc: Shuah Khan, linux-kernel, Rito Rhymes
Replace the stock Sphinx search page with one that reuses the
existing searchindex.js while adding structured result grouping,
filtering, and exact identifier matching.
Results are grouped into Symbols, Sections, Index entries, and
Pages, each in a collapsible section with a count. An Advanced
panel exposes filters for documentation area, object type, result
kind, and exact match mode. All state is URL-encoded so searches
remain shareable.
Page summary snippets are lazy-loaded via IntersectionObserver to
avoid fetching every matching page up front.
The sidebar keeps the existing quick-search box and adds an
"Advanced search" link below it.
Signed-off-by: Rito Rhymes <rito@ritovision.com>
---
Documentation/sphinx-static/custom.css | 163 ++++
Documentation/sphinx-static/kernel-search.js | 746 ++++++++++++++++++
Documentation/sphinx/templates/search.html | 106 +++
Documentation/sphinx/templates/searchbox.html | 18 +
4 files changed, 1033 insertions(+)
create mode 100644 Documentation/sphinx-static/kernel-search.js
create mode 100644 Documentation/sphinx/templates/search.html
create mode 100644 Documentation/sphinx/templates/searchbox.html
diff --git a/Documentation/sphinx-static/custom.css b/Documentation/sphinx-static/custom.css
index db24f4344..dd7cc221e 100644
--- a/Documentation/sphinx-static/custom.css
+++ b/Documentation/sphinx-static/custom.css
@@ -169,3 +169,166 @@ a.manpage {
font-weight: bold;
font-family: "Courier New", Courier, monospace;
}
+
+/* Keep the quick search box as-is and add a secondary advanced search link. */
+div.sphinxsidebar p.search-advanced-link {
+ margin: 0.5em 0 0 0;
+ font-size: 0.95em;
+}
+
+/*
+ * The enhanced search page keeps the stock GET workflow but adds
+ * filter controls and grouped results.
+ */
+form.kernel-search-form {
+ margin-bottom: 2em;
+}
+
+div.kernel-search-query-row {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 1em;
+ align-items: end;
+}
+
+div.kernel-search-query-field {
+ flex: 1 1 26em;
+}
+
+div.kernel-search-query-field label,
+div.kernel-search-field label,
+fieldset.kernel-search-kind-filters legend {
+ display: block;
+ font-weight: bold;
+ margin-bottom: 0.35em;
+}
+
+div.kernel-search-query-field input[type="text"],
+div.kernel-search-field select {
+ width: 100%;
+ box-sizing: border-box;
+}
+
+div.kernel-search-query-actions {
+ display: flex;
+ align-items: center;
+ gap: 0.75em;
+}
+
+span.kernel-search-progress {
+ min-height: 1.2em;
+ color: #666;
+}
+
+details.kernel-search-advanced {
+ margin-top: 1em;
+ padding: 0.75em 1em 1em 1em;
+ border: 1px solid #cccccc;
+ background: #f7f7f7;
+}
+
+details.kernel-search-advanced summary {
+ cursor: pointer;
+ font-weight: bold;
+}
+
+div.kernel-search-advanced-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(16em, 1fr));
+ gap: 1em 1.25em;
+ margin-top: 1em;
+}
+
+fieldset.kernel-search-kind-filters {
+ margin: 0;
+ padding: 0;
+ border: none;
+}
+
+label.kernel-search-checkbox {
+ display: flex;
+ align-items: flex-start;
+ gap: 0.5em;
+ margin-bottom: 0.35em;
+}
+
+div.kernel-search-results {
+ margin-top: 1.5em;
+}
+
+p.kernel-search-status {
+ margin-bottom: 1.5em;
+}
+
+details.kernel-search-group {
+ margin-top: 2em;
+}
+
+summary.kernel-search-group-summary {
+ cursor: pointer;
+ font-size: 150%;
+ margin: 0 0 0.6em 0;
+}
+
+summary.kernel-search-group-summary h2.kernel-search-group-title {
+ display: inline;
+ margin: 0;
+ font-size: inherit;
+ font-weight: normal;
+}
+
+span.kernel-search-group-count {
+ color: #666666;
+ margin-left: 0.35em;
+}
+
+ol.kernel-search-list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+li.kernel-search-result {
+ padding: 0.9em 0;
+ border-top: 1px solid #dddddd;
+}
+
+li.kernel-search-result:first-child {
+ border-top: none;
+}
+
+div.kernel-search-result-heading {
+ font-weight: bold;
+}
+
+div.kernel-search-path,
+div.kernel-search-meta,
+p.kernel-search-summary {
+ margin-top: 0.3em;
+ color: #555555;
+}
+
+div.kernel-search-path,
+div.kernel-search-meta {
+ font-size: 0.95em;
+}
+
+p.kernel-search-summary {
+ margin-bottom: 0;
+}
+
+@media screen and (max-width: 65em) {
+ div.kernel-search-query-actions {
+ width: 100%;
+ justify-content: flex-start;
+ }
+}
+
+@media screen and (min-width: 65em) {
+ div.kernel-search-result-heading,
+ div.kernel-search-path,
+ div.kernel-search-meta,
+ p.kernel-search-summary {
+ margin-left: 2rem;
+ }
+}
diff --git a/Documentation/sphinx-static/kernel-search.js b/Documentation/sphinx-static/kernel-search.js
new file mode 100644
index 000000000..bcf79f820
--- /dev/null
+++ b/Documentation/sphinx-static/kernel-search.js
@@ -0,0 +1,746 @@
+"use strict";
+
+(() => {
+ const RESULT_KIND_ORDER = ["object", "title", "index", "text"];
+ const RESULT_KIND_LABELS = {
+ object: "Symbols",
+ title: "Sections",
+ index: "Index entries",
+ text: "Pages",
+ };
+ const TOP_LEVEL_AREA = "__top_level__";
+ const OBJECT_PRIORITY = {
+ 0: 15,
+ 1: 5,
+ 2: -5,
+ };
+ const SUMMARY_ROOT_MARGIN = "300px 0px";
+ const documentTextCache = new Map();
+ const summaryTargets = new WeakMap();
+ let summaryObserver = null;
+
+ window.Search = window.Search || {};
+ window.Search._callbacks = window.Search._callbacks || [];
+ window.Search._index = window.Search._index || null;
+ window.Search.setIndex = (index) => {
+ window.Search._index = index;
+ const callbacks = window.Search._callbacks.slice();
+ window.Search._callbacks.length = 0;
+ callbacks.forEach((callback) => callback(index));
+ };
+ window.Search.whenReady = (callback) => {
+ if (window.Search._index) callback(window.Search._index);
+ else window.Search._callbacks.push(callback);
+ };
+
+ const splitQuery = (query) =>
+ query
+ .split(/[^\p{Letter}\p{Number}_\p{Emoji_Presentation}]+/gu)
+ .filter((term) => term);
+
+ const getStemmer = () =>
+ typeof Stemmer === "function" ? new Stemmer() : { stemWord: (word) => word };
+
+ const hasOwn = (object, key) =>
+ Object.prototype.hasOwnProperty.call(object, key);
+
+ const getContentRoot = () =>
+ document.documentElement.dataset.content_root
+ || (typeof DOCUMENTATION_OPTIONS !== "undefined" ? DOCUMENTATION_OPTIONS.URL_ROOT || "" : "");
+
+ const compareResults = (left, right) => {
+ if (left.score === right.score) {
+ const leftTitle = left.title.toLowerCase();
+ const rightTitle = right.title.toLowerCase();
+ if (leftTitle === rightTitle) return 0;
+ return leftTitle < rightTitle ? -1 : 1;
+ }
+ return right.score - left.score;
+ };
+
+ const getAreaValue = (docName) =>
+ docName.includes("/") ? docName.split("/", 1)[0] : TOP_LEVEL_AREA;
+
+ const getAreaLabel = (area) =>
+ area === TOP_LEVEL_AREA ? "Top level" : area;
+
+ const matchArea = (docName, area) => {
+ if (!area) return true;
+ if (area === TOP_LEVEL_AREA) return !docName.includes("/");
+ return docName === area || docName.startsWith(area + "/");
+ };
+
+ const buildDocUrls = (docName) => {
+ const contentRoot = getContentRoot();
+ const builder = DOCUMENTATION_OPTIONS.BUILDER;
+ const fileSuffix = DOCUMENTATION_OPTIONS.FILE_SUFFIX;
+ const linkSuffix = DOCUMENTATION_OPTIONS.LINK_SUFFIX;
+
+ if (builder === "dirhtml") {
+ let dirname = docName + "/";
+ if (dirname.match(/\/index\/$/)) dirname = dirname.substring(0, dirname.length - 6);
+ else if (dirname === "index/") dirname = "";
+
+ return {
+ requestUrl: contentRoot + dirname,
+ linkUrl: contentRoot + dirname,
+ };
+ }
+
+ return {
+ requestUrl: contentRoot + docName + fileSuffix,
+ linkUrl: docName + linkSuffix,
+ };
+ };
+
+ const htmlToText = (htmlString, anchor) => {
+ const htmlElement = new DOMParser().parseFromString(htmlString, "text/html");
+ for (const selector of [".headerlink", "script", "style"]) {
+ htmlElement.querySelectorAll(selector).forEach((element) => element.remove());
+ }
+
+ if (anchor) {
+ const anchorId = anchor[0] === "#" ? anchor.substring(1) : anchor;
+ const anchorContent = htmlElement.getElementById(anchorId);
+ if (anchorContent) return anchorContent.textContent;
+ }
+
+ const docContent = htmlElement.querySelector('[role="main"]');
+ return docContent ? docContent.textContent : "";
+ };
+
+ const makeSummary = (htmlText, keywords, anchor) => {
+ const text = htmlToText(htmlText, anchor);
+ if (!text) return null;
+
+ const lowered = text.toLowerCase();
+ const positions = keywords
+ .map((keyword) => lowered.indexOf(keyword.toLowerCase()))
+ .filter((position) => position > -1);
+ const actualStart = positions.length ? positions[0] : 0;
+ const start = Math.max(actualStart - 120, 0);
+ const prefix = start === 0 ? "" : "...";
+ const suffix = start + 240 < text.length ? "..." : "";
+
+ const summary = document.createElement("p");
+ summary.className = "kernel-search-summary";
+ summary.textContent = prefix + text.substring(start, start + 240).trim() + suffix;
+ return summary;
+ };
+
+ const loadDocumentText = (requestUrl) => {
+ if (!documentTextCache.has(requestUrl)) {
+ documentTextCache.set(
+ requestUrl,
+ fetch(requestUrl)
+ .then((response) => (response.ok ? response.text() : ""))
+ .catch(() => ""),
+ );
+ }
+
+ return documentTextCache.get(requestUrl);
+ };
+
+ const pushBest = (resultMap, result) => {
+ const key = [result.kind, result.docName, result.anchor || "", result.title].join("|");
+ const existing = resultMap.get(key);
+ if (!existing || existing.score < result.score) resultMap.set(key, result);
+ };
+
+ const buildQueryState = (query, exact) => {
+ const rawTerms = splitQuery(query.trim());
+ const rawTermsLower = rawTerms.map((term) => term.toLowerCase());
+ const objectTerms = new Set(rawTermsLower);
+ const highlightTerms = exact ? rawTermsLower : [];
+ const searchTerms = new Set();
+ const excludedTerms = new Set();
+
+ if (!exact) {
+ const stemmer = getStemmer();
+ rawTerms.forEach((term) => {
+ const lowered = term.toLowerCase();
+ if ((typeof stopwords !== "undefined" && stopwords.has(lowered)) || /^\d+$/.test(term)) {
+ return;
+ }
+
+ const word = stemmer.stemWord(lowered);
+ if (!word) return;
+
+ if (word[0] === "-") excludedTerms.add(word.substring(1));
+ else {
+ searchTerms.add(word);
+ highlightTerms.push(lowered);
+ }
+ });
+ } else {
+ rawTermsLower.forEach((term) => searchTerms.add(term));
+ }
+
+ if (typeof SPHINX_HIGHLIGHT_ENABLED !== "undefined" && SPHINX_HIGHLIGHT_ENABLED) {
+ localStorage.setItem("sphinx_highlight_terms", [...new Set(highlightTerms)].join(" "));
+ }
+
+ return {
+ exact,
+ query,
+ queryLower: query.toLowerCase().trim(),
+ rawTerms: rawTermsLower,
+ objectTerms,
+ searchTerms,
+ excludedTerms,
+ highlightTerms: [...new Set(highlightTerms)],
+ };
+ };
+
+ const candidateMatches = (candidateLower, state) => {
+ if (!state.queryLower) return false;
+ if (state.exact) return candidateLower === state.queryLower;
+
+ if (
+ candidateLower.includes(state.queryLower)
+ && state.queryLower.length >= Math.ceil(candidateLower.length / 2)
+ ) {
+ return true;
+ }
+
+ return state.rawTerms.length > 0
+ && state.rawTerms.every((term) => candidateLower.includes(term));
+ };
+
+ const scoreLabelMatch = (candidateLower, state, baseScore, partialScore) => {
+ if (state.exact) return baseScore + 20;
+ if (candidateLower === state.queryLower) return baseScore + 10;
+ if (candidateLower.includes(state.queryLower)) {
+ return Math.max(partialScore, Math.round((baseScore * state.queryLower.length) / candidateLower.length));
+ }
+
+ return partialScore * Math.max(1, state.rawTerms.filter((term) => candidateLower.includes(term)).length);
+ };
+
+ const collectObjectResults = (index, state, filters) => {
+ const resultMap = new Map();
+ const objects = index.objects || {};
+ const objNames = index.objnames || {};
+ const objTypes = index.objtypes || {};
+
+ Object.keys(objects).forEach((prefix) => {
+ objects[prefix].forEach((match) => {
+ const fileIndex = match[0];
+ const typeIndex = match[1];
+ const priority = match[2];
+ const anchorValue = match[3];
+ const name = match[4];
+ const docName = index.docnames[fileIndex];
+ const fileName = index.filenames[fileIndex];
+ const pageTitle = index.titles[fileIndex];
+ const objectLabel = objNames[typeIndex] ? objNames[typeIndex][2] : "Object";
+ const objectType = objTypes[typeIndex];
+
+ if (!matchArea(docName, filters.area)) return;
+ if (filters.objtype && filters.objtype !== objectType) return;
+
+ const fullName = prefix ? prefix + "." + name : name;
+ const fullNameLower = fullName.toLowerCase();
+ const lastNameLower = fullNameLower.split(".").slice(-1)[0];
+ const nameLower = name.toLowerCase();
+
+ let score = 0;
+ if (state.exact) {
+ if (
+ fullNameLower !== state.queryLower
+ && lastNameLower !== state.queryLower
+ && nameLower !== state.queryLower
+ ) {
+ return;
+ }
+ score = 120;
+ } else {
+ const haystack = `${fullName} ${objectLabel} ${pageTitle}`.toLowerCase();
+ if (state.objectTerms.size === 0) return;
+ if ([...state.objectTerms].some((term) => !haystack.includes(term))) return;
+ const matchedNameTerms = state.rawTerms.filter(
+ (term) =>
+ fullNameLower.includes(term)
+ || lastNameLower.includes(term)
+ || nameLower.includes(term),
+ ).length;
+
+ if (
+ fullNameLower === state.queryLower
+ || lastNameLower === state.queryLower
+ || nameLower === state.queryLower
+ ) {
+ score += 11;
+ } else if (
+ lastNameLower.includes(state.queryLower)
+ || nameLower.includes(state.queryLower)
+ ) {
+ score += 6;
+ } else if (fullNameLower.includes(state.queryLower)) {
+ score += 4;
+ } else if (matchedNameTerms > 0) {
+ score += matchedNameTerms;
+ } else {
+ return;
+ }
+ }
+
+ score += OBJECT_PRIORITY[priority] || 0;
+
+ let anchor = anchorValue;
+ if (anchor === "") anchor = fullName;
+ else if (anchor === "-" && objNames[typeIndex]) anchor = objNames[typeIndex][1] + "-" + fullName;
+
+ pushBest(resultMap, {
+ kind: "object",
+ docName,
+ fileName,
+ title: fullName,
+ anchor: anchor ? "#" + anchor : "",
+ description: `${objectLabel}, in ${pageTitle}`,
+ score,
+ });
+ });
+ });
+
+ return [...resultMap.values()].sort(compareResults);
+ };
+
+ const collectSectionResults = (index, state, filters) => {
+ const resultMap = new Map();
+ const allTitles = index.alltitles || {};
+
+ Object.entries(allTitles).forEach(([sectionTitle, entries]) => {
+ const lowered = sectionTitle.toLowerCase().trim();
+ if (!candidateMatches(lowered, state)) return;
+
+ entries.forEach(([fileIndex, anchorId]) => {
+ const docName = index.docnames[fileIndex];
+ const fileName = index.filenames[fileIndex];
+ const pageTitle = index.titles[fileIndex];
+ if (!matchArea(docName, filters.area)) return;
+
+ if (anchorId === null && sectionTitle === pageTitle) return;
+
+ pushBest(resultMap, {
+ kind: "title",
+ docName,
+ fileName,
+ title: pageTitle !== sectionTitle ? `${pageTitle} > ${sectionTitle}` : sectionTitle,
+ anchor: anchorId ? "#" + anchorId : "",
+ description: pageTitle,
+ score: scoreLabelMatch(lowered, state, 15, 7),
+ });
+ });
+ });
+
+ return [...resultMap.values()].sort(compareResults);
+ };
+
+ const collectIndexResults = (index, state, filters) => {
+ const resultMap = new Map();
+ const entries = index.indexentries || {};
+
+ Object.entries(entries).forEach(([entry, matches]) => {
+ const lowered = entry.toLowerCase().trim();
+ if (!candidateMatches(lowered, state)) return;
+
+ matches.forEach(([fileIndex, anchorId, isMain]) => {
+ const docName = index.docnames[fileIndex];
+ const fileName = index.filenames[fileIndex];
+ const pageTitle = index.titles[fileIndex];
+ if (!matchArea(docName, filters.area)) return;
+
+ let score = scoreLabelMatch(lowered, state, 20, 8);
+ if (!isMain) score -= 5;
+
+ pushBest(resultMap, {
+ kind: "index",
+ docName,
+ fileName,
+ title: entry,
+ anchor: anchorId ? "#" + anchorId : "",
+ description: pageTitle,
+ score,
+ });
+ });
+ });
+
+ return [...resultMap.values()].sort(compareResults);
+ };
+
+ const collectTextResults = (index, state, filters) => {
+ const resultMap = new Map();
+ const terms = index.terms || {};
+ const titleTerms = index.titleterms || {};
+ const searchTerms = [...state.searchTerms];
+
+ if (searchTerms.length === 0) return [];
+
+ const scoreMap = new Map();
+ const fileMap = new Map();
+
+ searchTerms.forEach((word) => {
+ const files = [];
+ const candidates = [
+ {
+ files: hasOwn(terms, word) ? terms[word] : undefined,
+ score: 5,
+ },
+ {
+ files: hasOwn(titleTerms, word) ? titleTerms[word] : undefined,
+ score: 15,
+ },
+ ];
+
+ if (!state.exact && word.length > 2) {
+ if (!hasOwn(terms, word)) {
+ Object.keys(terms).forEach((term) => {
+ if (term.includes(word)) candidates.push({ files: terms[term], score: 2 });
+ });
+ }
+ if (!hasOwn(titleTerms, word)) {
+ Object.keys(titleTerms).forEach((term) => {
+ if (term.includes(word)) candidates.push({ files: titleTerms[term], score: 7 });
+ });
+ }
+ }
+
+ if (candidates.every((candidate) => candidate.files === undefined)) return;
+
+ candidates.forEach((candidate) => {
+ if (candidate.files === undefined) return;
+
+ let recordFiles = candidate.files;
+ if (recordFiles.length === undefined) recordFiles = [recordFiles];
+ files.push(...recordFiles);
+
+ recordFiles.forEach((fileIndex) => {
+ if (!scoreMap.has(fileIndex)) scoreMap.set(fileIndex, new Map());
+ const currentScore = scoreMap.get(fileIndex).get(word) || 0;
+ scoreMap.get(fileIndex).set(word, Math.max(currentScore, candidate.score));
+ });
+ });
+
+ files.forEach((fileIndex) => {
+ if (!fileMap.has(fileIndex)) fileMap.set(fileIndex, [word]);
+ else if (!fileMap.get(fileIndex).includes(word)) fileMap.get(fileIndex).push(word);
+ });
+ });
+
+ const filteredTermCount = state.exact
+ ? searchTerms.length
+ : searchTerms.filter((term) => term.length > 2).length;
+
+ for (const [fileIndex, matchedWords] of fileMap.entries()) {
+ const docName = index.docnames[fileIndex];
+ const fileName = index.filenames[fileIndex];
+ if (!matchArea(docName, filters.area)) continue;
+
+ if (matchedWords.length !== searchTerms.length && matchedWords.length !== filteredTermCount) {
+ continue;
+ }
+
+ if (
+ [...state.excludedTerms].some(
+ (term) =>
+ terms[term] === fileIndex
+ || titleTerms[term] === fileIndex
+ || (terms[term] || []).includes(fileIndex)
+ || (titleTerms[term] || []).includes(fileIndex),
+ )
+ ) {
+ continue;
+ }
+
+ let score = Math.max(...matchedWords.map((word) => scoreMap.get(fileIndex).get(word)));
+ if (state.exact && index.titles[fileIndex].toLowerCase() === state.queryLower) score += 10;
+
+ pushBest(resultMap, {
+ kind: "text",
+ docName,
+ fileName,
+ title: index.titles[fileIndex],
+ anchor: "",
+ description: null,
+ score,
+ });
+ }
+
+ return [...resultMap.values()].sort(compareResults);
+ };
+
+ const buildFilters = (state) => ({
+ area: state.area,
+ objtype: state.objtype,
+ });
+
+ const ensureSummaryObserver = () => {
+ if (summaryObserver || typeof IntersectionObserver !== "function") return summaryObserver;
+
+ summaryObserver = new IntersectionObserver((entries) => {
+ entries.forEach((entry) => {
+ if (!entry.isIntersecting) return;
+
+ const target = entry.target;
+ summaryObserver.unobserve(target);
+ const payload = summaryTargets.get(target);
+ if (!payload || payload.loaded) return;
+
+ payload.loaded = true;
+ loadDocumentText(payload.requestUrl).then((htmlText) => {
+ if (!htmlText) return;
+
+ const summary = makeSummary(htmlText, payload.keywords, payload.anchor);
+ if (!summary) return;
+
+ payload.item.appendChild(summary);
+ });
+ });
+ }, { rootMargin: SUMMARY_ROOT_MARGIN });
+
+ return summaryObserver;
+ };
+
+ const queueSummaryLoad = (result, item, keywords) => {
+ const urls = buildDocUrls(result.docName);
+ const payload = {
+ anchor: result.anchor,
+ item,
+ keywords,
+ loaded: false,
+ requestUrl: urls.requestUrl,
+ };
+
+ const observer = ensureSummaryObserver();
+ if (!observer) {
+ payload.loaded = true;
+ loadDocumentText(payload.requestUrl).then((htmlText) => {
+ if (!htmlText) return;
+
+ const summary = makeSummary(htmlText, keywords, result.anchor);
+ if (summary) item.appendChild(summary);
+ });
+ return;
+ }
+
+ summaryTargets.set(item, payload);
+ observer.observe(item);
+ };
+
+ const createResultItem = (result, keywords) => {
+ const urls = buildDocUrls(result.docName);
+ const item = document.createElement("li");
+ item.className = `kernel-search-result kind-${result.kind}`;
+
+ const heading = item.appendChild(document.createElement("div"));
+ heading.className = "kernel-search-result-heading";
+
+ const link = heading.appendChild(document.createElement("a"));
+ link.href = urls.linkUrl + result.anchor;
+ link.dataset.score = String(result.score);
+ link.textContent = result.title;
+
+ const path = item.appendChild(document.createElement("div"));
+ path.className = "kernel-search-path";
+ path.textContent = result.fileName;
+
+ if (result.description) {
+ const meta = item.appendChild(document.createElement("div"));
+ meta.className = "kernel-search-meta";
+ meta.textContent = result.description;
+ }
+
+ if (result.kind === "text") {
+ queueSummaryLoad(result, item, keywords);
+ }
+ return item;
+ };
+
+ const renderResults = (state, resultsByKind) => {
+ const container = document.getElementById("kernel-search-results");
+ const totalResults = RESULT_KIND_ORDER.reduce(
+ (count, kind) => count + resultsByKind[kind].length,
+ 0,
+ );
+ if (summaryObserver) {
+ summaryObserver.disconnect();
+ summaryObserver = null;
+ }
+ container.replaceChildren();
+
+ const summary = document.createElement("p");
+ summary.className = "kernel-search-status";
+ if (!state.queryLower) {
+ summary.textContent = "Enter a search query to browse kernel documentation.";
+ container.appendChild(summary);
+ return;
+ }
+
+ if (!totalResults) {
+ summary.textContent =
+ "No matching results were found for the current query and filters.";
+ container.appendChild(summary);
+ return;
+ }
+
+ summary.textContent =
+ `Found ${totalResults} result${totalResults === 1 ? "" : "s"} for "${state.query}".`;
+ container.appendChild(summary);
+
+ RESULT_KIND_ORDER.forEach((kind) => {
+ const results = resultsByKind[kind];
+ if (!results.length) return;
+
+ const group = container.appendChild(document.createElement("details"));
+ group.className = `kernel-search-group kind-${kind}`;
+ group.open = true;
+
+ const summary = group.appendChild(document.createElement("summary"));
+ summary.className = "kernel-search-group-summary";
+
+ const heading = summary.appendChild(document.createElement("h2"));
+ heading.className = "kernel-search-group-title";
+ heading.textContent = RESULT_KIND_LABELS[kind];
+
+ const count = summary.appendChild(document.createElement("span"));
+ count.className = "kernel-search-group-count";
+ count.textContent = `(${results.length})`;
+
+ const list = group.appendChild(document.createElement("ol"));
+ list.className = "kernel-search-list";
+
+ results.forEach((result) => {
+ list.appendChild(createResultItem(result, state.highlightTerms));
+ });
+ });
+ };
+
+ const populateAreaOptions = (select, state) => {
+ const areas = new Set();
+ window.Search._index.docnames.forEach((docName) => areas.add(getAreaValue(docName)));
+
+ const options = [new Option("All documentation areas", "", false, !state.area)];
+ [...areas]
+ .sort((left, right) => {
+ if (left === TOP_LEVEL_AREA) return -1;
+ if (right === TOP_LEVEL_AREA) return 1;
+ return left.localeCompare(right);
+ })
+ .forEach((area) => {
+ options.push(new Option(getAreaLabel(area), area, false, area === state.area));
+ });
+
+ select.replaceChildren(...options);
+ };
+
+ const populateObjectTypeOptions = (select, state) => {
+ const objTypes = window.Search._index.objtypes || {};
+ const objNames = window.Search._index.objnames || {};
+ const entries = Object.keys(objTypes)
+ .map((key) => ({
+ value: objTypes[key],
+ label: objNames[key] ? objNames[key][2] : objTypes[key],
+ }))
+ .sort((left, right) => left.label.localeCompare(right.label));
+
+ const seen = new Set();
+ const options = [new Option("All object types", "", false, !state.objtype)];
+ entries.forEach((entry) => {
+ if (seen.has(entry.value)) return;
+ seen.add(entry.value);
+ options.push(new Option(entry.label, entry.value, false, entry.value === state.objtype));
+ });
+
+ select.replaceChildren(...options);
+ };
+
+ const parseState = () => {
+ const params = new URLSearchParams(window.location.search);
+ const kinds = params.getAll("kind").filter((kind) => RESULT_KIND_ORDER.includes(kind));
+
+ return {
+ query: params.get("q") || "",
+ queryLower: (params.get("q") || "").toLowerCase().trim(),
+ exact: params.get("exact") === "1",
+ area: params.get("area") || "",
+ objtype: params.get("objtype") || "",
+ advanced: params.get("advanced") === "1",
+ kinds: kinds.length ? new Set(kinds) : new Set(RESULT_KIND_ORDER),
+ };
+ };
+
+ const shouldOpenAdvanced = (state) =>
+ state.advanced
+ || state.exact
+ || state.area !== ""
+ || state.objtype !== ""
+ || RESULT_KIND_ORDER.some((kind) => !state.kinds.has(kind));
+
+ const bindFormState = (state) => {
+ document.getElementById("kernel-search-query").value = state.query;
+ document.getElementById("kernel-search-exact").checked = state.exact;
+ RESULT_KIND_ORDER.forEach((kind) => {
+ const checkbox = document.getElementById(`kernel-search-kind-${kind}`);
+ if (checkbox) checkbox.checked = state.kinds.has(kind);
+ });
+
+ const advanced = document.getElementById("kernel-search-advanced");
+ const advancedFlag = document.getElementById("kernel-search-advanced-flag");
+ const open = shouldOpenAdvanced(state);
+ advanced.open = open;
+ advancedFlag.disabled = !open;
+ advanced.addEventListener("toggle", () => {
+ advancedFlag.disabled = !advanced.open;
+ });
+ };
+
+ const runSearch = () => {
+ const baseState = parseState();
+ bindFormState(baseState);
+ populateAreaOptions(document.getElementById("kernel-search-area"), baseState);
+ populateObjectTypeOptions(document.getElementById("kernel-search-objtype"), baseState);
+
+ const queryState = buildQueryState(baseState.query, baseState.exact);
+ const filters = buildFilters(baseState);
+ const resultsByKind = {
+ object: [],
+ title: [],
+ index: [],
+ text: [],
+ };
+
+ if (!baseState.queryLower) {
+ renderResults(baseState, resultsByKind);
+ return;
+ }
+
+ if (baseState.kinds.has("object")) {
+ resultsByKind.object = collectObjectResults(window.Search._index, queryState, filters);
+ }
+ if (baseState.kinds.has("title")) {
+ resultsByKind.title = collectSectionResults(window.Search._index, queryState, filters);
+ }
+ if (baseState.kinds.has("index")) {
+ resultsByKind.index = collectIndexResults(window.Search._index, queryState, filters);
+ }
+ if (baseState.kinds.has("text")) {
+ resultsByKind.text = collectTextResults(window.Search._index, queryState, filters);
+ }
+
+ renderResults(baseState, resultsByKind);
+ };
+
+ document.addEventListener("DOMContentLoaded", () => {
+ const container = document.getElementById("kernel-search-results");
+ if (!container) return;
+
+ const progress = document.getElementById("search-progress");
+ if (progress) progress.textContent = "Preparing search...";
+
+ window.Search.whenReady(() => {
+ if (progress) progress.textContent = "";
+ runSearch();
+ });
+ });
+})();
diff --git a/Documentation/sphinx/templates/search.html b/Documentation/sphinx/templates/search.html
new file mode 100644
index 000000000..966740c12
--- /dev/null
+++ b/Documentation/sphinx/templates/search.html
@@ -0,0 +1,106 @@
+{# SPDX-License-Identifier: GPL-2.0 #}
+
+{# Enhanced search page for kernel documentation. #}
+{%- extends "layout.html" %}
+{% set title = _('Search') %}
+{%- block scripts %}
+ {{ super() }}
+ <script src="{{ pathto('_static/language_data.js', 1) }}"></script>
+ <script src="{{ pathto('_static/kernel-search.js', 1) }}"></script>
+{%- endblock %}
+{% block extrahead %}
+ <script src="{{ pathto('searchindex.js', 1) }}" defer="defer"></script>
+ <meta name="robots" content="noindex" />
+ {{ super() }}
+{% endblock %}
+{% block body %}
+ <h1 id="search-documentation">{{ _('Search') }}</h1>
+ <noscript>
+ <div class="admonition warning">
+ <p>
+ {% trans %}Please activate JavaScript to enable the search
+ functionality.{% endtrans %}
+ </p>
+ </div>
+ </noscript>
+ <p class="kernel-search-help">
+ {% trans %}Searching for multiple words only shows matches that contain
+ all words.{% endtrans %}
+ </p>
+ <form id="kernel-search-form" class="kernel-search-form" action="" method="get">
+ <div class="kernel-search-query-row">
+ <div class="kernel-search-query-field">
+ <label for="kernel-search-query">{{ _('Search query') }}</label>
+ <input
+ id="kernel-search-query"
+ type="text"
+ name="q"
+ value=""
+ autocomplete="off"
+ autocorrect="off"
+ autocapitalize="off"
+ spellcheck="false"
+ />
+ </div>
+ <div class="kernel-search-query-actions">
+ <input type="submit" value="{{ _('Search') }}" />
+ <span id="search-progress" class="kernel-search-progress"></span>
+ </div>
+ </div>
+
+ <details id="kernel-search-advanced" class="kernel-search-advanced">
+ <summary>{{ _('Advanced search') }}</summary>
+ <input
+ id="kernel-search-advanced-flag"
+ type="hidden"
+ name="advanced"
+ value="1"
+ disabled="disabled"
+ />
+ <div class="kernel-search-advanced-grid">
+ <div class="kernel-search-field">
+ <label class="kernel-search-checkbox" for="kernel-search-exact">
+ <input id="kernel-search-exact" type="checkbox" name="exact" value="1" />
+ <span>{{ _('Exact identifier match') }}</span>
+ </label>
+ </div>
+
+ <fieldset class="kernel-search-kind-filters">
+ <legend>{{ _('Result kinds') }}</legend>
+ <label class="kernel-search-checkbox" for="kernel-search-kind-object">
+ <input id="kernel-search-kind-object" type="checkbox" name="kind" value="object" />
+ <span>{{ _('Symbols') }}</span>
+ </label>
+ <label class="kernel-search-checkbox" for="kernel-search-kind-title">
+ <input id="kernel-search-kind-title" type="checkbox" name="kind" value="title" />
+ <span>{{ _('Sections') }}</span>
+ </label>
+ <label class="kernel-search-checkbox" for="kernel-search-kind-index">
+ <input id="kernel-search-kind-index" type="checkbox" name="kind" value="index" />
+ <span>{{ _('Index entries') }}</span>
+ </label>
+ <label class="kernel-search-checkbox" for="kernel-search-kind-text">
+ <input id="kernel-search-kind-text" type="checkbox" name="kind" value="text" />
+ <span>{{ _('Pages') }}</span>
+ </label>
+ </fieldset>
+
+ <div class="kernel-search-field">
+ <label for="kernel-search-area">{{ _('Documentation area') }}</label>
+ <select id="kernel-search-area" name="area">
+ <option value="">{{ _('All documentation areas') }}</option>
+ </select>
+ </div>
+
+ <div class="kernel-search-field">
+ <label for="kernel-search-objtype">{{ _('Object type') }}</label>
+ <select id="kernel-search-objtype" name="objtype">
+ <option value="">{{ _('All object types') }}</option>
+ </select>
+ </div>
+ </div>
+ </details>
+ </form>
+
+ <div id="kernel-search-results" class="kernel-search-results"></div>
+{% endblock %}
diff --git a/Documentation/sphinx/templates/searchbox.html b/Documentation/sphinx/templates/searchbox.html
new file mode 100644
index 000000000..6caa0498a
--- /dev/null
+++ b/Documentation/sphinx/templates/searchbox.html
@@ -0,0 +1,18 @@
+{# SPDX-License-Identifier: GPL-2.0 #}
+
+{# Sphinx sidebar template: quick search box plus advanced search link. #}
+{%- if pagename != "search" and builder != "singlehtml" %}
+<search id="searchbox" style="display: none" role="search">
+ <h3 id="searchlabel">{{ _('Quick search') }}</h3>
+ <div class="searchformwrapper">
+ <form class="search" action="{{ pathto('search') }}" method="get">
+ <input type="text" name="q" aria-labelledby="searchlabel" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
+ <input type="submit" value="{{ _('Go') }}" />
+ </form>
+ </div>
+ <p class="search-advanced-link">
+ <a href="{{ pathto('search') }}?advanced=1">{{ _('Advanced search') }}</a>
+ </p>
+</search>
+<script>document.getElementById('searchbox').style.display = "block"</script>
+{%- endif %}
--
2.51.0
^ permalink raw reply related [flat|nested] 14+ messages in thread
* Re: [PATCH] docs: add advanced search for kernel documentation
2026-03-21 18:15 [PATCH] docs: add advanced search for kernel documentation Rito Rhymes
@ 2026-03-21 22:53 ` Randy Dunlap
2026-03-21 23:15 ` Rito Rhymes
2026-03-22 16:01 ` Jonathan Corbet
2026-03-22 18:17 ` [PATCH v2] " Rito Rhymes
2 siblings, 1 reply; 14+ messages in thread
From: Randy Dunlap @ 2026-03-21 22:53 UTC (permalink / raw)
To: Rito Rhymes, Jonathan Corbet, Mauro Carvalho Chehab, linux-doc
Cc: Shuah Khan, linux-kernel
Hi,
On 3/21/26 11:15 AM, Rito Rhymes wrote:
> Replace the stock Sphinx search page with one that reuses the
> existing searchindex.js while adding structured result grouping,
> filtering, and exact identifier matching.
>
> Results are grouped into Symbols, Sections, Index entries, and
> Pages, each in a collapsible section with a count. An Advanced
> panel exposes filters for documentation area, object type, result
> kind, and exact match mode. All state is URL-encoded so searches
> remain shareable.
Apparently I need more help/instructions. When I click on Advanced Search
and get that web page, I enter "futex" into the Search Query and click
on [Search]. I am leaving all Advanced Search options at their default
settings.
I get no output. Is this expected?
What am I doing wrong?
thanks.
> Page summary snippets are lazy-loaded via IntersectionObserver to
> avoid fetching every matching page up front.
>
> The sidebar keeps the existing quick-search box and adds an
> "Advanced search" link below it.
>
> Signed-off-by: Rito Rhymes <rito@ritovision.com>
> ---
> Documentation/sphinx-static/custom.css | 163 ++++
> Documentation/sphinx-static/kernel-search.js | 746 ++++++++++++++++++
> Documentation/sphinx/templates/search.html | 106 +++
> Documentation/sphinx/templates/searchbox.html | 18 +
> 4 files changed, 1033 insertions(+)
> create mode 100644 Documentation/sphinx-static/kernel-search.js
> create mode 100644 Documentation/sphinx/templates/search.html
> create mode 100644 Documentation/sphinx/templates/searchbox.html
--
~Randy
^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [PATCH] docs: add advanced search for kernel documentation
2026-03-21 22:53 ` Randy Dunlap
@ 2026-03-21 23:15 ` Rito Rhymes
2026-03-21 23:59 ` Randy Dunlap
0 siblings, 1 reply; 14+ messages in thread
From: Rito Rhymes @ 2026-03-21 23:15 UTC (permalink / raw)
To: Randy Dunlap, Rito Rhymes, Jonathan Corbet, Mauro Carvalho Chehab,
linux-doc
Cc: Shuah Khan, linux-kernel
That is not expected.
On my side, searching for "futex" with the default advanced-search
settings returns exactly 232 results.
I just tested out reproducing the steps you described on
Chrome, Edge and Firefox desktop on Windows and the results were
identical across each.
Let's debug here:
Which browser/version are you using? What OS?
Are you running with JavaScript enabled?
Did the browser console show any JavaScript errors?
How did you build the docs? In particular, was this from a fresh
rebuild after applying the patch? A stale generated asset or a JS
parse/runtime failure could match the behavior you describe.
Rito
^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [PATCH] docs: add advanced search for kernel documentation
2026-03-21 23:15 ` Rito Rhymes
@ 2026-03-21 23:59 ` Randy Dunlap
2026-03-22 1:12 ` Rito Rhymes
0 siblings, 1 reply; 14+ messages in thread
From: Randy Dunlap @ 2026-03-21 23:59 UTC (permalink / raw)
To: Rito Rhymes, Jonathan Corbet, Mauro Carvalho Chehab, linux-doc
Cc: Shuah Khan, linux-kernel
On 3/21/26 4:15 PM, Rito Rhymes wrote:
> That is not expected.
>
> On my side, searching for "futex" with the default advanced-search
> settings returns exactly 232 results.
>
> I just tested out reproducing the steps you described on
> Chrome, Edge and Firefox desktop on Windows and the results were
> identical across each.
>
> Let's debug here:
>
> Which browser/version are you using? What OS?
>
I tested with chromium-browser and opera.
Chromium: Version 146.0.7680.80 (Official Build) stable@@ (64-bit)
opera: version 121.0.5600.50
Linux v6.19.5 on x86_64.
> Are you running with JavaScript enabled?
>
Yes.
> Did the browser console show any JavaScript errors?
No.
> How did you build the docs? In particular, was this from a fresh
> rebuild after applying the patch? A stale generated asset or a JS
> parse/runtime failure could match the behavior you describe.
I removed my previous DOCS build output directory and then did
$ make O=DOCS htmldocs
--
~Randy
^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [PATCH] docs: add advanced search for kernel documentation
2026-03-21 23:59 ` Randy Dunlap
@ 2026-03-22 1:12 ` Rito Rhymes
2026-03-22 19:59 ` Randy Dunlap
0 siblings, 1 reply; 14+ messages in thread
From: Rito Rhymes @ 2026-03-22 1:12 UTC (permalink / raw)
To: Randy Dunlap, Rito Rhymes, Jonathan Corbet, Mauro Carvalho Chehab,
linux-doc
Cc: Shuah Khan, linux-kernel
I was using the in-tree build (`make htmldocs`) before. I just ran
fresh rebuilds with both in-tree and out-of-tree (`make O=DOCS
htmldocs`) builds, and I got identical search results of 280 hits for
"futex" when serving both.
I ran the builds and served the output on Linux/x86_64, and tested the
pages in Chrome, Edge, and Firefox on Windows.
So at this point I have not been able to reproduce the "no output"
behavior with either build mode, I think we can rule out build mode
quirks.
More debugging:
As a comparison, WITHOUT using my patch, upstream only build, does the
normal Quick Search work? What results do you get for Futex?
WITH my patch, do Quick Search return any results for Futex?
How are you opening or serving the generated docs? For example, via a
local web server (`http://...`) or directly from disk (`file://...`)?
I am serving the generated output over HTTP locally via
`python -m http.server`.
What Sphinx version are you using for the build?
I built with Sphinx 9.1.0.
If possible, could you also check in the browser network tab whether
`_static/kernel-search.js`, `_static/language_data.js`, and
`searchindex.js` are all loading successfully?
^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [PATCH] docs: add advanced search for kernel documentation
2026-03-21 18:15 [PATCH] docs: add advanced search for kernel documentation Rito Rhymes
2026-03-21 22:53 ` Randy Dunlap
@ 2026-03-22 16:01 ` Jonathan Corbet
2026-03-22 17:08 ` Rito Rhymes
2026-03-22 18:17 ` [PATCH v2] " Rito Rhymes
2 siblings, 1 reply; 14+ messages in thread
From: Jonathan Corbet @ 2026-03-22 16:01 UTC (permalink / raw)
To: Rito Rhymes, Mauro Carvalho Chehab, linux-doc
Cc: Shuah Khan, linux-kernel, Rito Rhymes
Rito Rhymes <rito@ritovision.com> writes:
> Replace the stock Sphinx search page with one that reuses the
> existing searchindex.js while adding structured result grouping,
> filtering, and exact identifier matching.
>
> Results are grouped into Symbols, Sections, Index entries, and
> Pages, each in a collapsible section with a count. An Advanced
> panel exposes filters for documentation area, object type, result
> kind, and exact match mode. All state is URL-encoded so searches
> remain shareable.
>
> Page summary snippets are lazy-loaded via IntersectionObserver to
> avoid fetching every matching page up front.
>
> The sidebar keeps the existing quick-search box and adds an
> "Advanced search" link below it.
>
> Signed-off-by: Rito Rhymes <rito@ritovision.com>
> ---
> Documentation/sphinx-static/custom.css | 163 ++++
> Documentation/sphinx-static/kernel-search.js | 746 ++++++++++++++++++
> Documentation/sphinx/templates/search.html | 106 +++
> Documentation/sphinx/templates/searchbox.html | 18 +
> 4 files changed, 1033 insertions(+)
> create mode 100644 Documentation/sphinx-static/kernel-search.js
> create mode 100644 Documentation/sphinx/templates/search.html
> create mode 100644 Documentation/sphinx/templates/searchbox.html
Without looking into detail at the work (yet), can you tell me something
about how this work was created? We do have guidance for the disclosure
of the use of coding tools in
Documentation/process/coding-assistants.rst ...
I'm curious about where you are going with this in general. A look at
ritovision.com does not suggest "kernel developer" to me.
Thanks,
jon
^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [PATCH] docs: add advanced search for kernel documentation
2026-03-22 16:01 ` Jonathan Corbet
@ 2026-03-22 17:08 ` Rito Rhymes
2026-03-22 17:25 ` Rito Rhymes
2026-03-22 20:25 ` Jonathan Corbet
0 siblings, 2 replies; 14+ messages in thread
From: Rito Rhymes @ 2026-03-22 17:08 UTC (permalink / raw)
To: Jonathan Corbet, Rito Rhymes, Mauro Carvalho Chehab, linux-doc
Cc: Shuah Khan, linux-kernel
Hi Jon
> Documentation/process/coding-assistants.rst
That was my oversight. I failed to include the appropriate
coding-assistant attribution/disclosure, and I will reroll my patches
accordingly.
> I'm curious about where you are going with this in general
I am not contributing as a kernel developer. My background is in
front-end engineering, product/UX, and developer-facing documentation
and platform surfaces, and that is where I believe I can add value here.
Linux is important infrastructure, and I have already been making
related contributions in its ecosystem. In trying to improve
lore.kernel.org, I contributed patches merged upstream to Public Inbox
for small-screen layout behavior and for enabling admin-injected meta
tags in the document head.
After my Git patches for gitweb mobile responsiveness were merged,
I prepared a kernel.org mobile-responsiveness patch series
(current theme is only built for desktop and breaks on small screens).
Since there was not an established mailing-list path for merging that
work, Johannes Schindelin introduced me to Konstantin in an archived
thread, which opened a concrete path for contributing to kernel.org.
More broadly, I have worked on improving developer-facing surfaces,
including documentation and related tooling, in other OSS projects.
That is the kind of value I am aiming to add here: not direct kernel
development, but improving usability, discoverability, and developer
experience around important technical infrastructure.
Rito
^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [PATCH] docs: add advanced search for kernel documentation
2026-03-22 17:08 ` Rito Rhymes
@ 2026-03-22 17:25 ` Rito Rhymes
2026-03-22 20:25 ` Jonathan Corbet
1 sibling, 0 replies; 14+ messages in thread
From: Rito Rhymes @ 2026-03-22 17:25 UTC (permalink / raw)
To: Rito Rhymes, Jonathan Corbet, Mauro Carvalho Chehab, linux-doc
Cc: Shuah Khan, linux-kernel
Also for reference, I have twice implemented an advanced search
feature for the OWASP Vulnerable Web Applications Directory
(VWAD): first for an earlier version of the site, and more
recently for its newly launched current version.
https://vwad.owasp.org
The kernel documentation search work here is a different system with
a different purpose, but it is adjacent experience, and I would
appreciate any feedback you or others may have on improving the
patch.
Rito
^ permalink raw reply [flat|nested] 14+ messages in thread
* [PATCH v2] docs: add advanced search for kernel documentation
2026-03-21 18:15 [PATCH] docs: add advanced search for kernel documentation Rito Rhymes
2026-03-21 22:53 ` Randy Dunlap
2026-03-22 16:01 ` Jonathan Corbet
@ 2026-03-22 18:17 ` Rito Rhymes
2 siblings, 0 replies; 14+ messages in thread
From: Rito Rhymes @ 2026-03-22 18:17 UTC (permalink / raw)
To: Jonathan Corbet, Mauro Carvalho Chehab, linux-doc
Cc: Shuah Khan, linux-kernel, rdunlap, Rito Rhymes
Replace the stock Sphinx search page with one that reuses the
existing searchindex.js while adding structured result grouping,
filtering, and exact identifier matching.
Results are grouped into Symbols, Sections, Index entries, and
Pages, each in a collapsible section with a count. An Advanced
panel exposes filters for documentation area, object type, result
kind, and exact match mode. All state is URL-encoded so searches
remain shareable.
Page summary snippets are lazy-loaded via IntersectionObserver to
avoid fetching every matching page up front.
The sidebar keeps the existing quick-search box and adds an
"Advanced search" link below it.
Signed-off-by: Rito Rhymes <rito@ritovision.com>
Assisted-by: Codex:GPT-5.4
Assisted-by: Claude:Opus-4.6
---
v2: add Assisted-by attribution
Documentation/sphinx-static/custom.css | 163 ++++
Documentation/sphinx-static/kernel-search.js | 746 ++++++++++++++++++
Documentation/sphinx/templates/search.html | 106 +++
Documentation/sphinx/templates/searchbox.html | 18 +
4 files changed, 1033 insertions(+)
create mode 100644 Documentation/sphinx-static/kernel-search.js
create mode 100644 Documentation/sphinx/templates/search.html
create mode 100644 Documentation/sphinx/templates/searchbox.html
diff --git a/Documentation/sphinx-static/custom.css b/Documentation/sphinx-static/custom.css
index db24f4344..dd7cc221e 100644
--- a/Documentation/sphinx-static/custom.css
+++ b/Documentation/sphinx-static/custom.css
@@ -169,3 +169,166 @@ a.manpage {
font-weight: bold;
font-family: "Courier New", Courier, monospace;
}
+
+/* Keep the quick search box as-is and add a secondary advanced search link. */
+div.sphinxsidebar p.search-advanced-link {
+ margin: 0.5em 0 0 0;
+ font-size: 0.95em;
+}
+
+/*
+ * The enhanced search page keeps the stock GET workflow but adds
+ * filter controls and grouped results.
+ */
+form.kernel-search-form {
+ margin-bottom: 2em;
+}
+
+div.kernel-search-query-row {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 1em;
+ align-items: end;
+}
+
+div.kernel-search-query-field {
+ flex: 1 1 26em;
+}
+
+div.kernel-search-query-field label,
+div.kernel-search-field label,
+fieldset.kernel-search-kind-filters legend {
+ display: block;
+ font-weight: bold;
+ margin-bottom: 0.35em;
+}
+
+div.kernel-search-query-field input[type="text"],
+div.kernel-search-field select {
+ width: 100%;
+ box-sizing: border-box;
+}
+
+div.kernel-search-query-actions {
+ display: flex;
+ align-items: center;
+ gap: 0.75em;
+}
+
+span.kernel-search-progress {
+ min-height: 1.2em;
+ color: #666;
+}
+
+details.kernel-search-advanced {
+ margin-top: 1em;
+ padding: 0.75em 1em 1em 1em;
+ border: 1px solid #cccccc;
+ background: #f7f7f7;
+}
+
+details.kernel-search-advanced summary {
+ cursor: pointer;
+ font-weight: bold;
+}
+
+div.kernel-search-advanced-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(16em, 1fr));
+ gap: 1em 1.25em;
+ margin-top: 1em;
+}
+
+fieldset.kernel-search-kind-filters {
+ margin: 0;
+ padding: 0;
+ border: none;
+}
+
+label.kernel-search-checkbox {
+ display: flex;
+ align-items: flex-start;
+ gap: 0.5em;
+ margin-bottom: 0.35em;
+}
+
+div.kernel-search-results {
+ margin-top: 1.5em;
+}
+
+p.kernel-search-status {
+ margin-bottom: 1.5em;
+}
+
+details.kernel-search-group {
+ margin-top: 2em;
+}
+
+summary.kernel-search-group-summary {
+ cursor: pointer;
+ font-size: 150%;
+ margin: 0 0 0.6em 0;
+}
+
+summary.kernel-search-group-summary h2.kernel-search-group-title {
+ display: inline;
+ margin: 0;
+ font-size: inherit;
+ font-weight: normal;
+}
+
+span.kernel-search-group-count {
+ color: #666666;
+ margin-left: 0.35em;
+}
+
+ol.kernel-search-list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+li.kernel-search-result {
+ padding: 0.9em 0;
+ border-top: 1px solid #dddddd;
+}
+
+li.kernel-search-result:first-child {
+ border-top: none;
+}
+
+div.kernel-search-result-heading {
+ font-weight: bold;
+}
+
+div.kernel-search-path,
+div.kernel-search-meta,
+p.kernel-search-summary {
+ margin-top: 0.3em;
+ color: #555555;
+}
+
+div.kernel-search-path,
+div.kernel-search-meta {
+ font-size: 0.95em;
+}
+
+p.kernel-search-summary {
+ margin-bottom: 0;
+}
+
+@media screen and (max-width: 65em) {
+ div.kernel-search-query-actions {
+ width: 100%;
+ justify-content: flex-start;
+ }
+}
+
+@media screen and (min-width: 65em) {
+ div.kernel-search-result-heading,
+ div.kernel-search-path,
+ div.kernel-search-meta,
+ p.kernel-search-summary {
+ margin-left: 2rem;
+ }
+}
diff --git a/Documentation/sphinx-static/kernel-search.js b/Documentation/sphinx-static/kernel-search.js
new file mode 100644
index 000000000..bcf79f820
--- /dev/null
+++ b/Documentation/sphinx-static/kernel-search.js
@@ -0,0 +1,746 @@
+"use strict";
+
+(() => {
+ const RESULT_KIND_ORDER = ["object", "title", "index", "text"];
+ const RESULT_KIND_LABELS = {
+ object: "Symbols",
+ title: "Sections",
+ index: "Index entries",
+ text: "Pages",
+ };
+ const TOP_LEVEL_AREA = "__top_level__";
+ const OBJECT_PRIORITY = {
+ 0: 15,
+ 1: 5,
+ 2: -5,
+ };
+ const SUMMARY_ROOT_MARGIN = "300px 0px";
+ const documentTextCache = new Map();
+ const summaryTargets = new WeakMap();
+ let summaryObserver = null;
+
+ window.Search = window.Search || {};
+ window.Search._callbacks = window.Search._callbacks || [];
+ window.Search._index = window.Search._index || null;
+ window.Search.setIndex = (index) => {
+ window.Search._index = index;
+ const callbacks = window.Search._callbacks.slice();
+ window.Search._callbacks.length = 0;
+ callbacks.forEach((callback) => callback(index));
+ };
+ window.Search.whenReady = (callback) => {
+ if (window.Search._index) callback(window.Search._index);
+ else window.Search._callbacks.push(callback);
+ };
+
+ const splitQuery = (query) =>
+ query
+ .split(/[^\p{Letter}\p{Number}_\p{Emoji_Presentation}]+/gu)
+ .filter((term) => term);
+
+ const getStemmer = () =>
+ typeof Stemmer === "function" ? new Stemmer() : { stemWord: (word) => word };
+
+ const hasOwn = (object, key) =>
+ Object.prototype.hasOwnProperty.call(object, key);
+
+ const getContentRoot = () =>
+ document.documentElement.dataset.content_root
+ || (typeof DOCUMENTATION_OPTIONS !== "undefined" ? DOCUMENTATION_OPTIONS.URL_ROOT || "" : "");
+
+ const compareResults = (left, right) => {
+ if (left.score === right.score) {
+ const leftTitle = left.title.toLowerCase();
+ const rightTitle = right.title.toLowerCase();
+ if (leftTitle === rightTitle) return 0;
+ return leftTitle < rightTitle ? -1 : 1;
+ }
+ return right.score - left.score;
+ };
+
+ const getAreaValue = (docName) =>
+ docName.includes("/") ? docName.split("/", 1)[0] : TOP_LEVEL_AREA;
+
+ const getAreaLabel = (area) =>
+ area === TOP_LEVEL_AREA ? "Top level" : area;
+
+ const matchArea = (docName, area) => {
+ if (!area) return true;
+ if (area === TOP_LEVEL_AREA) return !docName.includes("/");
+ return docName === area || docName.startsWith(area + "/");
+ };
+
+ const buildDocUrls = (docName) => {
+ const contentRoot = getContentRoot();
+ const builder = DOCUMENTATION_OPTIONS.BUILDER;
+ const fileSuffix = DOCUMENTATION_OPTIONS.FILE_SUFFIX;
+ const linkSuffix = DOCUMENTATION_OPTIONS.LINK_SUFFIX;
+
+ if (builder === "dirhtml") {
+ let dirname = docName + "/";
+ if (dirname.match(/\/index\/$/)) dirname = dirname.substring(0, dirname.length - 6);
+ else if (dirname === "index/") dirname = "";
+
+ return {
+ requestUrl: contentRoot + dirname,
+ linkUrl: contentRoot + dirname,
+ };
+ }
+
+ return {
+ requestUrl: contentRoot + docName + fileSuffix,
+ linkUrl: docName + linkSuffix,
+ };
+ };
+
+ const htmlToText = (htmlString, anchor) => {
+ const htmlElement = new DOMParser().parseFromString(htmlString, "text/html");
+ for (const selector of [".headerlink", "script", "style"]) {
+ htmlElement.querySelectorAll(selector).forEach((element) => element.remove());
+ }
+
+ if (anchor) {
+ const anchorId = anchor[0] === "#" ? anchor.substring(1) : anchor;
+ const anchorContent = htmlElement.getElementById(anchorId);
+ if (anchorContent) return anchorContent.textContent;
+ }
+
+ const docContent = htmlElement.querySelector('[role="main"]');
+ return docContent ? docContent.textContent : "";
+ };
+
+ const makeSummary = (htmlText, keywords, anchor) => {
+ const text = htmlToText(htmlText, anchor);
+ if (!text) return null;
+
+ const lowered = text.toLowerCase();
+ const positions = keywords
+ .map((keyword) => lowered.indexOf(keyword.toLowerCase()))
+ .filter((position) => position > -1);
+ const actualStart = positions.length ? positions[0] : 0;
+ const start = Math.max(actualStart - 120, 0);
+ const prefix = start === 0 ? "" : "...";
+ const suffix = start + 240 < text.length ? "..." : "";
+
+ const summary = document.createElement("p");
+ summary.className = "kernel-search-summary";
+ summary.textContent = prefix + text.substring(start, start + 240).trim() + suffix;
+ return summary;
+ };
+
+ const loadDocumentText = (requestUrl) => {
+ if (!documentTextCache.has(requestUrl)) {
+ documentTextCache.set(
+ requestUrl,
+ fetch(requestUrl)
+ .then((response) => (response.ok ? response.text() : ""))
+ .catch(() => ""),
+ );
+ }
+
+ return documentTextCache.get(requestUrl);
+ };
+
+ const pushBest = (resultMap, result) => {
+ const key = [result.kind, result.docName, result.anchor || "", result.title].join("|");
+ const existing = resultMap.get(key);
+ if (!existing || existing.score < result.score) resultMap.set(key, result);
+ };
+
+ const buildQueryState = (query, exact) => {
+ const rawTerms = splitQuery(query.trim());
+ const rawTermsLower = rawTerms.map((term) => term.toLowerCase());
+ const objectTerms = new Set(rawTermsLower);
+ const highlightTerms = exact ? rawTermsLower : [];
+ const searchTerms = new Set();
+ const excludedTerms = new Set();
+
+ if (!exact) {
+ const stemmer = getStemmer();
+ rawTerms.forEach((term) => {
+ const lowered = term.toLowerCase();
+ if ((typeof stopwords !== "undefined" && stopwords.has(lowered)) || /^\d+$/.test(term)) {
+ return;
+ }
+
+ const word = stemmer.stemWord(lowered);
+ if (!word) return;
+
+ if (word[0] === "-") excludedTerms.add(word.substring(1));
+ else {
+ searchTerms.add(word);
+ highlightTerms.push(lowered);
+ }
+ });
+ } else {
+ rawTermsLower.forEach((term) => searchTerms.add(term));
+ }
+
+ if (typeof SPHINX_HIGHLIGHT_ENABLED !== "undefined" && SPHINX_HIGHLIGHT_ENABLED) {
+ localStorage.setItem("sphinx_highlight_terms", [...new Set(highlightTerms)].join(" "));
+ }
+
+ return {
+ exact,
+ query,
+ queryLower: query.toLowerCase().trim(),
+ rawTerms: rawTermsLower,
+ objectTerms,
+ searchTerms,
+ excludedTerms,
+ highlightTerms: [...new Set(highlightTerms)],
+ };
+ };
+
+ const candidateMatches = (candidateLower, state) => {
+ if (!state.queryLower) return false;
+ if (state.exact) return candidateLower === state.queryLower;
+
+ if (
+ candidateLower.includes(state.queryLower)
+ && state.queryLower.length >= Math.ceil(candidateLower.length / 2)
+ ) {
+ return true;
+ }
+
+ return state.rawTerms.length > 0
+ && state.rawTerms.every((term) => candidateLower.includes(term));
+ };
+
+ const scoreLabelMatch = (candidateLower, state, baseScore, partialScore) => {
+ if (state.exact) return baseScore + 20;
+ if (candidateLower === state.queryLower) return baseScore + 10;
+ if (candidateLower.includes(state.queryLower)) {
+ return Math.max(partialScore, Math.round((baseScore * state.queryLower.length) / candidateLower.length));
+ }
+
+ return partialScore * Math.max(1, state.rawTerms.filter((term) => candidateLower.includes(term)).length);
+ };
+
+ const collectObjectResults = (index, state, filters) => {
+ const resultMap = new Map();
+ const objects = index.objects || {};
+ const objNames = index.objnames || {};
+ const objTypes = index.objtypes || {};
+
+ Object.keys(objects).forEach((prefix) => {
+ objects[prefix].forEach((match) => {
+ const fileIndex = match[0];
+ const typeIndex = match[1];
+ const priority = match[2];
+ const anchorValue = match[3];
+ const name = match[4];
+ const docName = index.docnames[fileIndex];
+ const fileName = index.filenames[fileIndex];
+ const pageTitle = index.titles[fileIndex];
+ const objectLabel = objNames[typeIndex] ? objNames[typeIndex][2] : "Object";
+ const objectType = objTypes[typeIndex];
+
+ if (!matchArea(docName, filters.area)) return;
+ if (filters.objtype && filters.objtype !== objectType) return;
+
+ const fullName = prefix ? prefix + "." + name : name;
+ const fullNameLower = fullName.toLowerCase();
+ const lastNameLower = fullNameLower.split(".").slice(-1)[0];
+ const nameLower = name.toLowerCase();
+
+ let score = 0;
+ if (state.exact) {
+ if (
+ fullNameLower !== state.queryLower
+ && lastNameLower !== state.queryLower
+ && nameLower !== state.queryLower
+ ) {
+ return;
+ }
+ score = 120;
+ } else {
+ const haystack = `${fullName} ${objectLabel} ${pageTitle}`.toLowerCase();
+ if (state.objectTerms.size === 0) return;
+ if ([...state.objectTerms].some((term) => !haystack.includes(term))) return;
+ const matchedNameTerms = state.rawTerms.filter(
+ (term) =>
+ fullNameLower.includes(term)
+ || lastNameLower.includes(term)
+ || nameLower.includes(term),
+ ).length;
+
+ if (
+ fullNameLower === state.queryLower
+ || lastNameLower === state.queryLower
+ || nameLower === state.queryLower
+ ) {
+ score += 11;
+ } else if (
+ lastNameLower.includes(state.queryLower)
+ || nameLower.includes(state.queryLower)
+ ) {
+ score += 6;
+ } else if (fullNameLower.includes(state.queryLower)) {
+ score += 4;
+ } else if (matchedNameTerms > 0) {
+ score += matchedNameTerms;
+ } else {
+ return;
+ }
+ }
+
+ score += OBJECT_PRIORITY[priority] || 0;
+
+ let anchor = anchorValue;
+ if (anchor === "") anchor = fullName;
+ else if (anchor === "-" && objNames[typeIndex]) anchor = objNames[typeIndex][1] + "-" + fullName;
+
+ pushBest(resultMap, {
+ kind: "object",
+ docName,
+ fileName,
+ title: fullName,
+ anchor: anchor ? "#" + anchor : "",
+ description: `${objectLabel}, in ${pageTitle}`,
+ score,
+ });
+ });
+ });
+
+ return [...resultMap.values()].sort(compareResults);
+ };
+
+ const collectSectionResults = (index, state, filters) => {
+ const resultMap = new Map();
+ const allTitles = index.alltitles || {};
+
+ Object.entries(allTitles).forEach(([sectionTitle, entries]) => {
+ const lowered = sectionTitle.toLowerCase().trim();
+ if (!candidateMatches(lowered, state)) return;
+
+ entries.forEach(([fileIndex, anchorId]) => {
+ const docName = index.docnames[fileIndex];
+ const fileName = index.filenames[fileIndex];
+ const pageTitle = index.titles[fileIndex];
+ if (!matchArea(docName, filters.area)) return;
+
+ if (anchorId === null && sectionTitle === pageTitle) return;
+
+ pushBest(resultMap, {
+ kind: "title",
+ docName,
+ fileName,
+ title: pageTitle !== sectionTitle ? `${pageTitle} > ${sectionTitle}` : sectionTitle,
+ anchor: anchorId ? "#" + anchorId : "",
+ description: pageTitle,
+ score: scoreLabelMatch(lowered, state, 15, 7),
+ });
+ });
+ });
+
+ return [...resultMap.values()].sort(compareResults);
+ };
+
+ const collectIndexResults = (index, state, filters) => {
+ const resultMap = new Map();
+ const entries = index.indexentries || {};
+
+ Object.entries(entries).forEach(([entry, matches]) => {
+ const lowered = entry.toLowerCase().trim();
+ if (!candidateMatches(lowered, state)) return;
+
+ matches.forEach(([fileIndex, anchorId, isMain]) => {
+ const docName = index.docnames[fileIndex];
+ const fileName = index.filenames[fileIndex];
+ const pageTitle = index.titles[fileIndex];
+ if (!matchArea(docName, filters.area)) return;
+
+ let score = scoreLabelMatch(lowered, state, 20, 8);
+ if (!isMain) score -= 5;
+
+ pushBest(resultMap, {
+ kind: "index",
+ docName,
+ fileName,
+ title: entry,
+ anchor: anchorId ? "#" + anchorId : "",
+ description: pageTitle,
+ score,
+ });
+ });
+ });
+
+ return [...resultMap.values()].sort(compareResults);
+ };
+
+ const collectTextResults = (index, state, filters) => {
+ const resultMap = new Map();
+ const terms = index.terms || {};
+ const titleTerms = index.titleterms || {};
+ const searchTerms = [...state.searchTerms];
+
+ if (searchTerms.length === 0) return [];
+
+ const scoreMap = new Map();
+ const fileMap = new Map();
+
+ searchTerms.forEach((word) => {
+ const files = [];
+ const candidates = [
+ {
+ files: hasOwn(terms, word) ? terms[word] : undefined,
+ score: 5,
+ },
+ {
+ files: hasOwn(titleTerms, word) ? titleTerms[word] : undefined,
+ score: 15,
+ },
+ ];
+
+ if (!state.exact && word.length > 2) {
+ if (!hasOwn(terms, word)) {
+ Object.keys(terms).forEach((term) => {
+ if (term.includes(word)) candidates.push({ files: terms[term], score: 2 });
+ });
+ }
+ if (!hasOwn(titleTerms, word)) {
+ Object.keys(titleTerms).forEach((term) => {
+ if (term.includes(word)) candidates.push({ files: titleTerms[term], score: 7 });
+ });
+ }
+ }
+
+ if (candidates.every((candidate) => candidate.files === undefined)) return;
+
+ candidates.forEach((candidate) => {
+ if (candidate.files === undefined) return;
+
+ let recordFiles = candidate.files;
+ if (recordFiles.length === undefined) recordFiles = [recordFiles];
+ files.push(...recordFiles);
+
+ recordFiles.forEach((fileIndex) => {
+ if (!scoreMap.has(fileIndex)) scoreMap.set(fileIndex, new Map());
+ const currentScore = scoreMap.get(fileIndex).get(word) || 0;
+ scoreMap.get(fileIndex).set(word, Math.max(currentScore, candidate.score));
+ });
+ });
+
+ files.forEach((fileIndex) => {
+ if (!fileMap.has(fileIndex)) fileMap.set(fileIndex, [word]);
+ else if (!fileMap.get(fileIndex).includes(word)) fileMap.get(fileIndex).push(word);
+ });
+ });
+
+ const filteredTermCount = state.exact
+ ? searchTerms.length
+ : searchTerms.filter((term) => term.length > 2).length;
+
+ for (const [fileIndex, matchedWords] of fileMap.entries()) {
+ const docName = index.docnames[fileIndex];
+ const fileName = index.filenames[fileIndex];
+ if (!matchArea(docName, filters.area)) continue;
+
+ if (matchedWords.length !== searchTerms.length && matchedWords.length !== filteredTermCount) {
+ continue;
+ }
+
+ if (
+ [...state.excludedTerms].some(
+ (term) =>
+ terms[term] === fileIndex
+ || titleTerms[term] === fileIndex
+ || (terms[term] || []).includes(fileIndex)
+ || (titleTerms[term] || []).includes(fileIndex),
+ )
+ ) {
+ continue;
+ }
+
+ let score = Math.max(...matchedWords.map((word) => scoreMap.get(fileIndex).get(word)));
+ if (state.exact && index.titles[fileIndex].toLowerCase() === state.queryLower) score += 10;
+
+ pushBest(resultMap, {
+ kind: "text",
+ docName,
+ fileName,
+ title: index.titles[fileIndex],
+ anchor: "",
+ description: null,
+ score,
+ });
+ }
+
+ return [...resultMap.values()].sort(compareResults);
+ };
+
+ const buildFilters = (state) => ({
+ area: state.area,
+ objtype: state.objtype,
+ });
+
+ const ensureSummaryObserver = () => {
+ if (summaryObserver || typeof IntersectionObserver !== "function") return summaryObserver;
+
+ summaryObserver = new IntersectionObserver((entries) => {
+ entries.forEach((entry) => {
+ if (!entry.isIntersecting) return;
+
+ const target = entry.target;
+ summaryObserver.unobserve(target);
+ const payload = summaryTargets.get(target);
+ if (!payload || payload.loaded) return;
+
+ payload.loaded = true;
+ loadDocumentText(payload.requestUrl).then((htmlText) => {
+ if (!htmlText) return;
+
+ const summary = makeSummary(htmlText, payload.keywords, payload.anchor);
+ if (!summary) return;
+
+ payload.item.appendChild(summary);
+ });
+ });
+ }, { rootMargin: SUMMARY_ROOT_MARGIN });
+
+ return summaryObserver;
+ };
+
+ const queueSummaryLoad = (result, item, keywords) => {
+ const urls = buildDocUrls(result.docName);
+ const payload = {
+ anchor: result.anchor,
+ item,
+ keywords,
+ loaded: false,
+ requestUrl: urls.requestUrl,
+ };
+
+ const observer = ensureSummaryObserver();
+ if (!observer) {
+ payload.loaded = true;
+ loadDocumentText(payload.requestUrl).then((htmlText) => {
+ if (!htmlText) return;
+
+ const summary = makeSummary(htmlText, keywords, result.anchor);
+ if (summary) item.appendChild(summary);
+ });
+ return;
+ }
+
+ summaryTargets.set(item, payload);
+ observer.observe(item);
+ };
+
+ const createResultItem = (result, keywords) => {
+ const urls = buildDocUrls(result.docName);
+ const item = document.createElement("li");
+ item.className = `kernel-search-result kind-${result.kind}`;
+
+ const heading = item.appendChild(document.createElement("div"));
+ heading.className = "kernel-search-result-heading";
+
+ const link = heading.appendChild(document.createElement("a"));
+ link.href = urls.linkUrl + result.anchor;
+ link.dataset.score = String(result.score);
+ link.textContent = result.title;
+
+ const path = item.appendChild(document.createElement("div"));
+ path.className = "kernel-search-path";
+ path.textContent = result.fileName;
+
+ if (result.description) {
+ const meta = item.appendChild(document.createElement("div"));
+ meta.className = "kernel-search-meta";
+ meta.textContent = result.description;
+ }
+
+ if (result.kind === "text") {
+ queueSummaryLoad(result, item, keywords);
+ }
+ return item;
+ };
+
+ const renderResults = (state, resultsByKind) => {
+ const container = document.getElementById("kernel-search-results");
+ const totalResults = RESULT_KIND_ORDER.reduce(
+ (count, kind) => count + resultsByKind[kind].length,
+ 0,
+ );
+ if (summaryObserver) {
+ summaryObserver.disconnect();
+ summaryObserver = null;
+ }
+ container.replaceChildren();
+
+ const summary = document.createElement("p");
+ summary.className = "kernel-search-status";
+ if (!state.queryLower) {
+ summary.textContent = "Enter a search query to browse kernel documentation.";
+ container.appendChild(summary);
+ return;
+ }
+
+ if (!totalResults) {
+ summary.textContent =
+ "No matching results were found for the current query and filters.";
+ container.appendChild(summary);
+ return;
+ }
+
+ summary.textContent =
+ `Found ${totalResults} result${totalResults === 1 ? "" : "s"} for "${state.query}".`;
+ container.appendChild(summary);
+
+ RESULT_KIND_ORDER.forEach((kind) => {
+ const results = resultsByKind[kind];
+ if (!results.length) return;
+
+ const group = container.appendChild(document.createElement("details"));
+ group.className = `kernel-search-group kind-${kind}`;
+ group.open = true;
+
+ const summary = group.appendChild(document.createElement("summary"));
+ summary.className = "kernel-search-group-summary";
+
+ const heading = summary.appendChild(document.createElement("h2"));
+ heading.className = "kernel-search-group-title";
+ heading.textContent = RESULT_KIND_LABELS[kind];
+
+ const count = summary.appendChild(document.createElement("span"));
+ count.className = "kernel-search-group-count";
+ count.textContent = `(${results.length})`;
+
+ const list = group.appendChild(document.createElement("ol"));
+ list.className = "kernel-search-list";
+
+ results.forEach((result) => {
+ list.appendChild(createResultItem(result, state.highlightTerms));
+ });
+ });
+ };
+
+ const populateAreaOptions = (select, state) => {
+ const areas = new Set();
+ window.Search._index.docnames.forEach((docName) => areas.add(getAreaValue(docName)));
+
+ const options = [new Option("All documentation areas", "", false, !state.area)];
+ [...areas]
+ .sort((left, right) => {
+ if (left === TOP_LEVEL_AREA) return -1;
+ if (right === TOP_LEVEL_AREA) return 1;
+ return left.localeCompare(right);
+ })
+ .forEach((area) => {
+ options.push(new Option(getAreaLabel(area), area, false, area === state.area));
+ });
+
+ select.replaceChildren(...options);
+ };
+
+ const populateObjectTypeOptions = (select, state) => {
+ const objTypes = window.Search._index.objtypes || {};
+ const objNames = window.Search._index.objnames || {};
+ const entries = Object.keys(objTypes)
+ .map((key) => ({
+ value: objTypes[key],
+ label: objNames[key] ? objNames[key][2] : objTypes[key],
+ }))
+ .sort((left, right) => left.label.localeCompare(right.label));
+
+ const seen = new Set();
+ const options = [new Option("All object types", "", false, !state.objtype)];
+ entries.forEach((entry) => {
+ if (seen.has(entry.value)) return;
+ seen.add(entry.value);
+ options.push(new Option(entry.label, entry.value, false, entry.value === state.objtype));
+ });
+
+ select.replaceChildren(...options);
+ };
+
+ const parseState = () => {
+ const params = new URLSearchParams(window.location.search);
+ const kinds = params.getAll("kind").filter((kind) => RESULT_KIND_ORDER.includes(kind));
+
+ return {
+ query: params.get("q") || "",
+ queryLower: (params.get("q") || "").toLowerCase().trim(),
+ exact: params.get("exact") === "1",
+ area: params.get("area") || "",
+ objtype: params.get("objtype") || "",
+ advanced: params.get("advanced") === "1",
+ kinds: kinds.length ? new Set(kinds) : new Set(RESULT_KIND_ORDER),
+ };
+ };
+
+ const shouldOpenAdvanced = (state) =>
+ state.advanced
+ || state.exact
+ || state.area !== ""
+ || state.objtype !== ""
+ || RESULT_KIND_ORDER.some((kind) => !state.kinds.has(kind));
+
+ const bindFormState = (state) => {
+ document.getElementById("kernel-search-query").value = state.query;
+ document.getElementById("kernel-search-exact").checked = state.exact;
+ RESULT_KIND_ORDER.forEach((kind) => {
+ const checkbox = document.getElementById(`kernel-search-kind-${kind}`);
+ if (checkbox) checkbox.checked = state.kinds.has(kind);
+ });
+
+ const advanced = document.getElementById("kernel-search-advanced");
+ const advancedFlag = document.getElementById("kernel-search-advanced-flag");
+ const open = shouldOpenAdvanced(state);
+ advanced.open = open;
+ advancedFlag.disabled = !open;
+ advanced.addEventListener("toggle", () => {
+ advancedFlag.disabled = !advanced.open;
+ });
+ };
+
+ const runSearch = () => {
+ const baseState = parseState();
+ bindFormState(baseState);
+ populateAreaOptions(document.getElementById("kernel-search-area"), baseState);
+ populateObjectTypeOptions(document.getElementById("kernel-search-objtype"), baseState);
+
+ const queryState = buildQueryState(baseState.query, baseState.exact);
+ const filters = buildFilters(baseState);
+ const resultsByKind = {
+ object: [],
+ title: [],
+ index: [],
+ text: [],
+ };
+
+ if (!baseState.queryLower) {
+ renderResults(baseState, resultsByKind);
+ return;
+ }
+
+ if (baseState.kinds.has("object")) {
+ resultsByKind.object = collectObjectResults(window.Search._index, queryState, filters);
+ }
+ if (baseState.kinds.has("title")) {
+ resultsByKind.title = collectSectionResults(window.Search._index, queryState, filters);
+ }
+ if (baseState.kinds.has("index")) {
+ resultsByKind.index = collectIndexResults(window.Search._index, queryState, filters);
+ }
+ if (baseState.kinds.has("text")) {
+ resultsByKind.text = collectTextResults(window.Search._index, queryState, filters);
+ }
+
+ renderResults(baseState, resultsByKind);
+ };
+
+ document.addEventListener("DOMContentLoaded", () => {
+ const container = document.getElementById("kernel-search-results");
+ if (!container) return;
+
+ const progress = document.getElementById("search-progress");
+ if (progress) progress.textContent = "Preparing search...";
+
+ window.Search.whenReady(() => {
+ if (progress) progress.textContent = "";
+ runSearch();
+ });
+ });
+})();
diff --git a/Documentation/sphinx/templates/search.html b/Documentation/sphinx/templates/search.html
new file mode 100644
index 000000000..966740c12
--- /dev/null
+++ b/Documentation/sphinx/templates/search.html
@@ -0,0 +1,106 @@
+{# SPDX-License-Identifier: GPL-2.0 #}
+
+{# Enhanced search page for kernel documentation. #}
+{%- extends "layout.html" %}
+{% set title = _('Search') %}
+{%- block scripts %}
+ {{ super() }}
+ <script src="{{ pathto('_static/language_data.js', 1) }}"></script>
+ <script src="{{ pathto('_static/kernel-search.js', 1) }}"></script>
+{%- endblock %}
+{% block extrahead %}
+ <script src="{{ pathto('searchindex.js', 1) }}" defer="defer"></script>
+ <meta name="robots" content="noindex" />
+ {{ super() }}
+{% endblock %}
+{% block body %}
+ <h1 id="search-documentation">{{ _('Search') }}</h1>
+ <noscript>
+ <div class="admonition warning">
+ <p>
+ {% trans %}Please activate JavaScript to enable the search
+ functionality.{% endtrans %}
+ </p>
+ </div>
+ </noscript>
+ <p class="kernel-search-help">
+ {% trans %}Searching for multiple words only shows matches that contain
+ all words.{% endtrans %}
+ </p>
+ <form id="kernel-search-form" class="kernel-search-form" action="" method="get">
+ <div class="kernel-search-query-row">
+ <div class="kernel-search-query-field">
+ <label for="kernel-search-query">{{ _('Search query') }}</label>
+ <input
+ id="kernel-search-query"
+ type="text"
+ name="q"
+ value=""
+ autocomplete="off"
+ autocorrect="off"
+ autocapitalize="off"
+ spellcheck="false"
+ />
+ </div>
+ <div class="kernel-search-query-actions">
+ <input type="submit" value="{{ _('Search') }}" />
+ <span id="search-progress" class="kernel-search-progress"></span>
+ </div>
+ </div>
+
+ <details id="kernel-search-advanced" class="kernel-search-advanced">
+ <summary>{{ _('Advanced search') }}</summary>
+ <input
+ id="kernel-search-advanced-flag"
+ type="hidden"
+ name="advanced"
+ value="1"
+ disabled="disabled"
+ />
+ <div class="kernel-search-advanced-grid">
+ <div class="kernel-search-field">
+ <label class="kernel-search-checkbox" for="kernel-search-exact">
+ <input id="kernel-search-exact" type="checkbox" name="exact" value="1" />
+ <span>{{ _('Exact identifier match') }}</span>
+ </label>
+ </div>
+
+ <fieldset class="kernel-search-kind-filters">
+ <legend>{{ _('Result kinds') }}</legend>
+ <label class="kernel-search-checkbox" for="kernel-search-kind-object">
+ <input id="kernel-search-kind-object" type="checkbox" name="kind" value="object" />
+ <span>{{ _('Symbols') }}</span>
+ </label>
+ <label class="kernel-search-checkbox" for="kernel-search-kind-title">
+ <input id="kernel-search-kind-title" type="checkbox" name="kind" value="title" />
+ <span>{{ _('Sections') }}</span>
+ </label>
+ <label class="kernel-search-checkbox" for="kernel-search-kind-index">
+ <input id="kernel-search-kind-index" type="checkbox" name="kind" value="index" />
+ <span>{{ _('Index entries') }}</span>
+ </label>
+ <label class="kernel-search-checkbox" for="kernel-search-kind-text">
+ <input id="kernel-search-kind-text" type="checkbox" name="kind" value="text" />
+ <span>{{ _('Pages') }}</span>
+ </label>
+ </fieldset>
+
+ <div class="kernel-search-field">
+ <label for="kernel-search-area">{{ _('Documentation area') }}</label>
+ <select id="kernel-search-area" name="area">
+ <option value="">{{ _('All documentation areas') }}</option>
+ </select>
+ </div>
+
+ <div class="kernel-search-field">
+ <label for="kernel-search-objtype">{{ _('Object type') }}</label>
+ <select id="kernel-search-objtype" name="objtype">
+ <option value="">{{ _('All object types') }}</option>
+ </select>
+ </div>
+ </div>
+ </details>
+ </form>
+
+ <div id="kernel-search-results" class="kernel-search-results"></div>
+{% endblock %}
diff --git a/Documentation/sphinx/templates/searchbox.html b/Documentation/sphinx/templates/searchbox.html
new file mode 100644
index 000000000..6caa0498a
--- /dev/null
+++ b/Documentation/sphinx/templates/searchbox.html
@@ -0,0 +1,18 @@
+{# SPDX-License-Identifier: GPL-2.0 #}
+
+{# Sphinx sidebar template: quick search box plus advanced search link. #}
+{%- if pagename != "search" and builder != "singlehtml" %}
+<search id="searchbox" style="display: none" role="search">
+ <h3 id="searchlabel">{{ _('Quick search') }}</h3>
+ <div class="searchformwrapper">
+ <form class="search" action="{{ pathto('search') }}" method="get">
+ <input type="text" name="q" aria-labelledby="searchlabel" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
+ <input type="submit" value="{{ _('Go') }}" />
+ </form>
+ </div>
+ <p class="search-advanced-link">
+ <a href="{{ pathto('search') }}?advanced=1">{{ _('Advanced search') }}</a>
+ </p>
+</search>
+<script>document.getElementById('searchbox').style.display = "block"</script>
+{%- endif %}
--
2.51.0
^ permalink raw reply related [flat|nested] 14+ messages in thread
* Re: [PATCH] docs: add advanced search for kernel documentation
2026-03-22 1:12 ` Rito Rhymes
@ 2026-03-22 19:59 ` Randy Dunlap
2026-03-23 22:50 ` Rito Rhymes
0 siblings, 1 reply; 14+ messages in thread
From: Randy Dunlap @ 2026-03-22 19:59 UTC (permalink / raw)
To: Rito Rhymes, Jonathan Corbet, Mauro Carvalho Chehab, linux-doc
Cc: Shuah Khan, linux-kernel
On 3/21/26 6:12 PM, Rito Rhymes wrote:
> I was using the in-tree build (`make htmldocs`) before. I just ran
> fresh rebuilds with both in-tree and out-of-tree (`make O=DOCS
> htmldocs`) builds, and I got identical search results of 280 hits for
> "futex" when serving both.
>
> I ran the builds and served the output on Linux/x86_64, and tested the
> pages in Chrome, Edge, and Firefox on Windows.
>
> So at this point I have not been able to reproduce the "no output"
> behavior with either build mode, I think we can rule out build mode
> quirks.
>
> More debugging:
>
> As a comparison, WITHOUT using my patch, upstream only build, does the
> normal Quick Search work? What results do you get for Futex?
Yes:
Search Results
Search finished, found 225 pages matching the search query.
> WITH my patch, do Quick Search return any results for Futex?
No. Just a search dialog page with no matches listed.
> How are you opening or serving the generated docs? For example, via a
> local web server (`http://...`) or directly from disk (`file://...`)?
> I am serving the generated output over HTTP locally via
> `python -m http.server`.
file://...
> What Sphinx version are you using for the build?
> I built with Sphinx 9.1.0.
8.2.3-4.2 (latest from openSUSE Tumbleweed, rolling updates)
> If possible, could you also check in the browser network tab whether
> `_static/kernel-search.js`, `_static/language_data.js`, and
> `searchindex.js` are all loading successfully?
They appear to be (this is new to me).
They are listed and I don't see any errors associated with them.
--
~Randy
^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [PATCH] docs: add advanced search for kernel documentation
2026-03-22 17:08 ` Rito Rhymes
2026-03-22 17:25 ` Rito Rhymes
@ 2026-03-22 20:25 ` Jonathan Corbet
1 sibling, 0 replies; 14+ messages in thread
From: Jonathan Corbet @ 2026-03-22 20:25 UTC (permalink / raw)
To: Rito Rhymes, Rito Rhymes, Mauro Carvalho Chehab, linux-doc
Cc: Shuah Khan, linux-kernel
"Rito Rhymes" <rito@ritovision.com> writes:
> Hi Jon
>
>> Documentation/process/coding-assistants.rst
>
> That was my oversight. I failed to include the appropriate
> coding-assistant attribution/disclosure, and I will reroll my patches
> accordingly.
That's a good step in the right direction.
>> I'm curious about where you are going with this in general
>
> I am not contributing as a kernel developer. My background is in
> front-end engineering, product/UX, and developer-facing documentation
> and platform surfaces, and that is where I believe I can add value here.
The reason I ask is that submissions to the kernel - even those for the
documentation - have to be evaluated with an eye toward ongoing
maintenance. A couple of lines of CSS tweak are easily accepted. 1,000
lines of uncommented Javascript, CSS, and Jinja -- none of which fall in
the core strengths of most kernel developers -- have to be looked at
more carefully.
Who is going to maintain this code going forward? How well do you truly
understand this code, which you did not write yourself? Will you be
there to help resolve problems that show up in six months or a year?
> Linux is important infrastructure, and I have already been making
> related contributions in its ecosystem. In trying to improve
> lore.kernel.org, I contributed patches merged upstream to Public Inbox
> for small-screen layout behavior and for enabling admin-injected meta
> tags in the document head.
You did get a few patches past Eric, that says something :)
> More broadly, I have worked on improving developer-facing surfaces,
> including documentation and related tooling, in other OSS projects.
> That is the kind of value I am aiming to add here: not direct kernel
> development, but improving usability, discoverability, and developer
> experience around important technical infrastructure.
Worthy goals, certainly.
For the moment I'll ask you to slow down a bit; there are real humans on
the receiving side of these patches who have to deal with them. I'll
get there shortly, but you're not the only thing in the queue. The
simple changes seem generally OK from a first quick glance.
I am far from convinced about this particular patch, though. Before I
accept code that will run in the browser of everybody who reads the
rendered kernel docs, I need to understand that code well, and the
current posting is not entirely amenable to that.
Thanks,
jon
^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [PATCH] docs: add advanced search for kernel documentation
2026-03-22 19:59 ` Randy Dunlap
@ 2026-03-23 22:50 ` Rito Rhymes
2026-03-23 23:01 ` Randy Dunlap
0 siblings, 1 reply; 14+ messages in thread
From: Rito Rhymes @ 2026-03-23 22:50 UTC (permalink / raw)
To: Randy Dunlap, Rito Rhymes, Jonathan Corbet, Mauro Carvalho Chehab,
linux-doc
Cc: Shuah Khan, linux-kernel
I believe I identified the issue as a Sphinx version compatibility
problem.
I've been working on a more robust reroll. I expect to have it by
tomorrow, and we can test it when you're ready.
Rito
^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [PATCH] docs: add advanced search for kernel documentation
2026-03-23 22:50 ` Rito Rhymes
@ 2026-03-23 23:01 ` Randy Dunlap
2026-03-28 20:55 ` Rito Rhymes
0 siblings, 1 reply; 14+ messages in thread
From: Randy Dunlap @ 2026-03-23 23:01 UTC (permalink / raw)
To: Rito Rhymes, Jonathan Corbet, Mauro Carvalho Chehab, linux-doc
Cc: Shuah Khan, linux-kernel
On 3/23/26 3:50 PM, Rito Rhymes wrote:
> I believe I identified the issue as a Sphinx version compatibility
> problem.
>
> I've been working on a more robust reroll. I expect to have it by
> tomorrow, and we can test it when you're ready.
Sounds good. Thanks.
--
~Randy
^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [PATCH] docs: add advanced search for kernel documentation
2026-03-23 23:01 ` Randy Dunlap
@ 2026-03-28 20:55 ` Rito Rhymes
0 siblings, 0 replies; 14+ messages in thread
From: Rito Rhymes @ 2026-03-28 20:55 UTC (permalink / raw)
To: Randy Dunlap, Rito Rhymes, Jonathan Corbet, Mauro Carvalho Chehab,
linux-doc
Cc: Shuah Khan, linux-kernel
I'm still working on the advanced search reroll. It's turned into a more
substantial update than I first expected.
Rito
^ permalink raw reply [flat|nested] 14+ messages in thread
end of thread, other threads:[~2026-03-28 20:58 UTC | newest]
Thread overview: 14+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-03-21 18:15 [PATCH] docs: add advanced search for kernel documentation Rito Rhymes
2026-03-21 22:53 ` Randy Dunlap
2026-03-21 23:15 ` Rito Rhymes
2026-03-21 23:59 ` Randy Dunlap
2026-03-22 1:12 ` Rito Rhymes
2026-03-22 19:59 ` Randy Dunlap
2026-03-23 22:50 ` Rito Rhymes
2026-03-23 23:01 ` Randy Dunlap
2026-03-28 20:55 ` Rito Rhymes
2026-03-22 16:01 ` Jonathan Corbet
2026-03-22 17:08 ` Rito Rhymes
2026-03-22 17:25 ` Rito Rhymes
2026-03-22 20:25 ` Jonathan Corbet
2026-03-22 18:17 ` [PATCH v2] " Rito Rhymes
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox