+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 455 of 355

๐Ÿ” Building a Real-Time Search Feature with TypeScript and Astro

Master the art of building blazing-fast search functionality with TypeScript, React, and Astro's API routes ๐Ÿš€

๐Ÿ’ŽAdvanced
45 min read

Prerequisites

  • Strong TypeScript fundamentals ๐Ÿ“
  • React hooks knowledge (useState, useCallback, useMemo) โš›๏ธ
  • Basic understanding of Astro ๐Ÿš€
  • Familiarity with REST APIs ๐ŸŒ

What you'll learn

  • Build a performant real-time search interface ๐Ÿ”
  • Implement TypeScript interfaces for search results ๐Ÿ“Š
  • Create keyboard navigation and accessibility features โŒจ๏ธ
  • Optimize search performance with debouncing and caching ๐Ÿš€

๐ŸŽฏ Introduction

Welcome to this exciting tutorial on building a real-time search feature! ๐ŸŽ‰ In this guide, weโ€™ll create a powerful search system that can instantly find content across your entire application.

Youโ€™ll discover how TypeScript, React, and Astro work together to create a search experience thatโ€™s not just functional, but delightful to use. Whether youโ€™re building a documentation site ๐Ÿ“š, a blog platform ๐Ÿ“, or an e-commerce store ๐Ÿ›’, understanding how to implement fast, type-safe search is essential!

By the end of this tutorial, youโ€™ll have built a production-ready search feature with autocomplete, keyboard navigation, and beautiful animations! Letโ€™s dive in! ๐ŸŠโ€โ™‚๏ธ

๐Ÿค” What Makes Search โ€œReal-Timeโ€?

Real-time search is like having a super-smart assistant ๐Ÿค– who starts looking for what you want the moment you start typing. Think of it as the difference between sending a letter (traditional search) and having a live conversation (real-time search)!

In TypeScript terms, real-time search means:

  • โœจ Instant feedback as users type
  • ๐Ÿš€ Debounced API calls for performance
  • ๐Ÿ›ก๏ธ Type-safe data flow from API to UI
  • โšก Optimized rendering with React hooks

Hereโ€™s why TypeScript makes search features amazing:

  1. Type Safety ๐Ÿ”’: Know exactly what data youโ€™re getting
  2. Autocomplete ๐Ÿ’ป: IDE helps you build faster
  3. Refactoring Confidence ๐Ÿ”ง: Change without breaking
  4. Self-Documenting ๐Ÿ“–: Types explain the data flow

Real-world example: Imagine searching for products ๐Ÿ›’. With TypeScript, you know exactly what fields each product has, preventing those dreaded โ€œundefinedโ€ errors!

๐Ÿ”ง Basic Search Architecture

๐Ÿ“ Setting Up Our Types

Letโ€™s start by defining our search systemโ€™s types:

// ๐ŸŽฏ Define what a search result looks like
interface SearchResult {
  id: string;              // ๐Ÿ†” Unique identifier
  title: string;           // ๐Ÿ“ Main title
  description: string;     // ๐Ÿ“„ Brief description
  url: string;             // ๐Ÿ”— Where to navigate
  type: 'blog' | 'tutorial' | 'product';  // ๐Ÿท๏ธ Content type
  relevanceScore: number;  // ๐ŸŽฏ How well it matches (0-100)
  metadata?: {             // ๐Ÿ“Š Optional extra data
    author?: string;
    publishDate?: string;
    category?: string;
    tags?: string[];
  };
}

// ๐ŸŒ API response structure
interface SearchResponse {
  success: boolean;        // โœ… Did it work?
  data: SearchResult[];    // ๐Ÿ“Š The results
  query: string;           // ๐Ÿ” What was searched
  total: number;           // ๐Ÿ“ˆ Total matches found
  executionTime?: number;  // โฑ๏ธ How long it took
}

// ๐ŸŽจ Search UI state
interface SearchState {
  query: string;           // ๐Ÿ’ฌ Current search text
  results: SearchResult[]; // ๐Ÿ“‹ Current results
  isLoading: boolean;      // โณ Loading indicator
  error: string | null;    // โš ๏ธ Error messages
  selectedIndex: number;   // ๐Ÿ‘† Keyboard navigation
}

๐Ÿ’ก Explanation: Notice how we use union types for type and optional properties with ?. This gives us flexibility while maintaining type safety!

