Overview
The Search API provides powerful search capabilities across all marketplace entities including stores, products, and customers. It supports real-time search, advanced filtering, search history, and intelligent autocomplete functionality.
Location: convex/shared/search.ts
Search Types
Quick Search Fast autocomplete search with limited results for instant feedback
Comprehensive Search Full search with pagination, filtering, and sorting options
Advanced Search Complex search with multiple filters, price ranges, and location-based filtering
Search History Save and retrieve user search history for better UX
Quick Search
Performs fast search with limited results, perfect for autocomplete functionality.
Maximum results to return (default: 10)
TypeScript
JavaScript
Python
const results = await convex . query ( api . shared . search . quickSearch , {
query: "pizza" ,
limit: 5
});
{
"suggestions" : [
{ "text" : "Mario's Pizza Palace" , "type" : "store" , "id" : "j123456789" },
{ "text" : "Pizza Margherita" , "type" : "product" , "id" : "p123456789" },
{ "text" : "Pepperoni Pizza" , "type" : "product" , "id" : "p987654321" }
]
}
Comprehensive Search
Performs full search with pagination and detailed results.
type
'all' | 'customers' | 'stores' | 'products'
Filter by entity type (default: “all”)
Optional category filter applied to stores/products
Pagination configuration with numItems and cursor
const searchResults = await convex . query ( api . shared . search . search , {
query: "italian restaurant" ,
type: "stores" ,
categoryId: "k123456789" ,
paginationOpts: {
numItems: 20 ,
cursor: null
}
});
{
"results" : [
{
"type" : "store" ,
"name" : "Mario's Italian Kitchen" ,
"id" : "j123456789" ,
"description" : "Authentic Italian cuisine in the heart of Dubai"
},
{
"type" : "store" ,
"name" : "Luigi's Trattoria" ,
"id" : "j987654321" ,
"description" : "Family-owned Italian restaurant since 1995"
},
{
"type" : "product" ,
"name" : "Spaghetti Carbonara" ,
"id" : "p555666777" ,
"description" : "Classic Italian pasta with eggs, cheese, and pancetta"
}
],
"totalCount" : 25 ,
"isDone" : false ,
"continueCursor" : "eyJfaWQiOiJqOTg3NjU0MzIxIn0"
}
Advanced Search
Performs complex search with multiple filters and sorting options.
Advanced filter options: type
'customers' | 'stores' | 'products'
Constrain search to a specific entity type
Filter products within a specific store
priceRange
{ min: number; max: number }
Inclusive price range filter (products only)
sortBy
'relevance' | 'name' | 'price_low' | 'price_high' | 'popularity'
Sort results (default: ‘relevance’)
const advancedResults = await convex . query ( api . shared . search . advancedSearch , {
query: "pizza" ,
filters: {
type: "products" ,
categoryId: "k123456789" ,
priceRange: { min: 10 , max: 30 },
sortBy: "price_low"
},
paginationOpts: {
numItems: 20 ,
cursor: null
}
});
{
"results" : [
{
"type" : "product" ,
"name" : "Pizza Margherita" ,
"id" : "p123456789" ,
"description" : "Classic Italian pizza with fresh mozzarella" ,
"price" : 15.99 ,
"rating" : 4.8 ,
"store" : {
"name" : "Mario's Pizza" ,
"city" : "Dubai"
}
},
{
"type" : "product" ,
"name" : "Pepperoni Pizza" ,
"id" : "p987654321" ,
"description" : "Traditional pepperoni pizza with spicy salami" ,
"price" : 18.99 ,
"rating" : 4.6 ,
"store" : {
"name" : "Tony's Pizzeria" ,
"city" : "Dubai"
}
}
],
"totalCount" : 45 ,
"isDone" : false ,
"continueCursor" : "eyJfaWQiOiJwOTg3NjU0MzIxIiwicHJpY2UiOjE4Ljk5fQ"
}
Save Search History
Save a user’s search query to their search history.
searchType
'all' | 'customers' | 'stores' | 'products'
required
Search type that was used
Number of results returned for this search
await convex . mutation ( api . shared . search . saveSearchHistory , {
searchTerm: "italian pizza" ,
searchType: "products" ,
resultCount: 42
});
Get Search History
Retrieve user’s search history.
Maximum number of history entries to return (default: 20)
const history = await convex . query ( api . shared . search . getSearchHistory , {
limit: 10
});
[
{
"_id" : "sh123456789" ,
"searchTerm" : "italian pizza" ,
"searchType" : "products" ,
"resultCount" : 42 ,
"_creationTime" : 1640995200000 ,
"customerAvatarUrl" : "https://storage.convex.dev/john.jpg"
},
{
"_id" : "sh987654321" ,
"searchTerm" : "best restaurants dubai" ,
"searchType" : "stores" ,
"resultCount" : 17 ,
"_creationTime" : 1640994000000 ,
"customerAvatarUrl" : null
}
]
Clear Search History
Clear all search history for the authenticated user.
await convex . mutation ( api . shared . search . clearSearchHistory );
Get Friends List
Get all mutual friends (customers who follow each other bidirectionally).
const friends = await convex . query ( api . shared . search . getFriends , {
paginationOpts: { numItems: 20 , cursor: null }
});
{
"results" : [
{
"_id" : "c123456789" ,
"name" : "John Doe" ,
"username" : "johndoe" ,
"avatarUrl" : "https://storage.convex.dev/john.jpg" ,
"followersCount" : 45 ,
"isActive" : true ,
"friendsSince" : 1640995800000
},
{
"_id" : "c987654321" ,
"name" : "Sarah Smith" ,
"username" : "sarahsmith" ,
"avatarUrl" : null ,
"followersCount" : 32 ,
"isActive" : true ,
"friendsSince" : 1640994600000
}
],
"totalCount" : 2 ,
"isDone" : true ,
"continueCursor" : null
}
Search Friends Only
Search among mutual friends (customers who follow each other bidirectionally).
Search query to find among mutual friends
const friendResults = await convex . query ( api . shared . search . searchFriends , {
query: "john" ,
paginationOpts: { numItems: 10 , cursor: null }
});
{
"results" : [
{
"_id" : "c123456789" ,
"name" : "John Doe" ,
"username" : "johndoe" ,
"avatarUrl" : "https://storage.convex.dev/john.jpg" ,
"followersCount" : 45 ,
"isActive" : true
},
{
"_id" : "c987654321" ,
"name" : "Sarah Smith" ,
"username" : "sarahsmith" ,
"avatarUrl" : null ,
"followersCount" : 32 ,
"isActive" : true
}
],
"totalCount" : 2 ,
"isDone" : true ,
"continueCursor" : null
}
Get Share Recipients
Get recent and frequent share recipients based on the user’s sharing history.
Maximum number of recipients to return per category (default: 20)
const recipients = await convex . query ( api . shared . search . getShareRecipients , {
limit: 10
});
{
"customerAvatarUrl" : "https://storage.convex.dev/john.jpg" ,
"recent" : [
{
"_id" : "c123456789" ,
"name" : "John Doe" ,
"username" : "johndoe" ,
"avatarUrl" : "https://storage.convex.dev/john.jpg" ,
"lastSharedAt" : 1640995800000 ,
"shareCount" : 3
},
{
"_id" : "c987654321" ,
"name" : "Sarah Smith" ,
"username" : "sarahsmith" ,
"avatarUrl" : null ,
"lastSharedAt" : 1640994600000 ,
"shareCount" : 1
}
],
"frequent" : [
{
"_id" : "c555666777" ,
"name" : "Mike Johnson" ,
"username" : "mikejohnson" ,
"avatarUrl" : "https://storage.convex.dev/mike.jpg" ,
"shareCount" : 8 ,
"lastSharedAt" : 1640990000000
},
{
"_id" : "c123456789" ,
"name" : "John Doe" ,
"username" : "johndoe" ,
"avatarUrl" : "https://storage.convex.dev/john.jpg" ,
"shareCount" : 3 ,
"lastSharedAt" : 1640995800000
}
]
}
Test Search
Test search functionality with sample results (development/debugging).
type
'all' | 'customers' | 'stores' | 'products'
Search type filter
const testResult = await convex . query ( api . shared . search . testSearch , {
query: "pizza" ,
type: "all"
});
{
"success" : true ,
"message" : "Search completed successfully" ,
"resultCount" : 15 ,
"sampleResults" : [
"Mario's Pizza Palace" ,
"Pizza Margherita" ,
"Tony's Pizzeria" ,
"Pepperoni Pizza Special"
]
}
Search Component Examples
Complete search implementations for your frontend:
React Search Component
Advanced Search Form
import { useState , useEffect , useMemo } from 'react' ;
import { useQuery , useMutation } from 'convex/react' ;
import { api } from './convex/_generated/api' ;
import { debounce } from 'lodash' ;
interface SearchComponentProps {
onResultSelect ?: ( result : any ) => void ;
placeholder ?: string ;
searchType ?: 'all' | 'stores' | 'products' | 'customers' ;
}
function SearchComponent ({ onResultSelect , placeholder = "Search..." , searchType = "all" } : SearchComponentProps ) {
const [ query , setQuery ] = useState ( '' );
const [ showResults , setShowResults ] = useState ( false );
const [ showHistory , setShowHistory ] = useState ( false );
// Quick search for autocomplete
const quickResults = useQuery (
api . shared . search . quickSearch ,
query . length > 2 ? { query , limit: 8 } : "skip"
);
// Search history
const searchHistory = useQuery ( api . shared . search . getSearchHistory , { limit: 5 });
const saveSearch = useMutation ( api . shared . search . saveSearchHistory );
// Debounced search to avoid too many queries
const debouncedSetQuery = useMemo (
() => debounce (( value : string ) => setQuery ( value ), 300 ),
[]
);
const handleInputChange = ( e : React . ChangeEvent < HTMLInputElement >) => {
const value = e . target . value ;
debouncedSetQuery ( value );
setShowResults ( value . length > 2 );
setShowHistory ( value . length === 0 );
};
const handleResultClick = async ( result : any ) => {
setShowResults ( false );
setShowHistory ( false );
// Save to search history
if ( query . length > 2 ) {
await saveSearch ({
searchTerm: query ,
searchType ,
resultCount: ( quickResults ?. suggestions ?. length ?? 0 )
});
}
onResultSelect ?.( result );
};
const handleHistoryClick = ( historyItem : any ) => {
setQuery ( historyItem . query );
setShowHistory ( false );
setShowResults ( true );
};
return (
< div className = "relative w-full" >
< div className = "relative" >
< input
type = "text"
placeholder = { placeholder }
onChange = { handleInputChange }
onFocus = {() => setShowHistory (query.length === 0)}
onBlur = {() => {
// Delay hiding to allow clicks on results
setTimeout (() => {
setShowResults ( false );
setShowHistory ( false );
}, 200 );
}}
className = "w-full p-3 pl-10 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
< div className = "absolute left-3 top-1/2 transform -translate-y-1/2" >
< svg className = "w-5 h-5 text-gray-400" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
< path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 2 } d = "M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</ svg >
</ div >
</ div >
{ /* Search Results */ }
{ showResults && quickResults && quickResults . suggestions ?. length > 0 && (
< div className = "absolute z-10 w-full mt-1 bg-white border border-gray-200 rounded-lg shadow-lg max-h-96 overflow-y-auto" >
< div className = "p-2 text-xs text-gray-500 border-b" >
{ quickResults . suggestions . length } results for "{ query }"
</ div >
{ quickResults . suggestions . map (( result , index ) => (
< div
key = { ` ${ result . type } - ${ result . id } ` }
onClick = {() => handleResultClick ( result )}
className = "p-3 hover:bg-gray-50 cursor-pointer border-b last:border-b-0"
>
< div className = "flex items-center space-x-3" >
< div className = "flex-shrink-0" >
{ result . type === ' store ' && < span className = "text-blue-500" > 🏪 </ span > }
{ result . type === ' product ' && < span className = "text-green-500" > 📦 </ span > }
{ result . type === ' customer ' && < span className = "text-purple-500" > 👤 </ span > }
</ div >
< div className = "flex-1 min-w-0" >
< p className = "text-sm font-medium text-gray-900 truncate" >
{ result . name }
</ p >
< p className = "text-xs text-gray-500 capitalize" >
{ result . type }
</ p >
</ div >
</ div >
</ div >
))}
</ div >
)}
{ /* Search History */ }
{ showHistory && searchHistory && searchHistory . length > 0 && (
< div className = "absolute z-10 w-full mt-1 bg-white border border-gray-200 rounded-lg shadow-lg" >
< div className = "p-2 text-xs text-gray-500 border-b" >
Recent searches
</ div >
{ searchHistory . map (( item ) => (
< div
key = {item. _id }
onClick = {() => handleHistoryClick ( item )}
className = "p-3 hover:bg-gray-50 cursor-pointer border-b last:border-b-0 flex items-center space-x-3"
>
< svg className = "w-4 h-4 text-gray-400" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
< path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 2 } d = "M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</ svg >
< span className = "text-sm text-gray-700" > {item. query } </ span >
< span className = "text-xs text-gray-400 ml-auto" >
{ item . searchCount > 1 && ` $ { item . searchCount } x `}
</ span >
</ div >
))}
</ div >
)}
</ div >
);
}
export default SearchComponent ;
Search Features
Real-time Results Instant search results as users type with debounced queries
Search History Save and display user search history for better UX
Advanced Filters Filter by category, location, price range, and more
Smart Sorting Sort by relevance, price, rating, date, or alphabetically
Error Handling
Status Code : 400{
"error" : "Search query cannot be empty" ,
"minLength" : 1
}
Status Code : 400{
"error" : "Invalid search type" ,
"validTypes" : [ "all" , "customers" , "stores" , "products" ]
}
Search service unavailable
Status Code : 503{
"error" : "Search service temporarily unavailable" ,
"retryAfter" : 30
}
Search results are automatically indexed and updated in real-time as data changes. The search system supports fuzzy matching and handles typos gracefully.
Use quick search for autocomplete functionality and comprehensive search for full search pages. Save frequently searched terms to improve user experience.