Skip to content

feat: use flexsearch instead of algolia#138

Merged
LuLaValva merged 2 commits intomainfrom
flexsearch
Apr 13, 2026
Merged

feat: use flexsearch instead of algolia#138
LuLaValva merged 2 commits intomainfrom
flexsearch

Conversation

@LuLaValva
Copy link
Copy Markdown
Member

Abandon Algolia in favor of flexsearch (this is what svelte.dev uses).

Also changed the styles for the search dialog

image

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 13, 2026

Caution

Review failed

Pull request was closed or merged during review

Walkthrough

This pull request removes the Algolia DocSearch integration and related assets (preconnect header, DocSearch styles, and module), adds FlexSearch and types, and introduces a build-time search index generator that writes public/search-index.json. It adds a FlexSearch-based web worker and a search-worker client, a new search dialog component with keyboard shortcut handling, and updates the header markup/styles to open the dialog. Minor config and artifact files were also updated (package.json, .gitignore, cspell, skills-lock).

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 46.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: replacing Algolia with flexsearch as the search solution.
Description check ✅ Passed The description explains the core change (switching from Algolia to flexsearch) and references a library comparison, directly relating to the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch flexsearch

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (1)
src/tags/search-dialog/search-worker-client.ts (1)

24-24: Defer worker startup until search is actually used.

Line 24 eagerly spins up the worker and starts the /search-index.json fetch as soon as this module evaluates. Deferring that to the first open/query avoids front-loading the full index for visitors who never use search.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/tags/search-dialog/search-worker-client.ts` at line 24, The module
currently calls ensureWorker() at top-level which eagerly starts the worker and
fetches /search-index.json; remove that top-level invocation and instead call
ensureWorker() lazily from the user-facing entrypoints that open or query search
(e.g., the functions that show the search dialog and run queries such as
openSearchDialog, handleSearchQuery or performSearch—use the actual exported
names in this file). Add a simple started/initializing guard around
ensureWorker() so concurrent calls don't re-start the worker, and await
ensureWorker() inside those entrypoints before triggering any fetches or worker
messages so the index is only loaded when search is actually used.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/tags/app-header/app-header.marko`:
- Line 57: The header's two-way binding using search-dialog open:=searchOpen
breaks because search-dialog never calls input.openChange; update search-dialog
(src/tags/search-dialog/search-dialog.marko) to invoke input.openChange?.(false)
whenever the dialog closes internally (e.g., backdrop clicks, ESC/key handlers,
or any internal closeButton handlers) so searchOpen is kept in sync, or
alternatively stop using := and bind one-way (open=) plus an explicit close
handler in app-header if you prefer to avoid two-way binding until openChange
exists.

In `@src/tags/search-dialog/search-dialog.marko`:
- Around line 35-43: The current handler that awaits sendQuery(query) can apply
out-of-order responses; change the flow so only the latest query's response
updates results/activeIndex: either (A) modify sendQuery to accept and return a
unique requestId (or include the original query) and, in the script block, track
latestRequestId/latestQuery and only assign results/activeIndex when the
returned requestId/query matches the latest one, or (B) make sendQuery support
cancellation (e.g., via AbortController) and abort any previous pending request
before issuing a new sendQuery; reference the sendQuery function in
src/tags/search-dialog/search-worker-client.ts and the results/activeIndex
assignments in the script block to implement one of these defenses.
- Around line 63-72: The form currently uses method="dialog" which lets the
native dialog close before your onSubmit handler runs, causing state desync;
remove method="dialog" from the form element and update the onSubmit handler
(the anonymous onSubmit in the form tag that references results, activeIndex,
open, and query) to call event.preventDefault() at the top, then only perform
the navigation and set open = false and query = "" inside the existing if
(results && results[activeIndex]) block so the component state and the dialog
element remain in sync.

In `@src/tags/search-dialog/search-worker-client.ts`:
- Around line 26-38: sendQuery currently subscribes with { once: true } and
resolves on the next "results" message regardless of which query produced it;
change it to correlate responses by sending a unique request id (e.g., generate
a short uid inside sendQuery) with the postMessage and install a message
listener that checks e.data.type === "results" && e.data.requestId === id (or
e.data.query === q if you prefer matching query), resolving when matched and
then removing that listener; update sendQuery (and ensureWorker usage) to post {
type: "query", query: q, requestId: id } and make the listener remove itself
only after a matching response so overlapping queries don't interfere (note:
search-worker.ts already echoes query so mirror that behavior for requestId or
query matching).

