Skip to content

Commit

Permalink
feat: add static search (#487)
Browse files Browse the repository at this point in the history
Co-authored-by: pyansys-ci-bot <[email protected]>
Co-authored-by: Jorge Martínez <[email protected]>
  • Loading branch information
3 people authored Sep 12, 2024
1 parent bd04fb7 commit 54b211c
Show file tree
Hide file tree
Showing 12 changed files with 466 additions and 403 deletions.
1 change: 1 addition & 0 deletions doc/changelog.d/487.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
feat: add static search
11 changes: 4 additions & 7 deletions doc/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
ansys_favicon,
ansys_logo_white,
ansys_logo_white_cropped,
convert_version_to_pymeilisearch,
generate_404,
get_version_match,
latex,
Expand Down Expand Up @@ -60,12 +59,6 @@
"json_url": f"https://{cname}/versions.json",
"version_match": get_version_match(__version__),
},
"use_meilisearch": {
"api_key": os.getenv("MEILISEARCH_PUBLIC_API_KEY", ""),
"index_uids": {
f"ansys-sphinx-theme-v{convert_version_to_pymeilisearch(__version__)}": "ansys-sphinx-theme", # noqa: E501
},
},
"ansys_sphinx_theme_autoapi": {
"project": project,
"directory": "src/ansys_sphinx_theme/examples",
Expand All @@ -74,6 +67,10 @@
"package_depth": 1,
},
"logo": "ansys",
"static_search": {
"threshold": 0.5,
"ignoreLocation": True,
},
}


Expand Down
91 changes: 25 additions & 66 deletions doc/source/user-guide/options.rst
Original file line number Diff line number Diff line change
Expand Up @@ -108,92 +108,51 @@ If you want to hide all icons, use the ``show_icons`` Boolean variable:
...
}
Use MeiliSearch
----------------

MeiliSearch is an open source search engine that allows developers to
easily integrate search functionality into their applications.

To use MeiliSearch in your documentation, in the ``conf.py`` file,
a child dictionary named ``use_meilisearch``is added to the ``html_theme_options``
dictionary.

This dictionary contains these keys, in the order given:

#. ``host``: Host name of your MeiliSearch instance. If no value is provided,
the default public host for PyAnsys is used: ``https://backend.search.pyansys.com``
on port ``7700``. If added security is needed, you can use the ``os.getenv()`` function
to set the instance using an environment variable.
Static search options
----------------------

#. ``api_key``: API key for your MeiliSearch instance. If no value is provided,
the default public API key for PyAnsys is used. If added security is needed,
you can use the ``os.getenv()`` function to set the key using an environment
variable.
The Ansys Sphinx theme supports static search options to customize the search experience.

#. ``index_uids``: Dictionary that provides the mapping between the unique
identifier (UID) of an index and its corresponding user-friendly name.
Each key-value pair in the dictionary represents an index, with the key
being the index UID and the value being the index name. The index UID
points to an index on the server.
The static search bar is created using ``Fuse.js``. You can provide all options supported by ``Fuse.js`` through the ``static_search`` dictionary in the ``html_theme_options``.

Here is an example of how to configure MeiliSearch for use in the ``conf.py`` file:
Additional options include:

.. code-block:: python
import os
1. ``keys``: List of keys to search in the documents. Default are ``["title", "text"]``.
2. ``threshold``: The minimum score a search result must have to be included in the results. Default is ``0.5``.
3. ``ignoreLocation``: Whether to ignore the location of the search term in the document. Default is ``False``. Ignoring the location can increase the search speed for large documents.
4. ``limit``: The maximum number of search results to display. Default is ``10``.
5. ``min_chars_for_search``: The minimum number of characters required to start the search. Default is ``1``.

use_meilisearch = {
"host": os.getenv("MEILISEARCH_HOST_NAME", ""),
"api_key": os.getenv("MEILISEARCH_API_KEY", ""),
"index_uids": {
"index-uid of current project": "index name to display",
"another-index-uid": "index name to display",
},
}
.. note::

All other options are available in the `Fuse.js documentation <https://fusejs.io/api/options.html>`_.

