Skip to main content

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
Performs fast search with limited results, perfect for autocomplete functionality.
query
string
required
Search query string
limit
number
Maximum results to return (default: 10)
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" }
  ]
}
Performs full search with pagination and detailed results.
query
string
required
Search query string
type
'all' | 'customers' | 'stores' | 'products'
Filter by entity type (default: “all”)
categoryId
Id<'categories'>
Optional category filter applied to stores/products
paginationOpts
PaginationOptions
required
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"
}
Performs complex search with multiple filters and sorting options.
query
string
required
Search query string
filters
object
Advanced filter options:
paginationOpts
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
  }
});
{
  "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.
searchTerm
string
required
Search query text
searchType
'all' | 'customers' | 'stores' | 'products'
required
Search type that was used
resultCount
number
required
Number of results returned for this search
await convex.mutation(api.shared.search.saveSearchHistory, {
  searchTerm: "italian pizza",
  searchType: "products",
  resultCount: 42
});
null

Get Search History

Retrieve user’s search history.
limit
number
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);
null

Get Friends List

Get all mutual friends (customers who follow each other bidirectionally).
paginationOpts
PaginationOptions
required
Pagination configuration
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).
query
string
required
Search query to find among mutual friends
paginationOpts
PaginationOptions
required
Pagination configuration
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.
limit
number
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 functionality with sample results (development/debugging).
query
string
required
Search query to test
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:
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"]
}
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.