Skip to content

Commit 9dc86df

Browse files
petyosiKludex
andauthored
Algolia search for docs (#775)
Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com>
1 parent 425b441 commit 9dc86df

File tree

8 files changed

+243
-0
lines changed

8 files changed

+243
-0
lines changed

.github/workflows/main.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ jobs:
5252
else
5353
uv run --no-sync mkdocs build
5454
fi
55+
env:
56+
ALGOLIA_WRITE_API_KEY: ${{ secrets.ALGOLIA_WRITE_API_KEY }}
5557
5658
test:
5759
name: test on Python ${{ matrix.python-version }} and pydantic ${{ matrix.pydantic-version }}

docs/extra/tweaks.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,7 @@ li.md-nav__item>a[href^="#logfire.configure("] {
7575
text-transform: none;
7676
font-size: 1.1em;
7777
}
78+
79+
.md-search__output em {
80+
color: var(--md-primary-fg-color);
81+
}

docs/javascripts/search-worker.js

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
importScripts('https://cdn.jsdelivr.net/npm/algoliasearch@5.18.0/dist/algoliasearch.umd.min.js')
2+
3+
const SETUP = 0
4+
const READY = 1
5+
const QUERY = 2
6+
const RESULT = 3
7+
8+
9+
const appID = 'KPPUDTIAVX';
10+
const apiKey = '1fc841595212a2c3afe8c24dd4cb8790';
11+
const indexName = 'logfire-docs';
12+
13+
const client = algoliasearch.algoliasearch(appID, apiKey);
14+
15+
self.onmessage = async (event) => {
16+
if (event.data.type === SETUP) {
17+
self.postMessage({ type: READY });
18+
} else if (event.data.type === QUERY) {
19+
20+
const query = event.data.data
21+
22+
if (query === '') {
23+
self.postMessage({
24+
type: RESULT, data: {
25+
items: []
26+
}
27+
});
28+
return
29+
}
30+
31+
const { results } = await client.search({
32+
requests: [
33+
{
34+
indexName,
35+
query,
36+
},
37+
],
38+
});
39+
40+
const hits = results[0].hits
41+
42+
// make navigation work with preview deployments
43+
const stripDocsPathName = !(new URL(self.location.href).pathname.startsWith('/docs'));
44+
45+
const mappedGroupedResults = hits.reduce((acc, hit) => {
46+
if (!acc[hit.pageID]) {
47+
acc[hit.pageID] = []
48+
}
49+
acc[hit.pageID].push({
50+
score: 1,
51+
terms: {},
52+
location: stripDocsPathName ? hit.abs_url.replace('/docs', '') : hit.abs_url,
53+
title: hit.title,
54+
text: hit._highlightResult.content.value,
55+
56+
})
57+
return acc
58+
}, {})
59+
60+
61+
62+
63+
self.postMessage({
64+
type: RESULT, data: {
65+
items: Object.values(mappedGroupedResults)
66+
}
67+
});
68+
}
69+
};

docs/overrides/main.html

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,50 @@
11
{% extends "base.html" %}
22

3+
{% block config %}
4+
{%- set app = {
5+
"base": base_url,
6+
"features": features,
7+
"translations": {},
8+
"search": base_url + "/javascripts/search-worker.js" | url
9+
} -%}
10+
11+
<!-- Versioning -->
12+
{%- if config.extra.version -%}
13+
{%- set mike = config.plugins.get("mike") -%}
14+
{%- if not mike or mike.config.version_selector -%}
15+
{%- set _ = app.update({ "version": config.extra.version }) -%}
16+
{%- endif -%}
17+
{%- endif -%}
18+
19+
<!-- Tags -->
20+
{%- if config.extra.tags -%}
21+
{%- set _ = app.update({ "tags": config.extra.tags }) -%}
22+
{%- endif -%}
23+
24+
<!-- Translations -->
25+
{%- set translations = app.translations -%}
26+
{%- for key in [
27+
"clipboard.copy",
28+
"clipboard.copied",
29+
"search.result.placeholder",
30+
"search.result.none",
31+
"search.result.one",
32+
"search.result.other",
33+
"search.result.more.one",
34+
"search.result.more.other",
35+
"search.result.term.missing",
36+
"select.version"
37+
] -%}
38+
{%- set _ = translations.update({ key: lang.t(key) }) -%}
39+
{%- endfor -%}
40+
41+
<!-- Configuration -->
42+
<script id="__config" type="application/json">
43+
{{- app | tojson -}}
44+
</script>
45+
<!-- Add scripts that need to run afterwards here -->
46+
{% endblock %}
47+
348
{% block content %}
449
{{ super() }}
550
<script src="/flarelytics/client.js"></script>

docs/plugins/build_index.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
from __future__ import annotations as _annotations
2+
3+
import os
4+
from typing import Any, cast
5+
6+
from algoliasearch.search_client import SearchClient
7+
from bs4 import BeautifulSoup
8+
from mkdocs.config import Config
9+
from mkdocs.structure.files import Files
10+
from mkdocs.structure.pages import Page
11+
12+
records: list[dict[str, Any]] = []
13+
ALGOLIA_INDEX_NAME = 'logfire-docs'
14+
ALGOLIA_APP_ID = 'KPPUDTIAVX'
15+
ALGOLIA_WRITE_API_KEY = os.environ.get('ALGOLIA_WRITE_API_KEY')
16+
17+
18+
def on_page_content(html: str, page: Page, config: Config, files: Files) -> str:
19+
if not ALGOLIA_WRITE_API_KEY:
20+
return html
21+
22+
assert page.title is not None, 'Page title must not be None' # type: ignore[reportUnknownMemberType]
23+
title = cast(str, page.title) # type: ignore[reportUnknownMemberType]
24+
25+
soup = BeautifulSoup(html, 'html.parser')
26+
27+
# Find all h1 and h2 headings
28+
headings = soup.find_all(['h1', 'h2'])
29+
30+
# Process each section
31+
for i in range(len(headings)):
32+
current_heading = headings[i]
33+
heading_id = current_heading.get('id', '')
34+
section_title = current_heading.get_text().replace('¶', '').replace('dataclass', '').strip()
35+
36+
# Get content until next heading
37+
content: list[str] = []
38+
sibling = current_heading.find_next_sibling()
39+
while sibling and sibling.name not in ['h1', 'h2']:
40+
content.append(str(sibling))
41+
sibling = sibling.find_next_sibling()
42+
43+
section_html = ''.join(content)
44+
45+
# Create anchor URL
46+
anchor_url = f'{page.abs_url}#{heading_id}' if heading_id else page.abs_url
47+
48+
# Create record for this section
49+
records.append(
50+
{
51+
'content': section_html,
52+
'pageID': title,
53+
'abs_url': anchor_url,
54+
'title': f'{title} - {section_title}',
55+
'objectID': anchor_url,
56+
}
57+
)
58+
59+
return html
60+
61+
62+
def on_post_build(config: Config) -> None:
63+
if not ALGOLIA_WRITE_API_KEY:
64+
return
65+
66+
client = SearchClient.create(ALGOLIA_APP_ID, ALGOLIA_WRITE_API_KEY)
67+
index = client.init_index(ALGOLIA_INDEX_NAME)
68+
# temporary filter the records from the index if the content is bigger than 10k characters
69+
filtered_records = list(filter(lambda record: len(record['content']) < 9000, records))
70+
print(f'Uploading {len(filtered_records)} out of {len(records)} records to Algolia...')
71+
index.replace_all_objects(filtered_records, {'createIfNotExists': True}).wait() # type: ignore[reportUnknownMemberType]

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,3 +308,4 @@ plugins:
308308
"get-started/traces.md": "concepts.md"
309309
hooks:
310310
- docs/plugins/main.py
311+
- docs/plugins/build_index.py

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,8 @@ docs = [
172172
"mkdocstrings-python>=1.8.0",
173173
"mkdocs-redirects>=1.2.1",
174174
"griffe",
175+
"bs4>=0.0.2",
176+
"algoliasearch>=3,<4",
175177
]
176178

177179
[tool.inline-snapshot]

uv.lock

Lines changed: 49 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)