If your project features multiple documentation versions, it's crucial to adapt the
``index_uids`` mapping to accommodate different versions. To ensure seamless search
integration across versions, use the following format to dynamically generate
version-specific index ``UIDs``:
Here is an example of how to add the ``static_search`` dictionary to the ``html_theme_options`` dictionary:

.. code-block:: python
from ansys_sphinx_theme import convert_version_to_pymeilisearch
use_meilisearch = {
"api_key": os.getenv("MEILISEARCH_PUBLIC_API_KEY", ""),
"index_uids": {
f"ansys-sphinx-theme-v{convert_version_to_pymeilisearch(__version__)}": "ansys-sphinx-theme",
html_theme_options = {
"static_search": {
"threshold": 0.5,
"limit": 10,
"min_chars_for_search": 1,
},
}
Here is an example configuration of how to configure MeiliSearch in the ``conf.py`` file
for the Ansys Sphinx Theme:

.. code-block:: python
.. note::

import os
Serve locally your documentation using the ``python -m http.server -d /path/to/docs/html/`` to have a live-preview of your search results. This method is compliant with the `CORS policy <https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS>`_ and allows to load the generated resource files containing the indices of your documentation.
The search bar does not work if you open the HTML files directly in the browser.

html_theme_options = {
"use_meilisearch": {
"index_uids": {
"ansys-sphinx-theme-sphinx-docs": "ansys-sphinx-theme",
"pyansys-docs-all-public": "PyAnsys",
},
},
}
To open the documentation in a local server, run the following command in the directory where the HTML files are located:

.. code-block:: bash
With these options set, MeiliSearch is available for performing searches of
your documentation.
python -m http.server
.. note::
Then, open the browser and go to ``http://localhost:8000``.

If you do not set the ``use_meilisearch`` dictionary, the
Ansys Sphinx Theme uses the default search functionality
inherited from the PyData Sphinx Theme.

Cheat sheets
------------
Expand Down
41 changes: 20 additions & 21 deletions src/ansys_sphinx_theme/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@

from ansys_sphinx_theme.extension.linkcode import DOMAIN_KEYS, sphinx_linkcode_resolve
from ansys_sphinx_theme.latex import generate_404 # noqa: F401
from ansys_sphinx_theme.search import create_search_index, update_search_config

try:
import importlib.metadata as importlib_metadata
Expand Down Expand Up @@ -125,27 +126,6 @@ def get_version_match(semver: str) -> str:
return ".".join([major, minor])


def convert_version_to_pymeilisearch(semver: str) -> str:
"""Convert a semantic version number to pymeilisearch-compatible format.
This function evaluates the given semantic version number and returns a
version number that is compatible with `pymeilisearch`, where dots are
replaced with hyphens.
Parameters
----------
semver : str
Semantic version number in the form of a string.
Returns
-------
str
pymeilisearch-compatible version number.
"""
version = get_version_match(semver).replace(".", "-")
return version


def setup_default_html_theme_options(app):
"""Set up the default configuration for the HTML options.
Expand Down Expand Up @@ -534,6 +514,21 @@ def build_quarto_cheatsheet(app: Sphinx):
app.config.html_theme_options["cheatsheet"]["thumbnail"] = f"{output_dir}/{output_png}"


def check_for_depreciated_theme_options(app: Sphinx):
"""Check for depreciated theme options.
Parameters
----------
app : sphinx.application.Sphinx
Application instance for rendering the documentation.
"""
theme_options = app.config.html_theme_options
if "use_meilisearch" in theme_options:
raise DeprecationWarning(
"The 'use_meilisearch' option is deprecated. Remove the option from your configuration file." # noqa: E501
)


def setup(app: Sphinx) -> Dict:
"""Connect to the Sphinx theme app.
Expand All @@ -556,6 +551,8 @@ def setup(app: Sphinx) -> Dict:
# Add default HTML configuration
setup_default_html_theme_options(app)

update_search_config(app)

# Verify that the main CSS file exists
if not CSS_PATH.exists():
raise FileNotFoundError(f"Unable to locate ansys-sphinx theme at {CSS_PATH.absolute()}")
Expand All @@ -567,10 +564,12 @@ def setup(app: Sphinx) -> Dict:
app.add_css_file("https://www.nerdfonts.com/assets/css/webfont.css")
app.connect("builder-inited", configure_theme_logo)
app.connect("builder-inited", build_quarto_cheatsheet)
app.connect("builder-inited", check_for_depreciated_theme_options)
app.connect("html-page-context", update_footer_theme)
app.connect("html-page-context", fix_edit_html_page_context)
app.connect("html-page-context", add_cheat_sheet)
app.connect("build-finished", replace_html_tag)
app.connect("build-finished", create_search_index)
return {
"version": __version__,
"parallel_read_safe": True,
Expand Down
48 changes: 48 additions & 0 deletions src/ansys_sphinx_theme/search/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Copyright (C) 2021 - 2024 ANSYS, Inc. and/or its affiliates.
# SPDX-License-Identifier: MIT
#
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""Initiate the search."""

from sphinx.application import Sphinx

from ansys_sphinx_theme.search.fuse_search import create_search_index


def update_search_config(app: Sphinx) -> None:
"""Update the search configuration.
Parameters
----------
app : Sphinx
Sphinx application.
"""
theme_static_options = app.config.html_theme_options.get("static_search", {})
theme_static_options["keys"] = ["title", "text"]
theme_static_options["threshold"] = theme_static_options.get("threshold", 0.5)
theme_static_options["shouldSort"] = theme_static_options.get("shouldSort", "True")
theme_static_options["ignoreLocation"] = theme_static_options.get("ignoreLocation", "False")
theme_static_options["useExtendedSearch"] = theme_static_options.get(
"useExtendedSearch", "True"
)
theme_static_options["limit"] = theme_static_options.get("limit", 10)
theme_static_options["min_chars_for_search"] = theme_static_options.get(
"min_chars_for_search", 1
)
113 changes: 113 additions & 0 deletions src/ansys_sphinx_theme/search/fuse_search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Copyright (C) 2021 - 2024 ANSYS, Inc. and/or its affiliates.
# SPDX-License-Identifier: MIT
#
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

"""Module to fuse search."""

import json
import re

from docutils import nodes


class SearchIndex:
"""Class to get search index."""

def __init__(self, doc_name, app):
"""Initialize the class.
Parameters
----------
doc_name : str
Document name.
app : Sphinx
Sphinx application.
"""
self._doc_name = doc_name
self.doc_path = f"{self._doc_name}.html"
self.doc_title = app.env.titles[self._doc_name].astext()
self._doc_tree = app.env.get_doctree(self._doc_name)
self.sections = []

def iterate_through_docs(self):
"""Iterate through the document."""
for node in self._doc_tree.traverse(nodes.section):
self.section_title = node[0].astext()
self.section_text = "\n".join(
n.astext()
for node_type in [nodes.paragraph, nodes.literal_block]
for n in node.traverse(node_type)
)
self.section_anchor_id = _title_to_anchor(self.section_title)
self.sections.append(
{
"section_title": self.section_title,
"section_text": self.section_text,
"section_anchor_id": self.section_anchor_id,
}
)

@property
def indices(self):
"""Get search index."""
for sections in self.sections:
search_index = {
"objectID": self._doc_name,
"href": f"{self.doc_path}#{sections['section_anchor_id']}",
"title": self.doc_title,
"section": sections["section_title"],
"text": sections["section_text"],
}
yield search_index


def _title_to_anchor(title: str) -> str:
"""Convert title to anchor."""
return re.sub(r"[^\w\s-]", "", title.lower().strip().replace(" ", "-"))


def create_search_index(app, exception):
"""Create search index for the document in build finished.
Parameters
----------
app : Sphinx
Sphinx application.
exception : Any
Exception.
"""
if exception:
return

if not app.config.html_theme_options.get("static_search", {}):
return

all_docs = app.env.found_docs
search_index_list = []

for document in all_docs:
search_index = SearchIndex(document, app)
search_index.iterate_through_docs()
search_index_list.extend(search_index.indices)

search_index = app.builder.outdir / "search.json"
with search_index.open("w", encoding="utf-8") as index_file:
json.dump(search_index_list, index_file, ensure_ascii=False, indent=4)
Loading

0 comments on commit 54b211c

Please sign in to comment.