Skip to content

feat(search): add useSearchCollection composable with FTS5 full-text search#3787

Merged
farnabaz merged 19 commits into
mainfrom
feat/fts5-search
May 12, 2026
Merged

feat(search): add useSearchCollection composable with FTS5 full-text search#3787
farnabaz merged 19 commits into
mainfrom
feat/fts5-search

Conversation

@benjamincanac

@benjamincanac benjamincanac commented May 6, 2026

Copy link
Copy Markdown
Member

🔗 Linked issue

❓ Type of change

  • 📖 Documentation (updates to the documentation or readme)
  • 🐞 Bug fix (a non-breaking change that fixes an issue)
  • 👌 Enhancement (improving an existing functionality like performance)
  • ✨ New feature (a non-breaking change that adds functionality)
  • ⚠️ Breaking change (fix or feature that would cause existing functionality to change)

📚 Description

Adds useSearchCollection, a client-side composable that provides full-text search powered by SQLite FTS5. it builds an inverted index from content sections at runtime and queries it with BM25 ranking, prefix matching, and multi-column snippets. no external dependencies needed.

const { status, search } = useSearchCollection(['docs', 'blog'])

const results = await search('vue compo', {
  limit: 20,
  snippet: { columns: ['title', 'content'], around: 40 },
  weights: { title: 10, content: 5, heading: true }
})

Unified FTS5 table across multiple collections, lazy index building with immediate: false option, prefix matching on all terms (typing compo matches "composable"), configurable field restriction, BM25 column weights with heading-level boosting, camelCase title normalization for better ranking, and graceful handling of FTS5 syntax errors. queryCollectionSearchSections still available for Fuse.js/MiniSearch users who need typo tolerance.

📝 Checklist

  • I have linked an issue or discussion.
  • I have updated the documentation accordingly.

…t search

Co-Authored-By: Sébastien Chopin <atinux@gmail.com>
@vercel

vercel Bot commented May 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
content Ready Ready Preview, Comment May 11, 2026 8:33am

@pkg-pr-new

pkg-pr-new Bot commented May 6, 2026

Copy link
Copy Markdown
npm i https://pkg.pr.new/@nuxt/content@3787

commit: 606bf64

@socket-security

socket-security Bot commented May 8, 2026

Copy link
Copy Markdown

All alerts resolved. Learn more about Socket for GitHub.

This PR previously contained dependency changes with security issues that have been resolved, removed, or ignored.

View full report

@socket-security

socket-security Bot commented May 11, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addeddrizzle-kit@​0.31.10991009699100

View full report

@benjamincanac
benjamincanac marked this pull request as ready for review May 11, 2026 08:22
@benjamincanac
benjamincanac requested review from atinux and farnabaz May 11, 2026 08:22
@coderabbitai

coderabbitai Bot commented May 11, 2026

Copy link
Copy Markdown

Review Change Stack
No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 59e0bb86-e84c-41a9-a039-0f52cac2f11e

📥 Commits

Reviewing files that changed from the base of the PR and between 5ddcee3 and 606bf64.

📒 Files selected for processing (2)
  • docs/content/blog/studio-oss.md
  • src/runtime/client.ts
✅ Files skipped from review due to trivial changes (1)
  • docs/content/blog/studio-oss.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/runtime/client.ts

📝 Walkthrough

Walkthrough

This PR introduces SQLite FTS5-backed full-text search to Nuxt Content via a new useSearchCollection composable. It defines search result and query option types, implements FTS5 indexing and querying functions with BM25 ranking and snippet support, and exposes a reactive client-side composable that lazily initializes the database adapter and watches for collection changes. The composable is auto-imported at the module level. The playground is updated to remove NuxHub integration and demonstrate the feature with a search results page. Comprehensive tests verify FTS5 table creation, insertion, querying, snippet generation, field scoping, and error handling. Documentation includes detailed API descriptions, usage examples, and a comparison with Fuse.js.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(search): add useSearchCollection composable with FTS5 full-text search' clearly and concisely summarizes the main change: addition of a new composable for FTS5-powered full-text search.
Description check ✅ Passed The description is directly related to the changeset, explaining the new useSearchCollection composable, its key features (FTS5, BM25 ranking, prefix matching, lazy indexing), and implementation details.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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 feat/fts5-search

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (3)
docs/package.json (1)