๐ŸŽฏ Creating the Search Hook

Hereโ€™s a custom React hook for search functionality:

// ๐Ÿช Custom hook for search logic
import { useState, useCallback, useRef, useEffect } from 'react';

function useSearch(debounceMs = 300) {
  // ๐ŸŽจ State management
  const [state, setState] = useState<SearchState>({
    query: '',
    results: [],
    isLoading: false,
    error: null,
    selectedIndex: -1
  });

  // ๐Ÿ“Œ Refs for cleanup
  const abortControllerRef = useRef<AbortController | null>(null);
  const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);

  // ๐Ÿ” The search function
  const performSearch = useCallback(async (searchQuery: string) => {
    // Cancel previous request
    if (abortControllerRef.current) {
      abortControllerRef.current.abort();
    }

    // Create new abort controller
    abortControllerRef.current = new AbortController();

    setState(prev => ({ ...prev, isLoading: true, error: null }));

    try {
      const response = await fetch(
        `/api/search?q=${encodeURIComponent(searchQuery)}`,
        { signal: abortControllerRef.current.signal }
      );

      if (!response.ok) {
        throw new Error(`Search failed: ${response.status}`);
      }

      const data: SearchResponse = await response.json();

      if (data.success) {
        setState(prev => ({
          ...prev,
          results: data.data,
          isLoading: false
        }));
      } else {
        throw new Error('Search returned unsuccessful');
      }
    } catch (error) {
      // Ignore aborted requests
      if (error instanceof Error && error.name !== 'AbortError') {
        setState(prev => ({
          ...prev,
          error: error.message,
          isLoading: false
        }));
      }
    }
  }, []);

  // ๐ŸŽฏ Debounced search
  const search = useCallback((query: string) => {
    setState(prev => ({ ...prev, query }));

    // Clear existing timer
    if (debounceTimerRef.current) {
      clearTimeout(debounceTimerRef.current);
    }

    // Don't search for very short queries
    if (query.trim().length < 2) {
      setState(prev => ({ ...prev, results: [], error: null }));
      return;
    }

    // Debounce the search
    debounceTimerRef.current = setTimeout(() => {
      performSearch(query);
    }, debounceMs);
  }, [performSearch, debounceMs]);

  // ๐Ÿงน Cleanup on unmount
  useEffect(() => {
    return () => {
      if (debounceTimerRef.current) {
        clearTimeout(debounceTimerRef.current);
      }
      if (abortControllerRef.current) {
        abortControllerRef.current.abort();
      }
    };
  }, []);

  return { state, search };
}

๐Ÿ’ก Building the Search UI

๐Ÿ›’ Example 1: Search Modal Component

Letโ€™s build a beautiful search modal:

// ๐ŸŽจ Search Modal with all the bells and whistles!
import React, { useCallback, useEffect, memo } from 'react';

interface SearchModalProps {
  isOpen: boolean;
  onClose: () => void;
}

