8.7 KiB
Search
Type: page Date: 2026-01-08
Keyword Search
Keyword search matches exact words using Convex full-text search. Results update instantly as you type.
For meaning-based search that finds conceptually similar content, see Semantic Search.
Keyboard shortcuts
Press Cmd+K (Mac) or Ctrl+K (Windows/Linux) to open search. Click the magnifying glass icon works too.
| Key | Action |
|---|---|
Cmd+K / Ctrl+K |
Open/close search |
Tab |
Switch between Keyword and Semantic modes |
↑ ↓ |
Navigate results |
Enter |
Select result |
Esc |
Close modal |
How keyword search works
┌─────────────────────────────────────────────────────────────────────────┐
│ KEYWORD SEARCH FLOW │
└─────────────────────────────────────────────────────────────────────────┘
┌──────────┐ ┌─────────────┐ ┌──────────────────┐
│ Cmd+K │───▶│ SearchModal │───▶│ Convex Query │
└──────────┘ └─────────────┘ └────────┬─────────┘
│
┌──────────────────────┴──────────────────────┐
▼ ▼
┌────────────────┐ ┌────────────────┐
│ search_title │ │ search_content │
│ (index) │ │ (index) │
└───────┬────────┘ └───────┬────────┘
│ │
└──────────────────┬─────────────────────────┘
▼
┌─────────────────────┐
│ Dedupe + Rank │
│ (title matches first)│
└──────────┬──────────┘
▼
┌─────────────────────┐
│ Search Results │
│ (max 15) │
└──────────┬──────────┘
▼
┌─────────────────────┐
│ Navigate + ?q=term │
└──────────┬──────────┘
▼
┌─────────────────────┐
│ Highlight matches │
│ + scroll to first │
└─────────────────────┘
- User presses
Cmd+Kto open SearchModal - SearchModal sends reactive query to Convex as user types
- Convex searches both title and content indexes in parallel
- Results are deduplicated (same post can match both indexes)
- Results ranked with title matches first, limited to 15
- User selects result, navigates with
?q=searchtermparam - Destination page highlights all matches and scrolls to first
Why keyword search
Keyword search is the default because it's:
- Instant - Results return in milliseconds, no API calls
- Free - No external services, no per-query costs
- Reactive - Updates in real-time as you type
- Highlightable - Matches exact words, so results can be highlighted on the destination page
Use keyword search when you know the exact terms, code snippets, or commands you're looking for.
How search indexes work
Search indexes are defined in convex/schema.ts on the posts and pages tables:
posts: defineTable({
title: v.string(),
content: v.string(),
published: v.boolean(),
// ... other fields
})
.searchIndex("search_content", {
searchField: "content",
filterFields: ["published"],
})
.searchIndex("search_title", {
searchField: "title",
filterFields: ["published"],
}),
Each table has two search indexes:
search_title- Searches the title fieldsearch_content- Searches the full markdown content
The filterFields: ["published"] allows filtering to only published content without a separate query.
Search query implementation
The search query in convex/search.ts searches both titles and content, then deduplicates and ranks results:
// Search posts by title
const postsByTitle = await ctx.db
.query("posts")
.withSearchIndex("search_title", (q) =>
q.search("title", args.query).eq("published", true)
)
.take(10);
// Search posts by content
const postsByContent = await ctx.db
.query("posts")
.withSearchIndex("search_content", (q) =>
q.search("content", args.query).eq("published", true)
)
.take(10);
Key features:
- Dual search - Searches both title and content indexes
- Filter by published - Only returns published content
- Deduplication - Removes duplicates when a post matches both title and content
- Ranking - Title matches sort before content-only matches
- Snippets - Generates context snippets around the search term
- Unlisted filtering - Excludes posts with
unlisted: true
Frontend search modal
The SearchModal component (src/components/SearchModal.tsx) provides the UI:
// Reactive search query - updates as you type
const results = useQuery(
api.search.search,
searchQuery.trim() ? { query: searchQuery } : "skip"
);
The "skip" parameter tells Convex to skip the query when the search field is empty, avoiding unnecessary database calls.
Search result highlighting
When you click a search result, the app navigates to the page with a ?q= parameter. The useSearchHighlighting hook (src/hooks/useSearchHighlighting.ts) then:
- Waits for page content to load
- Finds all occurrences of the search term
- Wraps matches in
<mark>tags with highlight styling - Scrolls to the first match
- Highlights pulse, then fade after 4 seconds
- Press
Escto clear highlights manually
Files involved
| File | Purpose |
|---|---|
convex/schema.ts |
Search index definitions |
convex/search.ts |
Search query with deduplication and snippets |
src/components/SearchModal.tsx |
Search UI with keyboard navigation |
src/components/Layout.tsx |
Keyboard shortcut handler (Cmd+K) |
src/hooks/useSearchHighlighting.ts |
Result highlighting and scroll-to-match |
Adding search to new tables
To add search to a new table:
- Add a search index in
convex/schema.ts:
myTable: defineTable({
title: v.string(),
body: v.string(),
status: v.string(),
})
.searchIndex("search_body", {
searchField: "body",
filterFields: ["status"],
}),
- Create a search query in a Convex function:
const results = await ctx.db
.query("myTable")
.withSearchIndex("search_body", (q) =>
q.search("body", searchTerm).eq("status", "published")
)
.take(10);
- Run
npx convex devto deploy the new index
Limitations
- Search indexes only work on string fields
- One
searchFieldper index (create multiple indexes for multiple fields) - Filter fields support equality only (not ranges or inequalities)
- Results are ranked by relevance, not by date or other fields
- Maximum 10 results per
.take()call (paginate for more)
Resources
- Convex Full-Text Search
- Search Index API
- Semantic Search - Vector-based search for finding similar content