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.string
required
Search query string
number
Maximum results to return (default: 10)
const results = await convex.query(api.shared.search.quickSearch, {
query: "pizza",
limit: 5
});
const results = await convex.query("shared.search.quickSearch", {
query: "pizza",
limit: 8
});
import requests
response = requests.post(
f"{CONVEX_URL}/api/query",
json={
"path": "shared.search.quickSearch",
"args": {
"query": "pizza",
"type": "all",
"limit": 5
}
},
headers={"Authorization": f"Bearer {token}"}
)
results = response.json()
{
"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.string
required
Search query string
'all' | 'customers' | 'stores' | 'products'
Filter by entity type (default: “all”)
Id<'categories'>
Optional category filter applied to stores/products
PaginationOptions
required
Pagination configuration with
numItems and cursorconst searchResults = await convex.query(api.shared.search.search, {
query: "italian restaurant",
type: "stores",
categoryId: "k123456789",
paginationOpts: {
numItems: 20,
cursor: null
}
});
const searchResults = await convex.query("shared.search.search", {
query: "italian restaurant",
type: "all",
paginationOpts: {
numItems: 15,
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.string
required
Search query string
object
Advanced filter options:
Show Filter Fields
Show Filter Fields
'customers' | 'stores' | 'products'
Constrain search to a specific entity type
Id<'categories'>
Filter by category
Id<'stores'>
Filter products within a specific store
{ min: number; max: number }
Inclusive price range filter (products only)
'relevance' | 'name' | 'price_low' | 'price_high' | 'popularity'
Sort results (default: ‘relevance’)
PaginationOptions
required
Pagination configuration
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
}
});
const advancedResults = await convex.query("shared.search.advancedSearch", {
query: "electronics",
filters: {
type: "stores",
categoryId: "k987654321",
sortBy: "popularity"
},
paginationOpts: {
numItems: 10,
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.string
required
Search query text
'all' | 'customers' | 'stores' | 'products'
required
Search type that was used
number
required
Number of results returned for this search
await convex.mutation(api.shared.search.saveSearchHistory, {
searchTerm: "italian pizza",
searchType: "products",
resultCount: 42
});
await convex.mutation("shared.search.saveSearchHistory", {
searchTerm: "best restaurants dubai",
searchType: "stores",
resultCount: 17
});
null
Get Search History
Retrieve user’s search history.number
Maximum number of history entries to return (default: 20)
const history = await convex.query(api.shared.search.getSearchHistory, {
limit: 10
});
const history = await convex.query("shared.search.getSearchHistory", {
limit: 15
});
[
{
"_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);
await convex.mutation("shared.search.clearSearchHistory");
null
Get Friends List
Get all mutual friends (customers who follow each other bidirectionally).PaginationOptions
required
Pagination configuration
const friends = await convex.query(api.shared.search.getFriends, {
paginationOpts: { numItems: 20, cursor: null }
});
const friends = await convex.query("shared.search.getFriends", {
paginationOpts: { numItems: 15, 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).string
required
Search query to find among mutual friends
PaginationOptions
required
Pagination configuration
const friendResults = await convex.query(api.shared.search.searchFriends, {
query: "john",
paginationOpts: { numItems: 10, cursor: null }
});
const friendResults = await convex.query("shared.search.searchFriends", {
query: "sarah",
paginationOpts: { numItems: 15, 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.number
Maximum number of recipients to return per category (default: 20)
const recipients = await convex.query(api.shared.search.getShareRecipients, {
limit: 10
});
const recipients = await convex.query("shared.search.getShareRecipients", {
limit: 15
});
{
"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).string
required
Search query to test
'all' | 'customers' | 'stores' | 'products'
Search type filter
const testResult = await convex.query(api.shared.search.testSearch, {
query: "pizza",
type: "all"
});
const testResult = await convex.query("shared.search.testSearch", {
query: "electronics",
type: "products"
});
{
"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: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;
import { useState } from 'react';
import { useQuery } from 'convex/react';
import { api } from './convex/_generated/api';
interface AdvancedSearchProps {
onResults: (results: any) => void;
}
function AdvancedSearchForm({ onResults }: AdvancedSearchProps) {
const [searchParams, setSearchParams] = useState({
query: '',
type: 'all' as const,
categoryId: '',
cityId: '',
minPrice: '',
maxPrice: '',
sortBy: 'relevance' as const
});
const categories = useQuery(api.shared.categories.getParents);
const cities = useQuery(api.shared.locations.getCities);
const handleSearch = async () => {
const filters: any = {
type: searchParams.type !== 'all' ? searchParams.type : undefined,
categoryId: searchParams.categoryId || undefined,
cityId: searchParams.cityId || undefined,
minPrice: searchParams.minPrice ? parseFloat(searchParams.minPrice) : undefined,
maxPrice: searchParams.maxPrice ? parseFloat(searchParams.maxPrice) : undefined
};
// Remove undefined values
Object.keys(filters).forEach(key =>
filters[key] === undefined && delete filters[key]
);
const results = await convex.query(api.shared.search.advancedSearch, {
query: searchParams.query,
filters: { ...filters, sortBy: searchParams.sortBy },
paginationOpts: { numItems: 20, cursor: null }
});
onResults(results);
};
return (
<div className="bg-white p-6 rounded-lg shadow">
<h3 className="text-lg font-semibold mb-4">Advanced Search</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Search Query */}
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-2">
Search Query
</label>
<input
type="text"
value={searchParams.query}
onChange={(e) => setSearchParams(prev => ({ ...prev, query: e.target.value }))}
placeholder="Enter your search query..."
className="w-full p-3 border border-gray-300 rounded-lg"
/>
</div>
{/* Search Type */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Search Type
</label>
<select
value={searchParams.type}
onChange={(e) => setSearchParams(prev => ({ ...prev, type: e.target.value as any }))}
className="w-full p-3 border border-gray-300 rounded-lg"
>
<option value="all">All</option>
<option value="stores">Stores</option>
<option value="products">Products</option>
<option value="customers">Customers</option>
</select>
</div>
{/* Sort By */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Sort By
</label>
<select
value={searchParams.sortBy}
onChange={(e) => setSearchParams(prev => ({ ...prev, sortBy: e.target.value as any }))}
className="w-full p-3 border border-gray-300 rounded-lg"
>
<option value="relevance">Relevance</option>
<option value="name">Name</option>
<option value="price">Price</option>
<option value="rating">Rating</option>
<option value="date">Date</option>
</select>
</div>
{/* Category */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Category
</label>
<select
value={searchParams.categoryId}
onChange={(e) => setSearchParams(prev => ({ ...prev, categoryId: e.target.value }))}
className="w-full p-3 border border-gray-300 rounded-lg"
>
<option value="">All Categories</option>
{categories?.map((category) => (
<option key={category._id} value={category._id}>
{category.name}
</option>
))}
</select>
</div>
{/* City */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
City
</label>
<select
value={searchParams.cityId}
onChange={(e) => setSearchParams(prev => ({ ...prev, cityId: e.target.value }))}
className="w-full p-3 border border-gray-300 rounded-lg"
>
<option value="">All Cities</option>
{cities?.map((city) => (
<option key={city._id} value={city._id}>
{city.name}
</option>
))}
</select>
</div>
{/* Price Range */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Min Price
</label>
<input
type="number"
value={searchParams.minPrice}
onChange={(e) => setSearchParams(prev => ({ ...prev, minPrice: e.target.value }))}
placeholder="0"
className="w-full p-3 border border-gray-300 rounded-lg"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Max Price
</label>
<input
type="number"
value={searchParams.maxPrice}
onChange={(e) => setSearchParams(prev => ({ ...prev, maxPrice: e.target.value }))}
placeholder="1000"
className="w-full p-3 border border-gray-300 rounded-lg"
/>
</div>
</div>
<div className="mt-6">
<button
onClick={handleSearch}
disabled={!searchParams.query.trim()}
className="w-full bg-blue-600 text-white py-3 px-4 rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
Search
</button>
</div>
</div>
);
}
export default AdvancedSearchForm;
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
Empty search query
Empty search query
Status Code:
400{
"error": "Search query cannot be empty",
"minLength": 1
}
Invalid search type
Invalid search type
Status Code:
400{
"error": "Invalid search type",
"validTypes": ["all", "customers", "stores", "products"]
}
Search service unavailable
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.