export const SearchModal: React.FC<SearchModalProps> = memo(({ 
  isOpen, 
  onClose 
}) => {
  const { state, search } = useSearch(300);
  const inputRef = useRef<HTMLInputElement>(null);

  // ๐ŸŽฏ Handle keyboard navigation
  const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
    switch (e.key) {
      case 'Escape':
        onClose();
        break;
      case 'ArrowDown':
        e.preventDefault();
        setState(prev => ({
          ...prev,
          selectedIndex: Math.min(
            prev.selectedIndex + 1, 
            prev.results.length - 1
          )
        }));
        break;
      case 'ArrowUp':
        e.preventDefault();
        setState(prev => ({
          ...prev,
          selectedIndex: Math.max(prev.selectedIndex - 1, -1)
        }));
        break;
      case 'Enter':
        e.preventDefault();
        if (state.selectedIndex >= 0) {
          const selected = state.results[state.selectedIndex];
          if (selected) {
            window.location.href = selected.url;
          }
        }
        break;
    }
  }, [state.selectedIndex, state.results, onClose]);

  // ๐ŸŽฏ Focus input when modal opens
  useEffect(() => {
    if (isOpen && inputRef.current) {
      inputRef.current.focus();
    }
  }, [isOpen]);

  // ๐Ÿšซ Don't render if not open
  if (!isOpen) return null;

  return (
    <div className="search-modal-overlay" onClick={onClose}>
      <div className="search-modal" onClick={e => e.stopPropagation()}>
        {/* ๐Ÿ” Search Input */}
        <div className="search-input-wrapper">
          <span className="search-icon">๐Ÿ”</span>
          <input
            ref={inputRef}
            type="text"
            placeholder="Search for anything..."
            value={state.query}
            onChange={e => search(e.target.value)}
            onKeyDown={handleKeyDown}
            className="search-input"
          />
          {state.isLoading && <span className="loading-spinner">โณ</span>}
        </div>

        {/* ๐Ÿ“Š Search Results */}
        <div className="search-results">
          {state.error && (
            <div className="error-message">
              โš ๏ธ {state.error}
            </div>
          )}

          {state.results.length === 0 && state.query.length >= 2 && !state.isLoading && (
            <div className="no-results">
              ๐Ÿ˜” No results found for "{state.query}"
            </div>
          )}

          {state.results.map((result, index) => (
            <SearchResultItem
              key={result.id}
              result={result}
              isSelected={index === state.selectedIndex}
              onClick={() => window.location.href = result.url}
            />
          ))}
        </div>

        {/* โŒจ๏ธ Keyboard shortcuts help */}
        <div className="search-footer">
          <span>โ†‘โ†“ Navigate</span>
          <span>โ†ต Select</span>
          <span>ESC Close</span>
        </div>
      </div>
    </div>
  );
});

// ๐ŸŽฏ Individual search result component
const SearchResultItem: React.FC<{
  result: SearchResult;
  isSelected: boolean;
  onClick: () => void;
}> = memo(({ result, isSelected, onClick }) => {
  const getTypeEmoji = (type: SearchResult['type']) => {
    switch (type) {
      case 'blog': return '๐Ÿ“';
      case 'tutorial': return '๐Ÿ“š';
      case 'product': return '๐Ÿ›’';
      default: return '๐Ÿ“„';
    }
  };

  return (
    <div 
      className={`search-result ${isSelected ? 'selected' : ''}`}
      onClick={onClick}
    >
      <span className="result-emoji">{getTypeEmoji(result.type)}</span>
      <div className="result-content">
        <h3>{result.title}</h3>
        <p>{result.description}</p>
        {result.metadata?.tags && (
          <div className="result-tags">
            {result.metadata.tags.map(tag => (
              <span key={tag} className="tag">#{tag}</span>
            ))}
          </div>
        )}
      </div>
      <span className="relevance-score">
        {result.relevanceScore}% match
      </span>
    </div>
  );
});

๐ŸŽฎ Example 2: Astro API Route

Letโ€™s create the backend search API:

// ๐Ÿ“„ src/pages/api/search.ts
import type { APIRoute } from 'astro';

// ๐ŸŽฏ Search algorithm with scoring
export const GET: APIRoute = async ({ url }) => {
  const query = url.searchParams.get('q');
  const limit = parseInt(url.searchParams.get('limit') || '10');

  // ๐Ÿ›ก๏ธ Input validation
  if (!query || query.trim().length < 2) {
    return new Response(JSON.stringify({
      success: false,
      error: 'Query must be at least 2 characters',
      data: [],
      query: query || '',
      total: 0
    }), {
      status: 400,
      headers: { 'Content-Type': 'application/json' }
    });
  }

  try {
    // ๐Ÿš€ Perform search (example implementation)
    const startTime = performance.now();
    const results = await searchContent(query.trim(), limit);
    const executionTime = performance.now() - startTime;

    return new Response(JSON.stringify({
      success: true,
      data: results,
      query: query.trim(),
      total: results.length,
      executionTime: Math.round(executionTime)
    }), {
      status: 200,
      headers: {
        'Content-Type': 'application/json',
        'Cache-Control': 'public, max-age=60' // Cache for 1 minute
      }
    });
  } catch (error) {
    console.error('Search error:', error);
    return new Response(JSON.stringify({
      success: false,
      error: 'Search failed',
      data: [],
      query: query.trim(),
      total: 0
    }), {
      status: 500,
      headers: { 'Content-Type': 'application/json' }
    });
  }
};