In `@src/util/search-index-builder.ts`:
- Around line 49-65: The slug generation here must match the renderer's single
GithubSlugger sequence: seed and advance the same slugger for every heading in
document order instead of only calling slugger.slug when building sectionHref;
specifically, initialize GithubSlugger once (already present), call
slugger.slug(pageTitle) to account for the top-level heading and then, while
iterating the sections array, advance the same slugger for each heading
encountered (even if you don't use the returned value) so that when you compute
sectionHref you use the slug value returned for that heading from
slugger.slug(section.heading); keep references to GithubSlugger, slugger.slug,
sectionHref, sections and ensure the slugger state is advanced in the same order
the renderer visits headings.

In `@src/util/search-worker.ts`:
- Around line 154-158: The worker's "init" case must catch failures from
fetch()/res.json() and post an explicit error instead of never posting "ready";
wrap the fetch+json+init(blocks) sequence in a try/catch, only postMessage({
type: "ready" }) on success, and on error postMessage({ type: "init-error",
error: String(err) }) (include enough error text for debugging); update any
error paths in the "init" case that call init() to avoid throwing uncaught
exceptions so ensureWorker() (in search-worker-client.ts) can reject or recover
instead of hanging.
- Around line 83-84: The snippet clipping logic in src/util/search-worker.ts
incorrectly uses `content.indexOf(" ", end) || end`, which keeps -1 (truthy)
when no next space exists and thus trims the last character; change this to
explicitly check the indexOf result: call `content.indexOf(" ", end)` into a
temporary (e.g., nextSpace) and if nextSpace !== -1 set `end = nextSpace`,
otherwise set `end = content.length` (so the snippet runs to the end); do the
analogous explicit check for the `start` adjustment if needed to avoid relying
on `||` with -1.

---

Nitpick comments:
In `@src/tags/search-dialog/search-worker-client.ts`:
- Line 24: The module currently calls ensureWorker() at top-level which eagerly
starts the worker and fetches /search-index.json; remove that top-level
invocation and instead call ensureWorker() lazily from the user-facing
entrypoints that open or query search (e.g., the functions that show the search
dialog and run queries such as openSearchDialog, handleSearchQuery or
performSearch—use the actual exported names in this file). Add a simple
started/initializing guard around ensureWorker() so concurrent calls don't
re-start the worker, and await ensureWorker() inside those entrypoints before
triggering any fetches or worker messages so the index is only loaded when
search is actually used.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3b993727-04ec-4ea7-b34a-33237964d09e

📥 Commits

Reviewing files that changed from the base of the PR and between 5c6f372 and 37a329c.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json and included by **
📒 Files selected for processing (16)
  • .gitignore
  • cspell.json
  • docs/reference/core-tag.md
  • package.json
  • skills-lock.json
  • src/routes/+layout.marko
  • src/tags/app-header/app-header.marko
  • src/tags/app-header/app-header.style.module.scss
  • src/tags/app-header/docsearch.scss
  • src/tags/app-header/docsearch.ts
  • src/tags/search-dialog/search-dialog.marko
  • src/tags/search-dialog/search-dialog.style.module.scss
  • src/tags/search-dialog/search-worker-client.ts
  • src/util/markodown.ts
  • src/util/search-index-builder.ts
  • src/util/search-worker.ts
💤 Files with no reviewable changes (3)
  • src/routes/+layout.marko
  • src/tags/app-header/docsearch.ts
  • src/tags/app-header/docsearch.scss

Comment thread src/tags/app-header/app-header.marko
Comment thread src/tags/search-dialog/search-dialog.marko
Comment thread src/tags/search-dialog/search-dialog.marko
Comment on lines +26 to +38
export async function sendQuery(q: string): Promise<SearchHit[]> {
await ensureWorker();
const result = new Promise<SearchHit[]>((resolve) => {
worker!.addEventListener(
"message",
(e: MessageEvent) => {
if (e.data.type === "results") resolve(e.data.results);
},
{ once: true },
);
});
worker!.postMessage({ type: "query", query: q });
return result;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Correlate "results" messages to the query that sent them.

Every sendQuery() call resolves on the next "results" message, not its own response. If keystrokes fire overlapping queries, all pending promises can settle with the first result set and later responses are dropped. src/util/search-worker.ts already echoes query, but { once: true } means even a non-matching response would still remove the listener, so this really needs request ids or manual unsubscription after a match.

🧰 Tools
🪛 ast-grep (0.42.1)

[warning] 28-34: Message event listeners should validate the origin to prevent XSS attacks. Always check the event origin before processing the message.
Context: worker!.addEventListener(
"message",
(e: MessageEvent) => {
if (e.data.type === "results") resolve(e.data.results);
},
{ once: true },
)
Note: [CWE-346] Origin Validation Error [REFERENCES]
- https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html
- https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage

(event-origin-validation)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/tags/search-dialog/search-worker-client.ts` around lines 26 - 38,
sendQuery currently subscribes with { once: true } and resolves on the next
"results" message regardless of which query produced it; change it to correlate
responses by sending a unique request id (e.g., generate a short uid inside
sendQuery) with the postMessage and install a message listener that checks
e.data.type === "results" && e.data.requestId === id (or e.data.query === q if
you prefer matching query), resolving when matched and then removing that
listener; update sendQuery (and ensureWorker usage) to post { type: "query",
query: q, requestId: id } and make the listener remove itself only after a
matching response so overlapping queries don't interfere (note: search-worker.ts
already echoes query so mirror that behavior for requestId or query matching).

Comment thread src/util/search-index-builder.ts
Comment thread src/util/search-worker.ts Outdated
Comment thread src/util/search-worker.ts Outdated
@LuLaValva LuLaValva merged commit 05fc7da into main Apr 13, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant