This is an automated email from the ASF dual-hosted git repository.

potiuk pushed a commit to branch publish-airflow-docs-latest-layout-2025-12
in repository https://gitbox.apache.org/repos/asf/airflow.git

commit 4cfb5cf0be4004726a08948cd3e42c9f8901b20a
Author: Kaxil Naik <[email protected]>
AuthorDate: Sat Dec 20 13:01:09 2025 +0000

    Add fast client-side search to Airflow documentation (#59658)
    
    I have been frustrated by Sphinx search for a long-long time. So after 
adding dark-mode, this was next in my list!
    
    This PR/commit introduces a fast, fully client-side search experience for 
the Apache Airflow documentation, powered by [Pagefind](https://pagefind.app/). 
The new search is keyboard-accessible (Cmd+K / Ctrl+K), works offline, and 
requires no external services.
    
    Search indexes are generated automatically at documentation build time and 
loaded entirely in the browser, enabling sub-50 ms queries even on large docs.
    
    I have kept the Sphinx search too as a backup and it will keep functioning.
    
    ----
    
    Add keyboard-accessible search (Cmd+K) to Apache Airflow documentation with
    automatic indexing and offline support.
    
    New Sphinx extension: `pagefind_search`
    
    Located in `devel-common/src/sphinx_exts/pagefind_search/`:
    - __init__.py: Extension setup with configuration values and event handlers
    - builder.py: Automatic index building with graceful fallback
    - static/css/pagefind.css: Search modal and button styling with dark mode 
support
    - static/js/search.js: Search functionality with keyboard shortcuts
    - templates/search-modal.html: Search modal HTML template
    
    - Keyboard shortcut (Cmd+K/Ctrl+K) opens search modal
    - Arrow key navigation through results
    - Works offline (no external services)
    - Automatic indexing during documentation build
    - Dark mode support
    - Sub-50ms search performance
    - Configurable content indexing via conf.py
    
    Users can now:
    - Press Cmd+K from any documentation page to search
    - Navigate results with arrow keys, Enter to select, Esc to close
    - Search works immediately without network requests
    - Results show page title, breadcrumb, and excerpt
    
    Available in conf.py:
    - pagefind_enabled: Toggle search indexing
    - pagefind_verbose: Enable build logging
    - pagefind_root_selector: Define searchable content area
    - pagefind_exclude_selectors: Exclude navigation, headers, footers
    - pagefind_custom_records: Index non-HTML content (PDFs, etc.)
    
    (cherry picked from commit d0bd2df6d194a8b923fb55b2f37107642c261b40)
---
 Dockerfile                                         |   8 +-
 Dockerfile.ci                                      |   8 +-
 airflow-core/docs/conf.py                          |   8 +
 devel-common/pyproject.toml                        |   1 +
 devel-common/src/docs/utils/conf_constants.py      |   1 +
 .../src/sphinx_exts/pagefind_search/README.md      | 165 +++++++
 .../src/sphinx_exts/pagefind_search/__init__.py    | 103 ++++
 .../src/sphinx_exts/pagefind_search/builder.py     | 215 +++++++++
 .../pagefind_search/static/css/pagefind.css        | 529 +++++++++++++++++++++
 .../pagefind_search/static/js/search.js            | 228 +++++++++
 .../pagefind_search/templates/search-modal.html    |  48 ++
 .../pagefind_search/templates/searchbox.html       |  33 ++
 .../docker/install_airflow_when_building_images.sh |   8 +-
 13 files changed, 1349 insertions(+), 6 deletions(-)

diff --git a/Dockerfile b/Dockerfile
index 9b1f3afa64b..37e99edd016 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1235,9 +1235,13 @@ function install_airflow_when_building_images() {
     set +x
     common::install_packaging_tools
     echo
-    echo "${COLOR_BLUE}Running 'pip check'${COLOR_RESET}"
+    echo "${COLOR_BLUE}Running 'uv pip check'${COLOR_RESET}"
     echo
-    pip check
+    # Here we should use `pip check` not `uv pip check` to detect any 
incompatibilities that might happen
+    # between `pip` and `uv` installations
+    # However, in the current version of `pip` there is a bug that incorrectly 
detects `pagefind-bin` as unsupported
+    # https://github.com/pypa/pip/issues/13709 -> once this is fixed, we 
should bring `pip check` back.
+    uv pip check
 }
 
 common::get_colors
diff --git a/Dockerfile.ci b/Dockerfile.ci
index c4921b7df90..5d252318da4 100644
--- a/Dockerfile.ci
+++ b/Dockerfile.ci
@@ -989,9 +989,13 @@ function install_airflow_when_building_images() {
     set +x
     common::install_packaging_tools
     echo
-    echo "${COLOR_BLUE}Running 'pip check'${COLOR_RESET}"
+    echo "${COLOR_BLUE}Running 'uv pip check'${COLOR_RESET}"
     echo
-    pip check
+    # Here we should use `pip check` not `uv pip check` to detect any 
incompatibilities that might happen
+    # between `pip` and `uv` installations
+    # However, in the current version of `pip` there is a bug that incorrectly 
detects `pagefind-bin` as unsupported
+    # https://github.com/pypa/pip/issues/13709 -> once this is fixed, we 
should bring `pip check` back.
+    uv pip check
 }
 
 common::get_colors
diff --git a/airflow-core/docs/conf.py b/airflow-core/docs/conf.py
index 74c09b31c7a..4d505bf6654 100644
--- a/airflow-core/docs/conf.py
+++ b/airflow-core/docs/conf.py
@@ -268,6 +268,14 @@ global_substitutions = {
     "experimental": "This is an :ref:`experimental feature <experimental>`.",
 }
 
+# Pagefind search configuration
+pagefind_exclude_patterns = [
+    "_api/**",  # Exclude auto-generated API documentation
+    "_modules/**",  # Exclude source code modules
+    "release_notes.html",  # Exclude changelog aggregation page
+    "genindex.html",  # Exclude generated index
+]
+
 # -- Options for sphinx.ext.autodoc 
--------------------------------------------
 # See: https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html
 
diff --git a/devel-common/pyproject.toml b/devel-common/pyproject.toml
index ed8c483f4c4..e37be8e44f4 100644
--- a/devel-common/pyproject.toml
+++ b/devel-common/pyproject.toml
@@ -76,6 +76,7 @@ dependencies = [
     "rich-click>=1.7.1",
     "click>=8.1.8",
     "docutils>=0.21",
+    "pagefind[bin]",
     
"sphinx-airflow-theme@https://github.com/apache/airflow-site/releases/download/0.3.0/sphinx_airflow_theme-0.3.0-py3-none-any.whl";,
     "sphinx-argparse>=0.4.0",
     "sphinx-autoapi>=3",
diff --git a/devel-common/src/docs/utils/conf_constants.py 
b/devel-common/src/docs/utils/conf_constants.py
index 1b8285460e8..77b4426363c 100644
--- a/devel-common/src/docs/utils/conf_constants.py
+++ b/devel-common/src/docs/utils/conf_constants.py
@@ -91,6 +91,7 @@ BASIC_SPHINX_EXTENSIONS = [
     "redirects",
     "substitution_extensions",
     "sphinx_design",
+    "pagefind_search",
 ]
 
 SPHINX_REDOC_EXTENSIONS = [
diff --git a/devel-common/src/sphinx_exts/pagefind_search/README.md 
b/devel-common/src/sphinx_exts/pagefind_search/README.md
new file mode 100644
index 00000000000..b01e3c077ac
--- /dev/null
+++ b/devel-common/src/sphinx_exts/pagefind_search/README.md
@@ -0,0 +1,165 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements.  See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership.  The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License.  You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied.  See the License for the
+ specific language governing permissions and limitations
+ under the License.
+ -->
+
+# Pagefind Search Extension
+
+A Sphinx extension providing fast, self-hosted search for Apache Airflow 
documentation using [Pagefind](https://pagefind.app/).
+
+## Features
+
+- **Automatic indexing** when docs are built
+- **Cmd+K search** with modern UI and keyboard shortcuts
+- **Self-hosted** - No third-party services
+- **Content weighting** - Prioritizes titles and headings for better relevance
+- **Optimized ranking** - Tuned for exact phrase matching and title matches
+- **Playground support** - Debug and tune search behavior
+
+## Usage
+
+### Search Interface
+
+- **Keyboard**: Press `Cmd+K` (Mac) or `Ctrl+K` (Windows/Linux)
+- **Mouse**: Click the search button in the header
+- **Navigation**: Arrow keys to navigate, Enter to select, Esc to close
+
+## Configuration
+
+In Sphinx's `conf.py`:
+
+```python
+# Enable/disable search (default: True)
+pagefind_enabled = True
+
+# Verbose logging (default: False)
+pagefind_verbose = False
+
+# Content selector (default: "main")
+pagefind_root_selector = "main"
+
+# Exclude selectors (default: see below)
+# These elements won't be included in the search index
+pagefind_exclude_selectors = [
+    ".headerlink",  # Permalink icons
+    ".toctree-wrapper",  # Table of contents navigation
+    "nav",  # All navigation elements
+    "footer",  # Footer content
+    ".td-sidebar",  # Left sidebar
+    ".breadcrumb",  # Breadcrumb navigation
+    ".navbar",  # Top navigation bar
+    ".dropdown-menu",  # Dropdown menus (version selector, etc.)
+    ".docs-version-selector",  # Version selector widget
+    "[role='navigation']",  # ARIA navigation landmarks
+    ".d-print-none",  # Print-hidden elements (usually UI controls)
+    ".pagefind-search-button",  # Search button itself
+]
+
+# File pattern (default: "**/*.html")
+pagefind_glob = "**/*.html"
+
+# Exclude patterns (default: [])
+# Path patterns to exclude from indexing (e.g., auto-generated API docs)
+# Note: File-by-file indexing is used when patterns are specified (slightly 
slower but precise)
+# Pagefind does NOT automatically exclude underscore-prefixed directories
+pagefind_exclude_patterns = [
+    "_api/**",  # Exclude API documentation
+    "_modules/**",  # Exclude source code modules
+    "release_notes.html",  # Exclude specific files
+    "genindex.html",  # Exclude generated index
+]
+
+# Content weighting (default: True)
+# Uses lightweight regex to add data-pagefind-weight attributes to titles and 
headings
+pagefind_content_weighting = True
+
+# Enable playground (default: False)
+# Creates a playground at /_pagefind/playground/ for debugging search
+pagefind_enable_playground = False
+
+# Custom records for non-HTML content (default: [])
+pagefind_custom_records = [
+    {
+        "url": "/downloads/guide.pdf",
+        "content": "PDF content...",
+        "language": "en",
+        "meta": {"title": "Guide PDF"},
+    }
+]
+```
+
+### Ranking Optimization
+
+The extension uses optimized ranking parameters in `search.js`:
+
+- **termFrequency: 1.0** - Standard term occurrence weighting
+- **termSaturation: 0.7** - Moderate saturation to prevent over-rewarding 
repetition
+- **termSimilarity: 7.5** - Maximum boost for exact phrase matches and similar 
terms
+- **pageLength: 0** - No penalty for longer pages (important for reference 
documentation)
+
+Combined with content weighting (10x for titles, 9x for h1, 7x for h2), these 
settings ensure exact title matches rank highly even for very long pages.
+
+## Architecture
+
+### Build Process
+
+1. Sphinx builds HTML documentation
+2. Extension copies CSS/JS to `_static/`
+3. Extension injects search modal HTML into pages
+4. (Optional) Extension adds content weights to HTML files
+5. Extension builds Pagefind index with configured options
+
+### Runtime
+
+1. User presses Cmd+K or clicks search button
+2. JavaScript loads Pagefind library dynamically
+3. Search executes client-side
+4. Results rendered in modal
+
+## Troubleshooting
+
+### Search not working
+
+1. Check Pagefind is installed: `python -c "import pagefind; print('OK')"`
+2. Check index exists: `ls 
generated/_build/docs/apache-airflow/stable/_pagefind/`
+3. Enable verbose logging: `pagefind_verbose = True` in `conf.py`
+
+### Index not created
+
+- Ensure `pagefind_enabled = True` in `conf.py`
+- Check build logs for errors
+- If `pagefind[bin]` unavailable, use: `npx pagefind --site <build-dir>`
+
+### Poor search ranking
+
+1. Enable playground: `pagefind_enable_playground = True` in `conf.py`
+2. Rebuild docs and access playground at `/_pagefind/playground/`
+3. Test queries and analyze ranking scores
+4. Ensure `pagefind_content_weighting = True` (default)
+5. Check that titles and headings contain expected keywords
+
+### Debugging with Playground
+
+The playground provides detailed insights:
+
+- View all indexed pages
+- See ranking scores for each result
+- Analyze impact of different search terms
+- Verify content weighting is applied
+- Test ranking parameter changes
+
+Example, access at: 
`http://localhost:8000/docs/apache-airflow/stable/_pagefind/playground/`
diff --git a/devel-common/src/sphinx_exts/pagefind_search/__init__.py 
b/devel-common/src/sphinx_exts/pagefind_search/__init__.py
new file mode 100644
index 00000000000..acfe12ea1d8
--- /dev/null
+++ b/devel-common/src/sphinx_exts/pagefind_search/__init__.py
@@ -0,0 +1,103 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Sphinx extension for Pagefind search integration."""
+
+from __future__ import annotations
+
+from pathlib import Path
+from typing import TYPE_CHECKING, Any
+
+if TYPE_CHECKING:
+    from sphinx.application import Sphinx
+
+from sphinx_exts.pagefind_search.builder import build_index_finished, 
copy_static_files
+
+
+def register_templates(app: Sphinx, config) -> None:
+    """Register template directory for searchbox override."""
+    template_dir = str(Path(__file__).parent / "templates")
+    # Prepend so our template overrides Sphinx's default searchbox
+    if template_dir not in config.templates_path:
+        config.templates_path.insert(0, template_dir)
+
+
+def inject_search_html(app: Sphinx, pagename: str, templatename: str, context: 
dict, doctree) -> None:
+    """Inject Pagefind search modal HTML into page context."""
+    template_dir = Path(__file__).parent / "templates"
+    modal_file = template_dir / "search-modal.html"
+
+    if not modal_file.exists():
+        return
+
+    modal_content = modal_file.read_text()
+
+    from jinja2 import Template
+
+    modal_template = Template(modal_content)
+    search_modal_html = modal_template.render(pathto=context.get("pathto"))
+
+    context["pagefind_search_modal"] = search_modal_html
+
+    if "body" in context:
+        context["body"] = search_modal_html + context["body"]
+
+
+def setup(app: Sphinx) -> dict[str, Any]:
+    """Setup the Pagefind search extension."""
+    app.add_config_value("pagefind_enabled", True, "html", [bool])
+    app.add_config_value("pagefind_verbose", False, "html", [bool])
+    app.add_config_value("pagefind_root_selector", "main", "html", [str])
+    app.add_config_value(
+        "pagefind_exclude_selectors",
+        [
+            ".headerlink",
+            ".toctree-wrapper",
+            "nav",
+            "footer",
+            ".td-sidebar",
+            ".breadcrumb",
+            ".navbar",
+            ".dropdown-menu",
+            ".docs-version-selector",
+            "[role='navigation']",
+            ".d-print-none",
+            ".pagefind-search-button",
+        ],
+        "html",
+        [list],
+    )
+    app.add_config_value("pagefind_glob", "**/*.html", "html", [str])
+    app.add_config_value("pagefind_exclude_patterns", [], "html", [list])
+    app.add_config_value("pagefind_custom_records", [], "html", [list])
+    app.add_config_value("pagefind_content_weighting", True, "html", [bool])
+    app.add_config_value("pagefind_enable_playground", False, "html", [bool])
+
+    app.add_css_file("css/pagefind.css")
+    app.add_js_file("js/search.js")
+
+    # Register template directory for searchbox override
+    app.connect("config-inited", register_templates)
+    app.connect("html-page-context", inject_search_html)
+    app.connect("build-finished", copy_static_files, priority=100)
+    app.connect("build-finished", build_index_finished, priority=900)
+
+    return {
+        "version": "1.0.0",
+        "parallel_read_safe": True,
+        "parallel_write_safe": False,
+    }
diff --git a/devel-common/src/sphinx_exts/pagefind_search/builder.py 
b/devel-common/src/sphinx_exts/pagefind_search/builder.py
new file mode 100644
index 00000000000..066123bd980
--- /dev/null
+++ b/devel-common/src/sphinx_exts/pagefind_search/builder.py
@@ -0,0 +1,215 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Pagefind index builder and static file handler."""
+
+from __future__ import annotations
+
+import asyncio
+import logging
+import re
+from pathlib import Path
+from typing import TYPE_CHECKING
+
+from pagefind.index import IndexConfig, PagefindIndex
+from sphinx.util.fileutil import copy_asset
+
+if TYPE_CHECKING:
+    from sphinx.application import Sphinx
+
+logger = logging.getLogger(__name__)
+
+
+def add_content_weights_lightweight(
+    output_dir: Path, glob_pattern: str, exclude_patterns: list[str] | None = 
None
+) -> int:
+    """Add data-pagefind-weight attributes using simple regex replacement.
+
+    :param output_dir: Output directory
+    :param glob_pattern: Glob pattern
+    :param exclude_patterns: Exclude patterns
+    :return: Number of files processed
+    """
+    files_processed = 0
+    exclude_patterns = exclude_patterns or []
+
+    # Regex patterns to match opening tags without existing weight attribute
+    # Use maximum valid weights (0.0-10.0 range, quadratic scale)
+    # https://pagefind.app/docs/weighting/
+    # Weight of 10.0 = ~100x impact, 7.0 = ~49x impact (default h1), 5.0 = 
~25x impact
+    patterns = [
+        (re.compile(r"<title(?![^>]*data-pagefind-weight)"), '<title 
data-pagefind-weight="10.0"'),
+        (re.compile(r"<h1(?![^>]*data-pagefind-weight)"), '<h1 
data-pagefind-weight="9.0"'),
+    ]
+
+    for html_file in output_dir.glob(glob_pattern):
+        if not html_file.is_file():
+            continue
+
+        # Check if file matches any exclude pattern (using simple prefix 
matching)
+        relative_path = html_file.relative_to(output_dir)
+        relative_str = str(relative_path)
+        if any(relative_str.startswith(pattern.rstrip("/*")) for pattern in 
exclude_patterns):
+            continue
+
+        try:
+            content = html_file.read_text(encoding="utf-8")
+            modified_content = content
+
+            for pattern, replacement in patterns:
+                modified_content = pattern.sub(replacement, modified_content)
+
+            if modified_content != content:
+                html_file.write_text(modified_content, encoding="utf-8")
+                files_processed += 1
+
+        except Exception as e:
+            logger.warning("Failed to add weights to %s: %s", html_file, e)
+
+    return files_processed
+
+
+async def build_pagefind_index(app: Sphinx) -> dict[str, int]:
+    """Build Pagefind search index using Python API."""
+    output_dir = Path(app.builder.outdir)
+    pagefind_dir = output_dir / "_pagefind"
+
+    # Add content weighting if enabled
+    if getattr(app.config, "pagefind_content_weighting", True):
+        logger.info("Adding content weights to HTML files...")
+        exclude_patterns = getattr(app.config, "pagefind_exclude_patterns", [])
+        files_processed = add_content_weights_lightweight(
+            output_dir, app.config.pagefind_glob, exclude_patterns
+        )
+        logger.info("Added content weights to %s files", files_processed)
+
+    config = IndexConfig(
+        root_selector=app.config.pagefind_root_selector,
+        exclude_selectors=app.config.pagefind_exclude_selectors,
+        output_path=str(pagefind_dir),
+        verbose=app.config.pagefind_verbose,
+        force_language=app.config.language or "en",
+        keep_index_url=False,
+        write_playground=getattr(app.config, "pagefind_enable_playground", 
False),
+    )
+
+    logger.info("Building Pagefind search index...")
+
+    exclude_patterns = getattr(app.config, "pagefind_exclude_patterns", [])
+
+    if exclude_patterns:
+        # Need to index files individually to apply exclusion patterns
+        logger.info("Indexing with exclusion patterns: %s", exclude_patterns)
+        indexed = 0
+        skipped = 0
+
+        async with PagefindIndex(config=config) as index:
+            for html_file in output_dir.glob(app.config.pagefind_glob):
+                if not html_file.is_file():
+                    continue
+
+                relative_path = html_file.relative_to(output_dir)
+                relative_str = str(relative_path)
+
+                # Check if path matches any exclude pattern (prefix matching)
+                if any(relative_str.startswith(pattern.rstrip("/*")) for 
pattern in exclude_patterns):
+                    skipped += 1
+                    continue
+
+                try:
+                    content = html_file.read_text(encoding="utf-8")
+                    await index.add_html_file(
+                        content=content,
+                        source_path=str(html_file),
+                        url=str(relative_path),
+                    )
+                    indexed += 1
+                except Exception as e:
+                    logger.warning("Failed to index %s: %s", relative_path, e)
+
+            logger.info("Pagefind indexed %s pages (excluded %s)", indexed, 
skipped)
+
+            if app.config.pagefind_custom_records:
+                for record in app.config.pagefind_custom_records:
+                    try:
+                        await index.add_custom_record(**record)
+                    except Exception as e:
+                        logger.warning("Failed to add custom record: %s", e)
+
+            return {"page_count": indexed}
+    else:
+        # No exclusions - use fast directory indexing
+        async with PagefindIndex(config=config) as index:
+            result = await index.add_directory(path=str(output_dir), 
glob=app.config.pagefind_glob)
+            page_count = result.get("page_count", 0)
+            logger.info("Pagefind indexed %s pages", page_count)
+
+            if app.config.pagefind_custom_records:
+                for record in app.config.pagefind_custom_records:
+                    try:
+                        await index.add_custom_record(**record)
+                    except Exception as e:
+                        logger.warning("Failed to add custom record: %s", e)
+
+            return {"page_count": page_count}
+
+
+def build_index_finished(app: Sphinx, exception: Exception | None) -> None:
+    """Build Pagefind index after HTML build completes."""
+    if exception:
+        logger.info("Skipping Pagefind indexing due to build errors")
+        return
+
+    if app.builder.format != "html":
+        return
+
+    if not app.config.pagefind_enabled:
+        logger.info("Pagefind indexing disabled (pagefind_enabled=False)")
+        return
+
+    try:
+        result = asyncio.run(build_pagefind_index(app))
+        page_count = result.get("page_count", 0)
+
+        if page_count == 0:
+            raise RuntimeError("Pagefind indexing failed: no pages were 
indexed")
+
+        logger.info("✓ Pagefind index created with %s pages", page_count)
+    except Exception as e:
+        logger.exception("Failed to build Pagefind index")
+        raise RuntimeError(f"Pagefind indexing failed: {e}") from e
+
+
+def copy_static_files(app: Sphinx, exception: Exception | None) -> None:
+    """Copy CSS and JS files to _static directory."""
+    if exception or app.builder.format != "html":
+        return
+
+    static_dir = Path(app.builder.outdir) / "_static"
+    extension_static = Path(__file__).parent / "static"
+
+    css_src = extension_static / "css" / "pagefind.css"
+    css_dest = static_dir / "css"
+    css_dest.mkdir(parents=True, exist_ok=True)
+    if css_src.exists():
+        copy_asset(str(css_src), str(css_dest))
+
+    js_src = extension_static / "js" / "search.js"
+    js_dest = static_dir / "js"
+    js_dest.mkdir(parents=True, exist_ok=True)
+    if js_src.exists():
+        copy_asset(str(js_src), str(js_dest))
diff --git 
a/devel-common/src/sphinx_exts/pagefind_search/static/css/pagefind.css 
b/devel-common/src/sphinx_exts/pagefind_search/static/css/pagefind.css
new file mode 100644
index 00000000000..aa72e9440fb
--- /dev/null
+++ b/devel-common/src/sphinx_exts/pagefind_search/static/css/pagefind.css
@@ -0,0 +1,529 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/*
+ * Pagefind Search Modal - Cmd+K Search
+ * Minimal styling for search functionality only
+ */
+
+/* Search Modal */
+.search-modal {
+  position: fixed;
+  inset: 0;
+  z-index: 9999;
+  display: none;
+  align-items: flex-start;
+  padding-top: 15vh;
+}
+
+.search-modal.active {
+  display: flex;
+}
+
+.search-modal__backdrop {
+  position: absolute;
+  inset: 0;
+  background: rgba(0, 0, 0, 0.6);
+  backdrop-filter: blur(4px);
+  cursor: pointer;
+}
+
+.search-modal__container {
+  position: relative;
+  width: 90%;
+  max-width: 600px;
+  margin: 0 auto;
+  background: white;
+  border-radius: 8px;
+  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
+  overflow: hidden;
+  border: 1px solid #dee2e6;
+}
+
+[data-bs-theme="dark"] .search-modal__container {
+  background: #161b22;
+  border-color: #30363d;
+}
+
+.search-modal__header {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  padding: 16px 20px;
+  border-bottom: 1px solid #dee2e6;
+}
+
+[data-bs-theme="dark"] .search-modal__header {
+  border-bottom-color: #30363d;
+}
+
+.search-modal__icon {
+  color: #6c757d;
+  flex-shrink: 0;
+  width: 18px;
+  height: 18px;
+}
+
+.search-modal__input {
+  flex: 1;
+  font-size: 16px;
+  border: none;
+  background: transparent;
+  color: #1a1a1a;
+  font-family: inherit;
+}
+
+[data-bs-theme="dark"] .search-modal__input {
+  color: #e6edf3;
+}
+
+.search-modal__input:focus {
+  outline: none;
+}
+
+.search-modal__input::placeholder {
+  color: #adb5bd;
+}
+
+[data-bs-theme="dark"] .search-modal__input::placeholder {
+  color: #6e7681;
+}
+
+.search-modal__close {
+  background: transparent;
+  border: none;
+  color: #6c757d;
+  cursor: pointer;
+  padding: 6px;
+  border-radius: 4px;
+  transition: background 0.2s;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 28px;
+  height: 28px;
+}
+
+.search-modal__close:hover {
+  background: #f8f9fa;
+}
+
+[data-bs-theme="dark"] .search-modal__close {
+  color: #8b949e;
+}
+
+[data-bs-theme="dark"] .search-modal__close:hover {
+  background: #21262d;
+}
+
+.search-modal__close svg {
+  width: 16px;
+  height: 16px;
+}
+
+.search-modal__results {
+  max-height: 400px;
+  overflow-y: auto;
+  padding: 8px;
+}
+
+.search-modal__result-item {
+  padding: 10px 12px;
+  border-radius: 4px;
+  cursor: pointer;
+  display: flex;
+  gap: 10px;
+  text-decoration: none;
+  color: inherit;
+  transition: background 0.2s;
+  margin-bottom: 4px;
+}
+
+.search-modal__result-item:hover,
+.search-modal__result-item--selected {
+  background: #f8f9fa;
+}
+
+[data-bs-theme="dark"] .search-modal__result-item:hover,
+[data-bs-theme="dark"] .search-modal__result-item--selected {
+  background: #21262d;
+}
+
+.search-modal__result-icon {
+  color: #6c757d;
+  flex-shrink: 0;
+  margin-top: 2px;
+  width: 14px;
+  height: 14px;
+}
+
+.search-modal__result-icon svg {
+  width: 14px;
+  height: 14px;
+}
+
+.search-modal__result-content {
+  flex: 1;
+  min-width: 0;
+}
+
+.search-modal__result-title {
+  font-weight: 600;
+  color: #1a1a1a;
+  margin-bottom: 2px;
+  font-size: 14px;
+}
+
+[data-bs-theme="dark"] .search-modal__result-title {
+  color: #e6edf3;
+}
+
+.search-modal__result-breadcrumb {
+  font-size: 11px;
+  color: #6c757d;
+  margin-bottom: 4px;
+}
+
+[data-bs-theme="dark"] .search-modal__result-breadcrumb {
+  color: #8b949e;
+}
+
+.search-modal__result-excerpt {
+  font-size: 12px;
+  color: #6c757d;
+  line-height: 1.5;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  display: -webkit-box;
+  -webkit-line-clamp: 2;
+  -webkit-box-orient: vertical;
+}
+
+[data-bs-theme="dark"] .search-modal__result-excerpt {
+  color: #8b949e;
+}
+
+.search-modal__no-results {
+  text-align: center;
+  padding: 48px 24px;
+  color: #6c757d;
+}
+
+[data-bs-theme="dark"] .search-modal__no-results {
+  color: #8b949e;
+}
+
+.search-modal__no-results p:first-child {
+  font-size: 15px;
+  font-weight: 600;
+  color: #1a1a1a;
+  margin-bottom: 8px;
+}
+
+[data-bs-theme="dark"] .search-modal__no-results p:first-child {
+  color: #e6edf3;
+}
+
+.search-modal__no-results-hint {
+  font-size: 13px;
+  color: #adb5bd;
+}
+
+[data-bs-theme="dark"] .search-modal__no-results-hint {
+  color: #6e7681;
+}
+
+.search-modal__footer {
+  padding: 10px 16px;
+  border-top: 1px solid #dee2e6;
+  background: #f8f9fa;
+}
+
+[data-bs-theme="dark"] .search-modal__footer {
+  border-top-color: #30363d;
+  background: #0d1117;
+}
+
+.search-modal__shortcuts {
+  display: flex;
+  gap: 16px;
+  justify-content: center;
+  font-size: 12px;
+  color: #495057;
+  font-weight: 500;
+}
+
+[data-bs-theme="dark"] .search-modal__shortcuts {
+  color: #c9d1d9;
+}
+
+.search-modal__shortcuts span {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+}
+
+.search-modal__shortcuts kbd {
+  padding: 3px 8px;
+  background: white;
+  border: 1px solid #dee2e6;
+  border-radius: 4px;
+  font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', monospace;
+  margin: 0;
+  font-size: 11px;
+  font-weight: 600;
+  color: #495057;
+  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
+}
+
+[data-bs-theme="dark"] .search-modal__shortcuts kbd {
+  background: #21262d;
+  border-color: #30363d;
+  color: #c9d1d9;
+  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
+}
+
+/* ============================================
+   Search Button in Header - FIXED POSITIONING
+   ============================================ */
+
+.search-button {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 6px 12px;
+  border: 1px solid rgba(255, 255, 255, 0.2);
+  border-radius: 6px;
+  background: rgba(255, 255, 255, 0.1);
+  color: #dee2e6;
+  cursor: pointer;
+  transition: all 0.2s ease;
+  font-size: 14px;
+  font-family: inherit;
+  height: 36px;
+  margin-left: 16px;
+}
+
+.search-button:hover {
+  background: rgba(255, 255, 255, 0.15);
+  border-color: rgba(255, 255, 255, 0.3);
+  color: white;
+}
+
+.search-button__icon {
+  flex-shrink: 0;
+  width: 16px;
+  height: 16px;
+}
+
+.search-button__text {
+  font-weight: 400;
+  font-size: 14px;
+  color: inherit;
+}
+
+.search-button__shortcut {
+  display: flex;
+  gap: 2px;
+  margin-left: 8px;
+  padding: 2px 6px;
+  background: rgba(255, 255, 255, 0.15);
+  border: 1px solid rgba(255, 255, 255, 0.3);
+  border-radius: 4px;
+}
+
+.search-button__shortcut span {
+  font-size: 11px;
+  font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', monospace;
+  font-weight: 600;
+  line-height: 1.2;
+  color: rgba(255, 255, 255, 0.9);
+}
+
+/* Light theme adjustments */
+[data-bs-theme="light"] .search-button {
+  border-color: #dee2e6;
+  background: white;
+  color: #495057;
+}
+
+[data-bs-theme="light"] .search-button:hover {
+  background: #f8f9fa;
+  border-color: #adb5bd;
+  color: #212529;
+}
+
+[data-bs-theme="light"] .search-button__shortcut {
+  background: #e9ecef;
+  border-color: #adb5bd;
+}
+
+[data-bs-theme="light"] .search-button__shortcut span {
+  color: #495057;
+  font-weight: 600;
+}
+
+@media (max-width: 768px) {
+  .search-button__text,
+  .search-button__shortcut {
+    display: none;
+  }
+
+  .search-button {
+    padding: 6px;
+    width: 36px;
+    justify-content: center;
+    margin-left: 8px;
+  }
+}
+
+/* Floating Search Button (Fallback) */
+.pagefind-search-button {
+  position: fixed;
+  top: 20px;
+  right: 20px;
+  z-index: 9998;
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 8px 14px;
+  background: #017cee;
+  color: white;
+  border: 1px solid rgba(1, 124, 238, 0.2);
+  border-radius: 6px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+  cursor: pointer;
+  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, 
"Helvetica Neue", Arial, sans-serif;
+  font-size: 14px;
+  font-weight: 500;
+  transition: all 0.2s ease;
+}
+
+.pagefind-search-button:hover {
+  background: #0166c7;
+  box-shadow: 0 4px 12px rgba(1, 124, 238, 0.25);
+  transform: translateY(-1px);
+}
+
+.pagefind-search-button__icon {
+  flex-shrink: 0;
+  width: 18px;
+  height: 18px;
+}
+
+.pagefind-search-button__text {
+  color: white;
+  font-weight: 500;
+}
+
+.pagefind-search-button__shortcut {
+  display: flex;
+  gap: 2px;
+  padding: 3px 7px;
+  background: rgba(255, 255, 255, 0.2);
+  border: 1px solid rgba(255, 255, 255, 0.3);
+  border-radius: 4px;
+  font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', monospace;
+  font-size: 11px;
+  font-weight: 600;
+  line-height: 1;
+}
+
+.pagefind-search-button__shortcut span {
+  color: white;
+}
+
+@media (max-width: 768px) {
+  .pagefind-search-button__text,
+  .pagefind-search-button__shortcut {
+    display: none;
+  }
+
+  .pagefind-search-button {
+    width: 44px;
+    height: 44px;
+    padding: 10px;
+    justify-content: center;
+  }
+}
+
+/* Sidebar Search Button - Replaces Sphinx searchbox */
+.sidebar-search-container {
+  padding: 0;
+  margin-bottom: 1rem;
+}
+
+.sidebar-search-button {
+  position: static;
+  width: 100%;
+  justify-content: center;
+  padding: 10px 12px;
+  border-radius: 6px;
+}
+
+.sidebar-search-button .pagefind-search-button__text {
+  flex: 1;
+}
+
+/* Light mode - subtle styling */
+[data-bs-theme="light"] .sidebar-search-button {
+  background: white;
+  border-color: #dee2e6;
+  color: #495057;
+  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
+}
+
+[data-bs-theme="light"] .sidebar-search-button:hover {
+  background: #f8f9fa;
+  border-color: #adb5bd;
+  color: #212529;
+}
+
+[data-bs-theme="light"] .sidebar-search-button .pagefind-search-button__icon {
+  stroke: #6c757d;
+}
+
+[data-bs-theme="light"] .sidebar-search-button .pagefind-search-button__text {
+  color: #495057;
+}
+
+[data-bs-theme="light"] .sidebar-search-button 
.pagefind-search-button__shortcut {
+  background: #e9ecef;
+  border-color: #adb5bd;
+}
+
+[data-bs-theme="light"] .sidebar-search-button 
.pagefind-search-button__shortcut span {
+  color: #495057;
+}
+
+/* Dark mode - keep blue for visibility */
+[data-bs-theme="dark"] .sidebar-search-button {
+  background: #0166c7;
+}
+
+[data-bs-theme="dark"] .sidebar-search-button:hover {
+  background: #017cee;
+}
+
+/* Hide floating button when sidebar button exists */
+body:has(header .search-button) .pagefind-search-button,
+body:has(nav .search-button) .pagefind-search-button,
+body:has(.sidebar-search-button) 
.pagefind-search-button:not(.sidebar-search-button) {
+  display: none;
+}
diff --git a/devel-common/src/sphinx_exts/pagefind_search/static/js/search.js 
b/devel-common/src/sphinx_exts/pagefind_search/static/js/search.js
new file mode 100644
index 00000000000..4cffcd11c3a
--- /dev/null
+++ b/devel-common/src/sphinx_exts/pagefind_search/static/js/search.js
@@ -0,0 +1,228 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+let pagefind = null;
+let searchResults = [];
+let selectedIndex = 0;
+
+async function initPagefind() {
+  if (!pagefind) {
+    try {
+      const pagefindPath = window.PAGEFIND_PATH || './_pagefind/pagefind.js';
+      const absoluteUrl = new URL(pagefindPath, document.baseURI || 
window.location.href).href;
+
+      const pf = await import(/* webpackIgnore: true */ absoluteUrl);
+      pagefind = pf;
+      await pagefind.options({
+        excerptLength: 15,
+        ranking: {
+          termFrequency: 1.0,
+          termSaturation: 0.7,
+          termSimilarity: 7.5,  // Maximum boost for exact/similar matches
+          pageLength: 0         // No penalty for long pages
+        }
+      });
+    } catch (e) {
+      console.error('Failed to load Pagefind:', e);
+      displaySearchError('Search index not available. Please rebuild the 
documentation.');
+      return null;
+    }
+  }
+  return pagefind;
+}
+
+function displaySearchError(message) {
+  const resultsDiv = document.getElementById('search-results');
+  if (resultsDiv) {
+    resultsDiv.innerHTML = `
+      <div class="search-modal__no-results">
+        <p>${message}</p>
+      </div>
+    `;
+  }
+}
+
+function openSearch() {
+  const modal = document.getElementById('search-modal');
+  const input = document.getElementById('search-input');
+  if (modal && input) {
+    modal.style.display = 'flex';
+    input.focus();
+    document.body.style.overflow = 'hidden';
+  }
+}
+
+function closeSearch() {
+  const modal = document.getElementById('search-modal');
+  const input = document.getElementById('search-input');
+  const results = document.getElementById('search-results');
+
+  if (modal) modal.style.display = 'none';
+  if (input) input.value = '';
+  if (results) results.innerHTML = '';
+
+  document.body.style.overflow = '';
+  searchResults = [];
+  selectedIndex = 0;
+}
+
+function debounce(func, wait) {
+  let timeout;
+  return function executedFunction(...args) {
+    const later = () => {
+      clearTimeout(timeout);
+      func(...args);
+    };
+    clearTimeout(timeout);
+    timeout = setTimeout(later, wait);
+  };
+}
+
+async function performSearch(query) {
+  const resultsContainer = document.getElementById('search-results');
+  if (!resultsContainer) return;
+
+  if (!query || query.length < 2) {
+    resultsContainer.innerHTML = '';
+    return;
+  }
+
+  const pf = await initPagefind();
+  if (!pf) {
+    resultsContainer.innerHTML = `
+      <div class="search-modal__no-results">
+        <p>Search index not available</p>
+        <p class="search-modal__no-results-hint">Index is built automatically 
during 'make html'</p>
+      </div>
+    `;
+    return;
+  }
+
+  try {
+    const search = await pf.search(query);
+    searchResults = await Promise.all(
+      search.results.slice(0, 10).map(r => r.data())
+    );
+
+    renderResults(searchResults);
+  } catch (e) {
+    console.error('Search error:', e);
+    resultsContainer.innerHTML = `
+      <div class="search-modal__no-results">
+        <p>Search temporarily unavailable</p>
+        <p class="search-modal__no-results-hint">Please try again later</p>
+      </div>
+    `;
+  }
+}
+
+function renderResults(results) {
+  const container = document.getElementById('search-results');
+  if (!container) return;
+
+  if (results.length === 0) {
+    container.innerHTML = `
+      <div class="search-modal__no-results">
+        <p>No results found</p>
+        <p class="search-modal__no-results-hint">Try different keywords or 
check spelling</p>
+      </div>
+    `;
+    return;
+  }
+
+  container.innerHTML = results.map((result, index) => `
+    <a
+      href="${result.url}"
+      class="search-modal__result-item ${index === selectedIndex ? 
'search-modal__result-item--selected' : ''}"
+      data-index="${index}"
+      role="option"
+      aria-selected="${index === selectedIndex}"
+    >
+      <div class="search-modal__result-icon">
+        <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
+          <path d="M1 2.5A1.5 1.5 0 0 1 2.5 1h11A1.5 1.5 0 0 1 15 2.5v11a1.5 
1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 1 13.5v-11zM2.5 2a.5.5 0 0 0-.5.5v11a.5.5 0 
0 0 .5.5h11a.5.5 0 0 0 .5-.5v-11a.5.5 0 0 0-.5-.5h-11z"/>
+          <path d="M4 5.5a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 
1-.5-.5zm0 3a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5zm0 3a.5.5 0 
0 1 .5-.5h4a.5.5 0 0 1 0 1h-4a.5.5 0 0 1-.5-.5z"/>
+        </svg>
+      </div>
+      <div class="search-modal__result-content">
+        <div class="search-modal__result-title">${result.meta?.title || 
'Untitled'}</div>
+        <div 
class="search-modal__result-breadcrumb">${result.url.replace(/^\/docs\//, 
'').replace(/\.html$/, '')}</div>
+        ${result.excerpt ? `<div 
class="search-modal__result-excerpt">${result.excerpt}</div>` : ''}
+      </div>
+    </a>
+  `).join('');
+}
+
+function handleKeyboardNav(e) {
+  if (e.key === 'ArrowDown') {
+    e.preventDefault();
+    selectedIndex = Math.min(selectedIndex + 1, searchResults.length - 1);
+    renderResults(searchResults);
+    scrollToSelected();
+  } else if (e.key === 'ArrowUp') {
+    e.preventDefault();
+    selectedIndex = Math.max(selectedIndex - 1, 0);
+    renderResults(searchResults);
+    scrollToSelected();
+  } else if (e.key === 'Enter' && searchResults.length > 0) {
+    e.preventDefault();
+    window.location.href = searchResults[selectedIndex].url;
+  } else if (e.key === 'Escape') {
+    closeSearch();
+  }
+}
+
+function scrollToSelected() {
+  const selected = 
document.querySelector('.search-modal__result-item--selected');
+  if (selected) {
+    selected.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
+  }
+}
+
+document.addEventListener('DOMContentLoaded', () => {
+  document.addEventListener('keydown', (e) => {
+    if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
+      e.preventDefault();
+      openSearch();
+    }
+  });
+
+  const searchInput = document.getElementById('search-input');
+  if (searchInput) {
+    searchInput.addEventListener('input', debounce((e) => {
+      selectedIndex = 0;
+      performSearch(e.target.value);
+    }, 150));
+
+    searchInput.addEventListener('keydown', handleKeyboardNav);
+  }
+
+  const backdrop = document.querySelector('.search-modal__backdrop');
+  if (backdrop) {
+    backdrop.addEventListener('click', closeSearch);
+  }
+
+  const closeButton = document.querySelector('.search-modal__close');
+  if (closeButton) {
+    closeButton.addEventListener('click', closeSearch);
+  }
+});
+
+window.openSearch = openSearch;
+window.closeSearch = closeSearch;
diff --git 
a/devel-common/src/sphinx_exts/pagefind_search/templates/search-modal.html 
b/devel-common/src/sphinx_exts/pagefind_search/templates/search-modal.html
new file mode 100644
index 00000000000..b53a1f26a81
--- /dev/null
+++ b/devel-common/src/sphinx_exts/pagefind_search/templates/search-modal.html
@@ -0,0 +1,48 @@
+{#
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements.  See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership.  The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License.  You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied.  See the License for the
+ specific language governing permissions and limitations
+ under the License.
+#}
+
+<script>
+window.PAGEFIND_PATH = '{{ pathto("", 1) }}_pagefind/pagefind.js';
+</script>
+
+<div id="search-modal" class="search-modal" role="dialog" aria-modal="true" 
aria-label="Search documentation">
+    <div class="search-modal__backdrop"></div>
+    <div class="search-modal__container">
+        <div class="search-modal__header">
+            <svg class="search-modal__icon" width="20" height="20" viewBox="0 
0 20 20" fill="none" stroke="currentColor" stroke-width="2" 
stroke-linecap="round" stroke-linejoin="round">
+                <circle cx="9" cy="9" r="7"></circle>
+                <line x1="14" y1="14" x2="19" y2="19"></line>
+            </svg>
+            <input type="text" id="search-input" class="search-modal__input" 
placeholder="Search documentation..." aria-label="Search documentation input">
+            <button class="search-modal__close" onclick="closeSearch()" 
aria-label="Close search">
+                <svg width="16" height="16" viewBox="0 0 16 16" 
fill="currentColor">
+                    <path d="M14.354 1.646a.5.5 0 0 0-.708 0L8 7.293 2.354 
1.646a.5.5 0 0 0-.708.708L7.293 8l-5.647 5.646a.5.5 0 0 0 .708.708L8 
8.707l5.646 5.647a.5.5 0 0 0 .708-.708L8.707 8l5.647-5.646a.5.5 0 0 0 0-.708z"/>
+                </svg>
+            </button>
+        </div>
+        <div id="search-results" class="search-modal__results" role="listbox" 
aria-live="polite"></div>
+        <div class="search-modal__footer">
+            <div class="search-modal__shortcuts">
+                <span><kbd>↑↓</kbd> Navigate</span>
+                <span><kbd>⏎</kbd> Select</span>
+                <span><kbd>Esc</kbd> Close</span>
+            </div>
+        </div>
+    </div>
+</div>
diff --git 
a/devel-common/src/sphinx_exts/pagefind_search/templates/searchbox.html 
b/devel-common/src/sphinx_exts/pagefind_search/templates/searchbox.html
new file mode 100644
index 00000000000..0c3ec660b09
--- /dev/null
+++ b/devel-common/src/sphinx_exts/pagefind_search/templates/searchbox.html
@@ -0,0 +1,33 @@
+{#
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements.  See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership.  The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License.  You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied.  See the License for the
+ specific language governing permissions and limitations
+ under the License.
+#}
+
+{# Override default Sphinx searchbox with Pagefind button #}
+<div class="sidebar-search-container">
+    <button class="pagefind-search-button sidebar-search-button" 
onclick="openSearch()" aria-label="Search documentation" title="Search 
documentation (Cmd+K or Ctrl+K)">
+        <svg class="pagefind-search-button__icon" width="18" height="18" 
viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" 
stroke-linecap="round" stroke-linejoin="round">
+            <circle cx="9" cy="9" r="7"></circle>
+            <line x1="14" y1="14" x2="19" y2="19"></line>
+        </svg>
+        <span class="pagefind-search-button__text">Search docs</span>
+        <kbd class="pagefind-search-button__shortcut">
+            <span>⌘</span>
+            <span>K</span>
+        </kbd>
+    </button>
+</div>
diff --git a/scripts/docker/install_airflow_when_building_images.sh 
b/scripts/docker/install_airflow_when_building_images.sh
index c93c0387a63..fb7c1d86db1 100644
--- a/scripts/docker/install_airflow_when_building_images.sh
+++ b/scripts/docker/install_airflow_when_building_images.sh
@@ -200,9 +200,13 @@ function install_airflow_when_building_images() {
     set +x
     common::install_packaging_tools
     echo
-    echo "${COLOR_BLUE}Running 'pip check'${COLOR_RESET}"
+    echo "${COLOR_BLUE}Running 'uv pip check'${COLOR_RESET}"
     echo
-    pip check
+    # Here we should use `pip check` not `uv pip check` to detect any 
incompatibilities that might happen
+    # between `pip` and `uv` installations
+    # However, in the current version of `pip` there is a bug that incorrectly 
detects `pagefind-bin` as unsupported
+    # https://github.com/pypa/pip/issues/13709 -> once this is fixed, we 
should bring `pip check` back.
+    uv pip check
 }
 
 common::get_colors

Reply via email to