// ๐Ÿ” Scoring algorithm
async function searchContent(
  query: string, 
  limit: number
): Promise<SearchResult[]> {
  const searchTerms = query.toLowerCase().split(' ').filter(Boolean);
  
  // Get all content (this would come from your database)
  const allContent = await getAllContent();
  
  // Score each item
  const scoredResults = allContent.map(item => {
    let score = 0;
    
    // Title matches (highest weight)
    searchTerms.forEach(term => {
      if (item.title.toLowerCase().includes(term)) {
        score += 10;
        // Exact match bonus
        if (item.title.toLowerCase() === query.toLowerCase()) {
          score += 20;
        }
      }
    });
    
    // Description matches
    searchTerms.forEach(term => {
      if (item.description.toLowerCase().includes(term)) {
        score += 5;
      }
    });
    
    // Tag matches
    item.tags?.forEach(tag => {
      searchTerms.forEach(term => {
        if (tag.toLowerCase().includes(term)) {
          score += 3;
        }
      });
    });
    
    return {
      ...item,
      relevanceScore: Math.min(100, score * 2) // Normalize to 0-100
    };
  });
  
  // Filter and sort
  return scoredResults
    .filter(result => result.relevanceScore > 0)
    .sort((a, b) => b.relevanceScore - a.relevanceScore)
    .slice(0, limit);
}

๐Ÿš€ Advanced Search Features

๐Ÿง™โ€โ™‚๏ธ Fuzzy Search Implementation

When youโ€™re ready to level up, try fuzzy matching:

// ๐ŸŽฏ Advanced fuzzy search with Levenshtein distance
type FuzzySearchOptions = {
  threshold: number;      // ๐ŸŽš๏ธ Matching threshold (0-1)
  caseSensitive: boolean; // ๐Ÿ”ค Case sensitivity
  sortByRelevance: boolean; // ๐Ÿ“Š Sort results
};

class FuzzySearcher<T extends Record<string, any>> {
  private items: T[];
  private searchableKeys: (keyof T)[];
  private options: FuzzySearchOptions;

  constructor(
    items: T[], 
    searchableKeys: (keyof T)[], 
    options: Partial<FuzzySearchOptions> = {}
  ) {
    this.items = items;
    this.searchableKeys = searchableKeys;
    this.options = {
      threshold: 0.6,
      caseSensitive: false,
      sortByRelevance: true,
      ...options
    };
  }

  // ๐Ÿ” Main search method
  search(query: string): Array<T & { score: number }> {
    if (!query) return [];

    const results = this.items.map(item => {
      const score = this.calculateItemScore(item, query);
      return { ...item, score };
    }).filter(item => item.score >= this.options.threshold);

    if (this.options.sortByRelevance) {
      results.sort((a, b) => b.score - a.score);
    }

    return results;
  }

  // ๐ŸŽฏ Calculate relevance score
  private calculateItemScore(item: T, query: string): number {
    let maxScore = 0;

    for (const key of this.searchableKeys) {
      const value = String(item[key] || '');
      const score = this.fuzzyMatch(
        this.options.caseSensitive ? value : value.toLowerCase(),
        this.options.caseSensitive ? query : query.toLowerCase()
      );
      maxScore = Math.max(maxScore, score);
    }

    return maxScore;
  }

  // ๐Ÿช„ Fuzzy matching algorithm
  private fuzzyMatch(text: string, query: string): number {
    if (text === query) return 1; // Perfect match
    if (text.includes(query)) return 0.9; // Contains match

    // Calculate similarity
    const distance = this.levenshteinDistance(text, query);
    const maxLength = Math.max(text.length, query.length);
    
    return 1 - (distance / maxLength);
  }

  // ๐Ÿ“ Levenshtein distance calculation
  private levenshteinDistance(a: string, b: string): number {
    const matrix: number[][] = [];

    // Initialize matrix
    for (let i = 0; i <= b.length; i++) {
      matrix[i] = [i];
    }
    for (let j = 0; j <= a.length; j++) {
      matrix[0][j] = j;
    }

    // Fill matrix
    for (let i = 1; i <= b.length; i++) {
      for (let j = 1; j <= a.length; j++) {
        if (b.charAt(i - 1) === a.charAt(j - 1)) {
          matrix[i][j] = matrix[i - 1][j - 1];
        } else {
          matrix[i][j] = Math.min(
            matrix[i - 1][j - 1] + 1, // substitution
            matrix[i][j - 1] + 1,     // insertion
            matrix[i - 1][j] + 1      // deletion
          );
        }
      }
    }

    return matrix[b.length][a.length];
  }
}

// ๐Ÿš€ Using the fuzzy searcher
const products = [
  { id: '1', name: 'TypeScript Handbook', category: 'Books' },
  { id: '2', name: 'JavaScript Guide', category: 'Books' },
  { id: '3', name: 'Type Safety Course', category: 'Courses' }
];

const searcher = new FuzzySearcher(products, ['name', 'category'], {
  threshold: 0.5,
  caseSensitive: false
});

// Will find "TypeScript Handbook" even with typo!
const results = searcher.search('typscript');

๐Ÿ—๏ธ Search Result Caching

For ultimate performance, implement caching:

// ๐Ÿš€ Advanced caching system
class SearchCache {
  private cache = new Map<string, {
    results: SearchResult[];
    timestamp: number;
  }>();
  private maxAge: number; // milliseconds
  private maxSize: number;

  constructor(maxAge = 5 * 60 * 1000, maxSize = 100) {
    this.maxAge = maxAge;
    this.maxSize = maxSize;
  }

  // ๐Ÿ“ฅ Get cached results
  get(query: string): SearchResult[] | null {
    const cached = this.cache.get(query);
    
    if (!cached) return null;
    
    // Check if expired
    if (Date.now() - cached.timestamp > this.maxAge) {
      this.cache.delete(query);
      return null;
    }
    
    return cached.results;
  }

  // ๐Ÿ“ค Store results
  set(query: string, results: SearchResult[]): void {
    // Enforce size limit
    if (this.cache.size >= this.maxSize) {
      // Remove oldest entry
      const oldestKey = Array.from(this.cache.entries())
        .sort(([, a], [, b]) => a.timestamp - b.timestamp)[0][0];
      this.cache.delete(oldestKey);
    }
    
    this.cache.set(query, {
      results,
      timestamp: Date.now()
    });
  }

  // ๐Ÿงน Clear cache
  clear(): void {
    this.cache.clear();
  }

  // ๐Ÿ“Š Get cache stats
  getStats(): {
    size: number;
    queries: string[];
    oldestEntry: number | null;
  } {
    const entries = Array.from(this.cache.entries());
    
    return {
      size: this.cache.size,
      queries: entries.map(([query]) => query),
      oldestEntry: entries.length > 0 
        ? Math.min(...entries.map(([, data]) => data.timestamp))
        : null
    };
  }
}

// ๐ŸŽฏ Using cache in search hook
function useSearchWithCache(debounceMs = 300) {
  const cacheRef = useRef(new SearchCache());
  
  const performSearch = useCallback(async (query: string) => {
    // Check cache first
    const cached = cacheRef.current.get(query);
    if (cached) {
      setState(prev => ({
        ...prev,
        results: cached,
        isLoading: false
      }));
      return;
    }
    
    // Perform search...
    const results = await fetchSearchResults(query);
    
    // Cache the results
    cacheRef.current.set(query, results);
    
    // Update state...
  }, []);
}

โš ๏ธ Common Pitfalls and Solutions

๐Ÿ˜ฑ Pitfall 1: The Race Condition Trap

// โŒ Wrong way - race conditions galore!
const BadSearch = () => {
  const [results, setResults] = useState([]);
  
  const search = async (query: string) => {
    const data = await fetch(`/api/search?q=${query}`);
    setResults(data); // ๐Ÿ’ฅ What if user typed something else?
  };
};

// โœ… Correct way - handle race conditions!
const GoodSearch = () => {
  const [results, setResults] = useState([]);
  const latestQueryRef = useRef('');
  
  const search = async (query: string) => {
    latestQueryRef.current = query;
    const data = await fetch(`/api/search?q=${query}`);
    
    // Only update if this is still the latest query
    if (query === latestQueryRef.current) {
      setResults(data); // โœ… Safe from race conditions!
    }
  };
};
// โŒ Dangerous - memory leak waiting to happen!
const LeakySearch = () => {
  const [results, setResults] = useState<SearchResult[]>([]);
  
  useEffect(() => {
    const interval = setInterval(async () => {
      const data = await fetchPopularSearches();
      setResults(prev => [...prev, ...data]); // ๐Ÿ’ฅ Grows forever!
    }, 5000);
    
    // Forgot to clean up! ๐Ÿ˜ฑ
  }, []);
};

// โœ… Safe - proper cleanup and limits!
const SafeSearch = () => {
  const [results, setResults] = useState<SearchResult[]>([]);
  const maxResults = 100;
  
  useEffect(() => {
    const interval = setInterval(async () => {
      const data = await fetchPopularSearches();
      setResults(prev => {
        const combined = [...prev, ...data];
        return combined.slice(-maxResults); // โœ… Limit size!
      });
    }, 5000);
    
    return () => clearInterval(interval); // โœ… Clean up!
  }, []);
};

๐Ÿ› ๏ธ Best Practices

  1. ๐ŸŽฏ Debounce Wisely: 200-300ms is the sweet spot
  2. ๐Ÿ“ Clear Types: Define all data structures upfront
  3. ๐Ÿ›ก๏ธ Handle Errors: Always show user-friendly messages
  4. ๐ŸŽจ Visual Feedback: Loading states are crucial
  5. โœจ Keyboard Support: Make it fully navigable
  6. ๐Ÿš€ Cache Smart: Balance freshness vs performance
  7. ๐Ÿ“Š Track Metrics: Measure search performance

๐Ÿงช Hands-On Exercise

๐ŸŽฏ Challenge: Build an Advanced Search System

Create a search system with these features:

๐Ÿ“‹ Requirements:

  • โœ… Fuzzy matching for typos
  • ๐Ÿท๏ธ Filter by content type
  • ๐Ÿ‘ค Search history with localStorage
  • ๐Ÿ“… Date range filtering
  • ๐ŸŽจ Highlighted search terms in results!

๐Ÿš€ Bonus Points:

  • Voice search integration
  • Search analytics tracking
  • Multi-language support
  • Instant search suggestions

๐Ÿ’ก Solution

๐Ÿ” Click to see solution
// ๐ŸŽฏ Complete advanced search implementation!
interface AdvancedSearchOptions {
  query: string;
  filters: {
    type?: string[];
    dateFrom?: Date;
    dateTo?: Date;
    tags?: string[];
  };
  fuzzyMatch: boolean;
  highlightTerms: boolean;
}

class AdvancedSearchSystem {
  private searchHistory: string[] = [];
  private fuzzySearcher: FuzzySearcher<any>;
  private cache = new SearchCache();
  
  constructor() {
    // Load search history
    this.loadSearchHistory();
    
    // Initialize fuzzy searcher
    this.fuzzySearcher = new FuzzySearcher([], ['title', 'description']);
  }
  
  // ๐Ÿ” Main search method
  async search(options: AdvancedSearchOptions): Promise<SearchResult[]> {
    // Save to history
    this.addToHistory(options.query);
    
    // Check cache
    const cacheKey = JSON.stringify(options);
    const cached = this.cache.get(cacheKey);
    if (cached) return cached;
    
    // Perform search
    let results = await this.performSearch(options);
    
    // Apply filters
    results = this.applyFilters(results, options.filters);
    
    // Highlight terms if requested
    if (options.highlightTerms) {
      results = this.highlightSearchTerms(results, options.query);
    }
    
    // Cache results
    this.cache.set(cacheKey, results);
    
    return results;
  }
  
  // ๐ŸŽฏ Fuzzy search implementation
  private async performSearch(
    options: AdvancedSearchOptions
  ): Promise<SearchResult[]> {
    if (options.fuzzyMatch) {
      // Use fuzzy searcher
      const allContent = await this.getAllContent();
      this.fuzzySearcher = new FuzzySearcher(
        allContent, 
        ['title', 'description']
      );
      return this.fuzzySearcher.search(options.query);
    } else {
      // Regular search
      return this.regularSearch(options.query);
    }
  }
  
  // ๐Ÿท๏ธ Apply filters
  private applyFilters(
    results: SearchResult[], 
    filters: AdvancedSearchOptions['filters']
  ): SearchResult[] {
    return results.filter(result => {
      // Type filter
      if (filters.type?.length && !filters.type.includes(result.type)) {
        return false;
      }
      
      // Date range filter
      if (filters.dateFrom || filters.dateTo) {
        const resultDate = new Date(result.metadata?.publishDate || '');
        if (filters.dateFrom && resultDate < filters.dateFrom) return false;
        if (filters.dateTo && resultDate > filters.dateTo) return false;
      }
      
      // Tag filter
      if (filters.tags?.length) {
        const resultTags = result.metadata?.tags || [];
        const hasMatchingTag = filters.tags.some(tag => 
          resultTags.includes(tag)
        );
        if (!hasMatchingTag) return false;
      }
      
      return true;
    });
  }
  