17-17: ⚡ Quick win

Move drizzle-kit to devDependencies.

drizzle-kit is a CLI tool for schema migrations and generation, not a runtime dependency. It should be in devDependencies to avoid including it in production installs. No runtime usage of this package was found in the codebase.

Suggested change
   "dependencies": {
     "@libsql/client": "^0.17.2",
     "@nuxt/content": "link:..",
     "@nuxthub/core": "^0.10.7",
     "@nuxtjs/plausible": "^3.0.2",
     "@vercel/analytics": "^2.0.1",
     "@vercel/speed-insights": "^2.0.0",
     "@vueuse/nuxt": "^14.2.1",
     "better-sqlite3": "^12.8.0",
     "docus": "^5.8.1",
-    "drizzle-kit": "^0.31.10",
     "drizzle-orm": "^0.45.1",
     "minisearch": "^7.2.0",
     "nuxt": "^4.4.2",
     "nuxt-studio": "^1.5.1"
+  },
+  "devDependencies": {
+    "drizzle-kit": "^0.31.10"
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/package.json` at line 17, The package.json currently lists "drizzle-kit"
in dependencies; move "drizzle-kit" into devDependencies instead because it's a
CLI/build tool not required at runtime: remove the "drizzle-kit" entry from the
top-level "dependencies" and add the same version string under "devDependencies"
(preserve the version "^0.31.10"), then update your lockfile by running your
package manager (npm/yarn/pnpm install) to reflect the change; verify no runtime
imports reference "drizzle-kit" and commit the modified package.json and updated
lockfile.
src/runtime/client.ts (2)

100-100: ⚖️ Poor tradeoff

Consider adding a cleanup method.

The composable doesn't expose a way to dispose of the DatabaseAdapter. If the component using this composable is unmounted or the collections change significantly, the database adapter and FTS index remain in memory.

Consider adding a destroy() or cleanup() method to the return object that closes the database adapter if the underlying implementation supports it:

function destroy() {
  db = undefined
  initPromise = undefined
  indexedFor = []
  status.value = 'idle'
}

return { status, search, init, destroy }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/runtime/client.ts` at line 100, Add a cleanup method to the composable
that disposes the DatabaseAdapter and resets internal state: implement a destroy
or cleanup function that, if the underlying DatabaseAdapter supports a
close/closeAsync method, calls it, then sets db = undefined, initPromise =
undefined, indexedFor = [], and status.value = 'idle'; finally include this
method in the returned object alongside status, search, and init so callers can
explicitly release resources.

74-74: 💤 Low value

Type assertion bypasses collection key validation.

The cast col as T assumes that the string from toIndex is a valid collection key, but the type system doesn't verify this at runtime. While the function signature constrains the input to keyof PageCollections, the string manipulation path through resolveCollections() erases that type information.

This is acceptable given the constraints, but if stricter type safety is desired, consider maintaining typed arrays throughout:

function resolveCollections(): T[] {
  const val = toValue(collection)
  return (Array.isArray(val) ? val : [val]) as T[]
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/runtime/client.ts` at line 74, The cast `col as T` in the line calling
queryCollection(col as T) bypasses collection key validation; change
resolveCollections() (and any path using toIndex/from toValue) to preserve the
narrower T[] type so callers can pass a properly typed collection key instead of
asserting. Concretely, update resolveCollections() to return T[] (e.g., coerce
the result of toValue(collection) to T[] once and use that typed array wherever
queryCollection is called), and remove the inline `as T` cast in queryCollection
calls so the compiler enforces the correct keyof PageCollections type at call
sites.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/content/blog/studio-oss.md`:
- Line 71: Update the sentence "The modern Notion-like editing experience for
Markdown content is back with a improved version, powered by
[TipTap](https://tiptap.dev/) integrated through the [Nuxt UI
Editor](https://ui.nuxt.com/components/editor) component:" to fix the article
agreement typo by changing "a improved version" to "an improved version" so it
reads "...is back with an improved version..."; locate the exact string in the
document and correct the article.

In `@src/runtime/client.ts`:
- Around line 93-98: The search function can trigger concurrent init() calls
because multiple callers see !db and start initialization; modify search to wait
on a shared initPromise: if db is not ready, ensure you assign/initiate a single
shared initPromise (create/initPromise when missing) and await that promise
before calling queryFTS; use the existing initPromise and init() symbols (and
db) so subsequent concurrent search() callers await the same initialization
instead of starting duplicate adapters/index builds.
- Line 55: The current early-return when collections.length is zero returns
initPromise ?? Promise.resolve(db!), which can resolve to undefined if db isn't
initialized; update the branch so it never resolves to undefined: if initPromise
exists return it, otherwise if db is defined return Promise.resolve(db as
DatabaseAdapter), else return Promise.reject(new Error("DatabaseAdapter not
initialized")) (or trigger the existing initialization path) — adjust references
to collections, initPromise and db to implement this safe behavior.

---

Nitpick comments:
In `@docs/package.json`:
- Line 17: The package.json currently lists "drizzle-kit" in dependencies; move
"drizzle-kit" into devDependencies instead because it's a CLI/build tool not
required at runtime: remove the "drizzle-kit" entry from the top-level
"dependencies" and add the same version string under "devDependencies" (preserve
the version "^0.31.10"), then update your lockfile by running your package
manager (npm/yarn/pnpm install) to reflect the change; verify no runtime imports
reference "drizzle-kit" and commit the modified package.json and updated
lockfile.

In `@src/runtime/client.ts`:
- Line 100: Add a cleanup method to the composable that disposes the
DatabaseAdapter and resets internal state: implement a destroy or cleanup
function that, if the underlying DatabaseAdapter supports a close/closeAsync
method, calls it, then sets db = undefined, initPromise = undefined, indexedFor
= [], and status.value = 'idle'; finally include this method in the returned
object alongside status, search, and init so callers can explicitly release
resources.
- Line 74: The cast `col as T` in the line calling queryCollection(col as T)
bypasses collection key validation; change resolveCollections() (and any path
using toIndex/from toValue) to preserve the narrower T[] type so callers can
pass a properly typed collection key instead of asserting. Concretely, update
resolveCollections() to return T[] (e.g., coerce the result of
toValue(collection) to T[] once and use that typed array wherever
queryCollection is called), and remove the inline `as T` cast in queryCollection
calls so the compiler enforces the correct keyof PageCollections type at call
sites.
🪄 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: 15a5f26c-4253-43e8-a332-13bd471f5702

📥 Commits

Reviewing files that changed from the base of the PR and between 55b8793 and 5ddcee3.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (13)
  • docs/content/blog/studio-oss.md
  • docs/content/docs/4.utils/5.use-search-collection.md
  • docs/content/docs/8.advanced/1.fulltext-search.md
  • docs/package.json
  • playground/app.vue
  • playground/content.config.ts
  • playground/nuxt.config.ts
  • playground/package.json
  • playground/pages/search.vue
  • src/module.ts
  • src/runtime/client.ts
  • src/runtime/internal/search.ts
  • test/unit/searchCollection.test.ts
💤 Files with no reviewable changes (2)
  • playground/package.json
  • playground/nuxt.config.ts

Comment thread docs/content/blog/studio-oss.md Outdated
Comment thread src/runtime/client.ts Outdated
Comment thread src/runtime/client.ts

@atinux atinux left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LFG!

@farnabaz
farnabaz merged commit 2a1a585 into main May 12, 2026
10 checks passed
@farnabaz
farnabaz deleted the feat/fts5-search branch May 12, 2026 15:59
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.

3 participants