  // โœจ Highlight search terms
  private highlightSearchTerms(
    results: SearchResult[], 
    query: string
  ): SearchResult[] {
    const terms = query.toLowerCase().split(' ').filter(Boolean);
    
    return results.map(result => ({
      ...result,
      title: this.highlightText(result.title, terms),
      description: this.highlightText(result.description, terms)
    }));
  }
  
  private highlightText(text: string, terms: string[]): string {
    let highlighted = text;
    
    terms.forEach(term => {
      const regex = new RegExp(`(${term})`, 'gi');
      highlighted = highlighted.replace(
        regex, 
        '<mark class="search-highlight">$1</mark>'
      );
    });
    
    return highlighted;
  }
  
  // ๐Ÿ“š Search history management
  private addToHistory(query: string): void {
    if (!query.trim()) return;
    
    // Remove if already exists
    this.searchHistory = this.searchHistory.filter(q => q !== query);
    
    // Add to beginning
    this.searchHistory.unshift(query);
    
    // Limit to 10 items
    this.searchHistory = this.searchHistory.slice(0, 10);
    
    // Save to localStorage
    this.saveSearchHistory();
  }
  
  private loadSearchHistory(): void {
    try {
      const saved = localStorage.getItem('searchHistory');
      if (saved) {
        this.searchHistory = JSON.parse(saved);
      }
    } catch (error) {
      console.error('Failed to load search history:', error);
    }
  }
  
  private saveSearchHistory(): void {
    try {
      localStorage.setItem(
        'searchHistory', 
        JSON.stringify(this.searchHistory)
      );
    } catch (error) {
      console.error('Failed to save search history:', error);
    }
  }
  
  // ๐ŸŽค Voice search support
  async startVoiceSearch(): Promise<string> {
    return new Promise((resolve, reject) => {
      if (!('webkitSpeechRecognition' in window)) {
        reject(new Error('Speech recognition not supported'));
        return;
      }
      
      const recognition = new (window as any).webkitSpeechRecognition();
      recognition.lang = 'en-US';
      recognition.onresult = (event: any) => {
        const transcript = event.results[0][0].transcript;
        resolve(transcript);
      };
      recognition.onerror = reject;
      recognition.start();
    });
  }
}

// ๐ŸŽฎ Using the advanced search system
const searchSystem = new AdvancedSearchSystem();

// Example search
const results = await searchSystem.search({
  query: 'typescript tutorial',
  filters: {
    type: ['tutorial'],
    dateFrom: new Date('2024-01-01'),
    tags: ['beginner', 'intermediate']
  },
  fuzzyMatch: true,
  highlightTerms: true
});

console.log('๐Ÿ” Found', results.length, 'results!');

๐ŸŽ“ Key Takeaways

Youโ€™ve learned so much! Hereโ€™s what you can now do:

  • โœ… Build real-time search with TypeScript and React ๐Ÿ’ช
  • โœ… Implement fuzzy matching for typo-tolerant search ๐Ÿ›ก๏ธ
  • โœ… Create accessible interfaces with keyboard navigation ๐ŸŽฏ
  • โœ… Optimize performance with debouncing and caching ๐Ÿš€
  • โœ… Handle edge cases like race conditions properly ๐Ÿ›

Remember: Great search isnโ€™t just about finding thingsโ€”itโ€™s about creating an experience that feels magical! ๐Ÿช„

๐Ÿค Next Steps

Congratulations! ๐ŸŽ‰ Youโ€™ve mastered building real-time search with TypeScript!

Hereโ€™s what to do next:

  1. ๐Ÿ’ป Build the search system from the exercise
  2. ๐Ÿ—๏ธ Add it to your own project
  3. ๐Ÿ“š Explore full-text search databases like Elasticsearch
  4. ๐ŸŒŸ Share your implementation with the community!

Remember: Every great app started with someone typing into a search box. Now you can build that experience too! Keep coding, keep searching, and most importantly, have fun! ๐Ÿš€


Happy searching! ๐Ÿ”๐ŸŽ‰โœจ