Search & Filter Implementation Implement search and filter interfaces with comprehensive frontend components and backend query optimization. Purpose This skill provides production-ready patterns for implementing search and filtering functionality across the full stack. It covers React/TypeScript components for the frontend (search inputs, filter UIs, autocomplete) and Python patterns for the backend (SQLAlchemy queries, Elasticsearch integration, API design). The skill emphasizes performance optimization, accessibility, and user experience. When to Use - Building product search with category…

)\n page: int = Field(1, ge=1, le=100)\n size: int = Field(20, ge=1, le=100)\n include_facets: bool = Field(True, description=\"Include facet counts\")\n\nclass SearchResponse(BaseModel):\n \"\"\"Search response structure.\"\"\"\n total: int\n page: int\n size: int\n items: List[Dict[str, Any]]\n facets: Optional[Dict[str, List[Dict]]] = None\n query_time_ms: float\n\n class Config:\n json_schema_extra = {\n \"example\": {\n \"total\": 150,\n \"page\": 1,\n \"size\": 20,\n \"items\": [...],\n \"facets\": {\n \"category\": [\n {\"value\": \"Electronics\", \"count\": 45},\n {\"value\": \"Books\", \"count\": 32}\n ]\n },\n \"query_time_ms\": 125.5\n }\n }\n\n# GET endpoint for simple searches\[email protected](\"/api/v1/search\", response_model=SearchResponse)\nasync def search_get(\n q: Optional[str] = Query(None, min_length=1, max_length=200, description=\"Search query\"),\n category: Optional[List[str]] = Query(None, description=\"Categories filter\"),\n brand: Optional[List[str]] = Query(None, description=\"Brands filter\"),\n min_price: Optional[float] = Query(None, ge=0, description=\"Minimum price\"),\n max_price: Optional[float] = Query(None, ge=0, description=\"Maximum price\"),\n in_stock: Optional[bool] = Query(None, description=\"Stock filter\"),\n sort: str = Query('relevance', regex='^(relevance|price_asc|price_desc|newest|rating)

Search & Filter Implementation Implement search and filter interfaces with comprehensive frontend components and backend query optimization. Purpose This skill provides production-ready patterns for implementing search and filtering functionality across the full stack. It covers React/TypeScript components for the frontend (search inputs, filter UIs, autocomplete) and Python patterns for the backend (SQLAlchemy queries, Elasticsearch integration, API design). The skill emphasizes performance optimization, accessibility, and user experience. When to Use - Building product search with category…

),\n page: int = Query(1, ge=1, le=100),\n size: int = Query(20, ge=1, le=100)\n):\n \"\"\"\n Search products with query and filters.\n\n - **q**: Search query text\n - **category**: Filter by categories (multiple allowed)\n - **brand**: Filter by brands (multiple allowed)\n - **min_price**: Minimum price filter\n - **max_price**: Maximum price filter\n - **in_stock**: Show only in-stock items\n - **sort**: Sort order\n - **page**: Page number (1-based)\n - **size**: Results per page\n \"\"\"\n\n # Build filters\n filters = SearchFilters(\n category=category,\n brand=brand,\n min_price=min_price,\n max_price=max_price,\n in_stock=in_stock\n )\n\n # Execute search\n results = await perform_search(\n query=q,\n filters=filters,\n sort_by=sort,\n page=page,\n size=size\n )\n\n return results\n\n# POST endpoint for complex searches\[email protected](\"/api/v1/search\", response_model=SearchResponse)\nasync def search_post(request: SearchRequest):\n \"\"\"\n Advanced search with complex filters.\n\n Accepts a JSON body with search parameters.\n Useful for complex queries that exceed URL length limits.\n \"\"\"\n\n results = await perform_search(\n query=request.query,\n filters=request.filters,\n sort_by=request.sort_by,\n page=request.page,\n size=request.size,\n include_facets=request.include_facets\n )\n\n return results\n```\n\n### Autocomplete API\n```python\nclass AutocompleteResponse(BaseModel):\n \"\"\"Autocomplete suggestions response.\"\"\"\n suggestions: List[Dict[str, Any]]\n query_time_ms: float\n\[email protected](\"/api/v1/autocomplete\", response_model=AutocompleteResponse)\nasync def autocomplete(\n q: str = Query(..., min_length=2, max_length=50, description=\"Query prefix\"),\n size: int = Query(10, ge=1, le=20, description=\"Number of suggestions\"),\n include_categories: bool = Query(False, description=\"Include category in suggestions\")\n):\n \"\"\"\n Get autocomplete suggestions for search input.\n\n Returns suggestions based on partial query match.\n Optimized for real-time typeahead functionality.\n \"\"\"\n\n import time\n start_time = time.time()\n\n # Get suggestions from search backend\n suggestions = await get_autocomplete_suggestions(\n prefix=q,\n size=size,\n include_categories=include_categories\n )\n\n query_time_ms = (time.time() - start_time) * 1000\n\n return AutocompleteResponse(\n suggestions=suggestions,\n query_time_ms=query_time_ms\n )\n```\n\n## Advanced Query Parameters\n\n### Query DSL Support\n```python\nfrom typing import Union\nimport json\n\nclass QueryDSL(BaseModel):\n \"\"\"Domain Specific Language for complex queries.\"\"\"\n\n # Boolean operators\n must: Optional[List[Union[str, Dict]]] = None\n should: Optional[List[Union[str, Dict]]] = None\n must_not: Optional[List[Union[str, Dict]]] = None\n\n # Field-specific queries\n fields: Optional[Dict[str, Any]] = None\n\n # Advanced options\n fuzzy: Optional[bool] = False\n boost: Optional[Dict[str, float]] = None\n minimum_should_match: Optional[int] = None\n\[email protected](\"/api/v1/search/advanced\")\nasync def advanced_search(\n query_dsl: QueryDSL,\n filters: Optional[SearchFilters] = None,\n page: int = Query(1, ge=1),\n size: int = Query(20, ge=1, le=100)\n):\n \"\"\"\n Execute advanced search with query DSL.\n\n Supports boolean logic, field-specific queries, and boosting.\n\n Example query DSL:\n ```json\n {\n \"must\": [\"laptop\"],\n \"should\": [\"gaming\", \"professional\"],\n \"must_not\": [\"refurbished\"],\n \"fields\": {\n \"brand\": \"dell\",\n \"category\": \"computers\"\n },\n \"fuzzy\": true,\n \"boost\": {\n \"title\": 2.0,\n \"description\": 1.5\n }\n }\n ```\n \"\"\"\n\n # Build and execute complex query\n results = await execute_dsl_query(\n dsl=query_dsl,\n filters=filters,\n page=page,\n size=size\n )\n\n return results\n```\n\n### GraphQL Search Schema\n```python\nimport strawberry\nfrom typing import Optional, List\n\[email protected]\nclass Product:\n id: str\n title: str\n description: str\n price: float\n category: str\n brand: str\n rating: Optional[float]\n in_stock: bool\n\[email protected]\nclass SearchResult:\n total: int\n items: List[Product]\n facets: Optional[str] # JSON string of facets\n\[email protected]\nclass SearchInput:\n query: Optional[str] = None\n category: Optional[List[str]] = None\n min_price: Optional[float] = None\n max_price: Optional[float] = None\n sort_by: str = \"relevance\"\n page: int = 1\n size: int = 20\n\[email protected]\nclass Query:\n @strawberry.field\n async def search(self, input: SearchInput) -> SearchResult:\n \"\"\"GraphQL search endpoint.\"\"\"\n results = await perform_search(\n query=input.query,\n filters={\n 'category': input.category,\n 'min_price': input.min_price,\n 'max_price': input.max_price\n },\n sort_by=input.sort_by,\n page=input.page,\n size=input.size\n )\n\n return SearchResult(\n total=results['total'],\n items=[Product(**item) for item in results['items']],\n facets=json.dumps(results.get('facets', {}))\n )\n\nschema = strawberry.Schema(query=Query)\n```\n\n## Pagination Strategies\n\n### Offset-Based Pagination\n```python\nclass OffsetPagination:\n \"\"\"Traditional offset-based pagination.\"\"\"\n\n @staticmethod\n def paginate(\n query,\n page: int = 1,\n per_page: int = 20,\n max_per_page: int = 100\n ):\n \"\"\"Apply offset pagination to query.\"\"\"\n # Validate inputs\n page = max(1, page)\n per_page = min(max_per_page, max(1, per_page))\n\n # Calculate offset\n offset = (page - 1) * per_page\n\n # Get total count\n total = query.count()\n\n # Apply pagination\n items = query.offset(offset).limit(per_page).all()\n\n # Calculate metadata\n total_pages = (total + per_page - 1) // per_page\n has_next = page \u003c total_pages\n has_prev = page > 1\n\n return {\n 'items': items,\n 'page': page,\n 'per_page': per_page,\n 'total': total,\n 'total_pages': total_pages,\n 'has_next': has_next,\n 'has_prev': has_prev,\n 'next_page': page + 1 if has_next else None,\n 'prev_page': page - 1 if has_prev else None\n }\n```\n\n### Cursor-Based Pagination\n```python\nimport base64\nfrom datetime import datetime\n\nclass CursorPagination:\n \"\"\"Cursor-based pagination for real-time data.\"\"\"\n\n @staticmethod\n def encode_cursor(position: Dict) -> str:\n \"\"\"Encode position as cursor.\"\"\"\n cursor_data = json.dumps(position, default=str)\n return base64.b64encode(cursor_data.encode()).decode()\n\n @staticmethod\n def decode_cursor(cursor: str) -> Dict:\n \"\"\"Decode cursor to position.\"\"\"\n try:\n cursor_data = base64.b64decode(cursor.encode()).decode()\n return json.loads(cursor_data)\n except:\n raise ValueError(\"Invalid cursor\")\n\n @staticmethod\n def paginate_with_cursor(\n query,\n cursor: Optional[str] = None,\n limit: int = 20,\n order_by: str = 'created_at'\n ):\n \"\"\"Apply cursor pagination.\"\"\"\n\n # Decode cursor if provided\n if cursor:\n position = CursorPagination.decode_cursor(cursor)\n query = query.filter(\n getattr(Product, order_by) > position[order_by]\n )\n\n # Order and limit\n query = query.order_by(getattr(Product, order_by))\n items = query.limit(limit + 1).all()\n\n # Check if there are more items\n has_next = len(items) > limit\n if has_next:\n items = items[:-1] # Remove extra item\n\n # Create next cursor\n next_cursor = None\n if items and has_next:\n last_item = items[-1]\n next_cursor = CursorPagination.encode_cursor({\n order_by: getattr(last_item, order_by)\n })\n\n return {\n 'items': items,\n 'next_cursor': next_cursor,\n 'has_next': has_next\n }\n\[email protected](\"/api/v1/search/cursor\")\nasync def search_with_cursor(\n q: Optional[str] = None,\n cursor: Optional[str] = None,\n limit: int = Query(20, ge=1, le=100)\n):\n \"\"\"Search with cursor-based pagination.\"\"\"\n\n results = await search_with_cursor_pagination(\n query=q,\n cursor=cursor,\n limit=limit\n )\n\n return results\n```\n\n## Rate Limiting and Caching\n\n### API Rate Limiting\n```python\nfrom slowapi import Limiter, _rate_limit_exceeded_handler\nfrom slowapi.util import get_remote_address\nfrom slowapi.errors import RateLimitExceeded\n\n# Create limiter\nlimiter = Limiter(\n key_func=get_remote_address,\n default_limits=[\"100/minute\"]\n)\n\napp.state.limiter = limiter\napp.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)\n\[email protected](\"/api/v1/search\")\[email protected](\"10/second\") # More restrictive limit for search\nasync def search_rate_limited(\n request: Request,\n q: str = Query(...),\n page: int = 1,\n size: int = 20\n):\n \"\"\"Rate-limited search endpoint.\"\"\"\n return await perform_search(q, page, size)\n```\n\n### Response Caching\n```python\nfrom fastapi_cache import FastAPICache\nfrom fastapi_cache.decorator import cache\nfrom fastapi_cache.backends.redis import RedisBackend\nimport redis\n\n# Initialize cache on startup\[email protected]_event(\"startup\")\nasync def startup():\n redis_client = redis.Redis(host=\"localhost\", port=6379)\n FastAPICache.init(RedisBackend(redis_client), prefix=\"search-cache:\")\n\[email protected](\"/api/v1/search\")\n@cache(expire=300) # Cache for 5 minutes\nasync def cached_search(\n q: str,\n category: Optional[List[str]] = None,\n page: int = 1,\n size: int = 20\n):\n \"\"\"Cached search endpoint.\"\"\"\n\n # Cache key includes all parameters\n results = await perform_search(\n query=q,\n filters={'category': category},\n page=page,\n size=size\n )\n\n return results\n\n# Custom cache key generation\ndef get_cache_key(func, *args, **kwargs):\n \"\"\"Generate cache key from search parameters.\"\"\"\n params = {\n 'query': kwargs.get('q'),\n 'filters': kwargs.get('filters'),\n 'page': kwargs.get('page', 1),\n 'size': kwargs.get('size', 20)\n }\n\n # Sort and hash parameters\n import hashlib\n param_str = json.dumps(params, sort_keys=True)\n return f\"search:{hashlib.md5(param_str.encode()).hexdigest()}\"\n```\n\n## Error Handling\n\n### Comprehensive Error Responses\n```python\nfrom enum import Enum\n\nclass ErrorCode(str, Enum):\n INVALID_QUERY = \"INVALID_QUERY\"\n INVALID_FILTERS = \"INVALID_FILTERS\"\n SERVICE_UNAVAILABLE = \"SERVICE_UNAVAILABLE\"\n RATE_LIMITED = \"RATE_LIMITED\"\n INTERNAL_ERROR = \"INTERNAL_ERROR\"\n\nclass ErrorResponse(BaseModel):\n error: ErrorCode\n message: str\n details: Optional[Dict] = None\n timestamp: datetime = Field(default_factory=datetime.utcnow)\n\[email protected]_handler(ValueError)\nasync def value_error_handler(request: Request, exc: ValueError):\n return JSONResponse(\n status_code=400,\n content=ErrorResponse(\n error=ErrorCode.INVALID_QUERY,\n message=str(exc),\n details={\"path\": request.url.path}\n ).dict()\n )\n\[email protected]_handler(HTTPException)\nasync def http_exception_handler(request: Request, exc: HTTPException):\n return JSONResponse(\n status_code=exc.status_code,\n content=ErrorResponse(\n error=ErrorCode.INTERNAL_ERROR,\n message=exc.detail,\n details={\"status_code\": exc.status_code}\n ).dict()\n )\n\n# Service health check\[email protected](\"/api/v1/health\")\nasync def health_check():\n \"\"\"Check search service health.\"\"\"\n try:\n # Verify backend connectivity\n await check_elasticsearch_connection()\n await check_database_connection()\n\n return {\n \"status\": \"healthy\",\n \"timestamp\": datetime.utcnow(),\n \"services\": {\n \"elasticsearch\": \"up\",\n \"database\": \"up\",\n \"cache\": \"up\"\n }\n }\n except Exception as e:\n raise HTTPException(\n status_code=503,\n detail=\"Search service unavailable\"\n )\n```\n\n## API Documentation\n\n### OpenAPI Specification\n```yaml\nopenapi: 3.0.0\ninfo:\n title: Search API\n version: 1.0.0\n description: Product search and filtering API\n\npaths:\n /api/v1/search:\n get:\n summary: Search products\n parameters:\n - name: q\n in: query\n required: false\n schema:\n type: string\n minLength: 1\n maxLength: 200\n description: Search query\n\n - name: category\n in: query\n required: false\n schema:\n type: array\n items:\n type: string\n style: form\n explode: true\n description: Filter by categories\n\n - name: min_price\n in: query\n required: false\n schema:\n type: number\n minimum: 0\n description: Minimum price filter\n\n - name: max_price\n in: query\n required: false\n schema:\n type: number\n minimum: 0\n description: Maximum price filter\n\n - name: page\n in: query\n required: false\n schema:\n type: integer\n minimum: 1\n default: 1\n description: Page number\n\n - name: size\n in: query\n required: false\n schema:\n type: integer\n minimum: 1\n maximum: 100\n default: 20\n description: Results per page\n\n responses:\n 200:\n description: Search results\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/SearchResponse'\n\n 400:\n description: Invalid parameters\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/ErrorResponse'\n\n 429:\n description: Rate limited\n\n 500:\n description: Internal server error\n\ncomponents:\n schemas:\n SearchResponse:\n type: object\n properties:\n total:\n type: integer\n description: Total number of results\n page:\n type: integer\n description: Current page\n size:\n type: integer\n description: Results per page\n items:\n type: array\n items:\n $ref: '#/components/schemas/Product'\n facets:\n type: object\n additionalProperties:\n type: array\n items:\n type: object\n properties:\n value:\n type: string\n count:\n type: integer\n\n Product:\n type: object\n properties:\n id:\n type: string\n title:\n type: string\n description:\n type: string\n price:\n type: number\n category:\n type: string\n brand:\n type: string\n in_stock:\n type: boolean\n\n ErrorResponse:\n type: object\n properties:\n error:\n type: string\n message:\n type: string\n details:\n type: object\n timestamp:\n type: string\n format: date-time\n```","content_type":"text/markdown; charset=utf-8","language":"markdown","size":19613,"content_sha256":"f45f9a99576ee14eb985064da6f33959922ca9faf9c495adba07019e88701a8f"},{"filename":"references/autocomplete-patterns.md","content":"# Autocomplete and Typeahead Patterns\n\n\n## Table of Contents\n\n- [Basic Autocomplete Implementation](#basic-autocomplete-implementation)\n - [React Autocomplete with Downshift](#react-autocomplete-with-downshift)\n - [Highlight Matching Text](#highlight-matching-text)\n- [Async Autocomplete with API](#async-autocomplete-with-api)\n - [Debounced API Autocomplete](#debounced-api-autocomplete)\n- [Advanced Autocomplete Features](#advanced-autocomplete-features)\n - [Multi-Section Autocomplete](#multi-section-autocomplete)\n - [Recent Searches and Suggestions](#recent-searches-and-suggestions)\n- [Search-as-you-type Implementation](#search-as-you-type-implementation)\n - [Real-time Search Results](#real-time-search-results)\n- [Performance Optimization](#performance-optimization)\n - [Virtual Scrolling for Large Lists](#virtual-scrolling-for-large-lists)\n - [Memoized Filtering](#memoized-filtering)\n- [Accessibility Features](#accessibility-features)\n - [ARIA Live Regions](#aria-live-regions)\n\n## Basic Autocomplete Implementation\n\n### React Autocomplete with Downshift\n```tsx\nimport { useCombobox } from 'downshift';\nimport { useState, useMemo } from 'react';\n\ninterface AutocompleteProps\u003cT> {\n items: T[];\n onSelect: (item: T | null) => void;\n itemToString: (item: T | null) => string;\n placeholder?: string;\n filterFunction?: (items: T[], inputValue: string) => T[];\n}\n\nexport function Autocomplete\u003cT>({\n items,\n onSelect,\n itemToString,\n placeholder = 'Type to search...',\n filterFunction\n}: AutocompleteProps\u003cT>) {\n const [inputItems, setInputItems] = useState(items);\n\n const defaultFilter = (items: T[], inputValue: string) => {\n return items.filter(item =>\n itemToString(item)\n .toLowerCase()\n .includes(inputValue.toLowerCase())\n );\n };\n\n const {\n isOpen,\n getToggleButtonProps,\n getLabelProps,\n getMenuProps,\n getInputProps,\n highlightedIndex,\n getItemProps,\n selectedItem,\n inputValue\n } = useCombobox({\n items: inputItems,\n itemToString,\n onInputValueChange: ({ inputValue }) => {\n const filterFn = filterFunction || defaultFilter;\n setInputItems(filterFn(items, inputValue || ''));\n },\n onSelectedItemChange: ({ selectedItem }) => {\n onSelect(selectedItem || null);\n }\n });\n\n return (\n \u003cdiv className=\"autocomplete-container\">\n \u003cdiv className=\"autocomplete-input-wrapper\">\n \u003cinput\n {...getInputProps()}\n placeholder={placeholder}\n className=\"autocomplete-input\"\n />\n \u003cbutton\n type=\"button\"\n {...getToggleButtonProps()}\n aria-label=\"toggle menu\"\n className=\"autocomplete-toggle\"\n >\n {isOpen ? '▲' : '▼'}\n \u003c/button>\n \u003c/div>\n\n \u003cul {...getMenuProps()} className=\"autocomplete-menu\">\n {isOpen && inputItems.length > 0 && (\n inputItems.map((item, index) => (\n \u003cli\n key={`${itemToString(item)}${index}`}\n className={`autocomplete-item ${\n highlightedIndex === index ? 'highlighted' : ''\n } ${selectedItem === item ? 'selected' : ''}`}\n {...getItemProps({ item, index })}\n >\n \u003cHighlightMatch\n text={itemToString(item)}\n query={inputValue || ''}\n />\n \u003c/li>\n ))\n )}\n\n {isOpen && inputItems.length === 0 && (\n \u003cli className=\"autocomplete-no-results\">\n No results found for \"{inputValue}\"\n \u003c/li>\n )}\n \u003c/ul>\n \u003c/div>\n );\n}\n```\n\n### Highlight Matching Text\n```tsx\ninterface HighlightMatchProps {\n text: string;\n query: string;\n}\n\nexport function HighlightMatch({ text, query }: HighlightMatchProps) {\n if (!query) return \u003c>{text}\u003c/>;\n\n const parts = text.split(new RegExp(`(${query})`, 'gi'));\n\n return (\n \u003c>\n {parts.map((part, index) =>\n part.toLowerCase() === query.toLowerCase() ? (\n \u003cmark key={index} className=\"highlight\">\n {part}\n \u003c/mark>\n ) : (\n \u003cspan key={index}>{part}\u003c/span>\n )\n )}\n \u003c/>\n );\n}\n```\n\n## Async Autocomplete with API\n\n### Debounced API Autocomplete\n```tsx\nimport { useState, useEffect, useCallback } from 'react';\nimport { debounce } from 'lodash';\n\ninterface AsyncAutocompleteProps {\n fetchSuggestions: (query: string) => Promise\u003cstring[]>;\n onSelect: (value: string) => void;\n debounceMs?: number;\n minChars?: number;\n}\n\nexport function AsyncAutocomplete({\n fetchSuggestions,\n onSelect,\n debounceMs = 300,\n minChars = 2\n}: AsyncAutocompleteProps) {\n const [inputValue, setInputValue] = useState('');\n const [suggestions, setSuggestions] = useState\u003cstring[]>([]);\n const [isLoading, setIsLoading] = useState(false);\n const [isOpen, setIsOpen] = useState(false);\n const [selectedIndex, setSelectedIndex] = useState(-1);\n\n // Debounced fetch function\n const debouncedFetch = useCallback(\n debounce(async (query: string) => {\n if (query.length \u003c minChars) {\n setSuggestions([]);\n setIsLoading(false);\n return;\n }\n\n setIsLoading(true);\n try {\n const results = await fetchSuggestions(query);\n setSuggestions(results);\n setIsOpen(true);\n } catch (error) {\n console.error('Failed to fetch suggestions:', error);\n setSuggestions([]);\n } finally {\n setIsLoading(false);\n }\n }, debounceMs),\n [fetchSuggestions, minChars]\n );\n\n // Fetch suggestions when input changes\n useEffect(() => {\n debouncedFetch(inputValue);\n return () => debouncedFetch.cancel();\n }, [inputValue, debouncedFetch]);\n\n // Keyboard navigation\n const handleKeyDown = (e: React.KeyboardEvent) => {\n if (!isOpen || suggestions.length === 0) return;\n\n switch (e.key) {\n case 'ArrowDown':\n e.preventDefault();\n setSelectedIndex(prev =>\n prev \u003c suggestions.length - 1 ? prev + 1 : 0\n );\n break;\n\n case 'ArrowUp':\n e.preventDefault();\n setSelectedIndex(prev =>\n prev > 0 ? prev - 1 : suggestions.length - 1\n );\n break;\n\n case 'Enter':\n e.preventDefault();\n if (selectedIndex >= 0) {\n const selected = suggestions[selectedIndex];\n setInputValue(selected);\n onSelect(selected);\n setIsOpen(false);\n }\n break;\n\n case 'Escape':\n setIsOpen(false);\n setSelectedIndex(-1);\n break;\n }\n };\n\n return (\n \u003cdiv className=\"async-autocomplete\">\n \u003cdiv className=\"input-container\">\n \u003cinput\n type=\"text\"\n value={inputValue}\n onChange={(e) => setInputValue(e.target.value)}\n onKeyDown={handleKeyDown}\n onFocus={() => suggestions.length > 0 && setIsOpen(true)}\n onBlur={() => setTimeout(() => setIsOpen(false), 200)}\n placeholder=\"Start typing to search...\"\n aria-autocomplete=\"list\"\n aria-expanded={isOpen}\n aria-controls=\"suggestions-list\"\n aria-activedescendant={\n selectedIndex >= 0 ? `suggestion-${selectedIndex}` : undefined\n }\n />\n\n {isLoading && (\n \u003cdiv className=\"loading-indicator\">\n \u003cspan className=\"spinner\" />\n \u003c/div>\n )}\n \u003c/div>\n\n {isOpen && suggestions.length > 0 && (\n \u003cul\n id=\"suggestions-list\"\n className=\"suggestions-dropdown\"\n role=\"listbox\"\n >\n {suggestions.map((suggestion, index) => (\n \u003cli\n key={index}\n id={`suggestion-${index}`}\n role=\"option\"\n aria-selected={index === selectedIndex}\n className={`suggestion-item ${\n index === selectedIndex ? 'selected' : ''\n }`}\n onMouseEnter={() => setSelectedIndex(index)}\n onClick={() => {\n setInputValue(suggestion);\n onSelect(suggestion);\n setIsOpen(false);\n }}\n >\n \u003cHighlightMatch text={suggestion} query={inputValue} />\n \u003c/li>\n ))}\n \u003c/ul>\n )}\n \u003c/div>\n );\n}\n```\n\n## Advanced Autocomplete Features\n\n### Multi-Section Autocomplete\n```tsx\ninterface Section\u003cT> {\n title: string;\n items: T[];\n}\n\ninterface MultiSectionAutocompleteProps\u003cT> {\n sections: Section\u003cT>[];\n onSelect: (item: T) => void;\n itemToString: (item: T) => string;\n renderItem?: (item: T, isHighlighted: boolean) => React.ReactNode;\n}\n\nexport function MultiSectionAutocomplete\u003cT>({\n sections,\n onSelect,\n itemToString,\n renderItem\n}: MultiSectionAutocompleteProps\u003cT>) {\n const [inputValue, setInputValue] = useState('');\n const [highlightedSection, setHighlightedSection] = useState(0);\n const [highlightedItem, setHighlightedItem] = useState(0);\n\n // Filter sections based on input\n const filteredSections = useMemo(() => {\n if (!inputValue) return sections;\n\n return sections\n .map(section => ({\n ...section,\n items: section.items.filter(item =>\n itemToString(item)\n .toLowerCase()\n .includes(inputValue.toLowerCase())\n )\n }))\n .filter(section => section.items.length > 0);\n }, [sections, inputValue, itemToString]);\n\n // Navigate through sections and items\n const handleKeyDown = (e: React.KeyboardEvent) => {\n // Implementation of keyboard navigation\n // through sections and items\n };\n\n return (\n \u003cdiv className=\"multi-section-autocomplete\">\n \u003cinput\n type=\"text\"\n value={inputValue}\n onChange={(e) => setInputValue(e.target.value)}\n onKeyDown={handleKeyDown}\n placeholder=\"Search...\"\n />\n\n {inputValue && filteredSections.length > 0 && (\n \u003cdiv className=\"sections-dropdown\">\n {filteredSections.map((section, sectionIndex) => (\n \u003cdiv key={section.title} className=\"section\">\n \u003cdiv className=\"section-title\">{section.title}\u003c/div>\n \u003cul className=\"section-items\">\n {section.items.map((item, itemIndex) => {\n const isHighlighted =\n sectionIndex === highlightedSection &&\n itemIndex === highlightedItem;\n\n return (\n \u003cli\n key={itemToString(item)}\n className={`item ${isHighlighted ? 'highlighted' : ''}`}\n onClick={() => onSelect(item)}\n >\n {renderItem ? (\n renderItem(item, isHighlighted)\n ) : (\n \u003cHighlightMatch\n text={itemToString(item)}\n query={inputValue}\n />\n )}\n \u003c/li>\n );\n })}\n \u003c/ul>\n \u003c/div>\n ))}\n \u003c/div>\n )}\n \u003c/div>\n );\n}\n```\n\n### Recent Searches and Suggestions\n```tsx\ninterface SmartAutocompleteProps {\n fetchSuggestions: (query: string) => Promise\u003cstring[]>;\n recentSearches: string[];\n popularSearches: string[];\n onSelect: (value: string) => void;\n onClearRecent: () => void;\n}\n\nexport function SmartAutocomplete({\n fetchSuggestions,\n recentSearches,\n popularSearches,\n onSelect,\n onClearRecent\n}: SmartAutocompleteProps) {\n const [inputValue, setInputValue] = useState('');\n const [suggestions, setSuggestions] = useState\u003cstring[]>([]);\n const [showInitial, setShowInitial] = useState(true);\n\n const sections = useMemo(() => {\n const result = [];\n\n if (showInitial && !inputValue) {\n if (recentSearches.length > 0) {\n result.push({\n title: 'Recent Searches',\n items: recentSearches,\n icon: '🕐',\n clearable: true\n });\n }\n\n if (popularSearches.length > 0) {\n result.push({\n title: 'Trending',\n items: popularSearches,\n icon: '🔥',\n clearable: false\n });\n }\n } else if (suggestions.length > 0) {\n result.push({\n title: 'Suggestions',\n items: suggestions,\n icon: '🔍',\n clearable: false\n });\n }\n\n return result;\n }, [showInitial, inputValue, recentSearches, popularSearches, suggestions]);\n\n return (\n \u003cdiv className=\"smart-autocomplete\">\n \u003cinput\n type=\"text\"\n value={inputValue}\n onChange={(e) => {\n setInputValue(e.target.value);\n setShowInitial(false);\n }}\n onFocus={() => setShowInitial(true)}\n placeholder=\"Search or select from suggestions...\"\n />\n\n {sections.length > 0 && (\n \u003cdiv className=\"smart-dropdown\">\n {sections.map((section) => (\n \u003cdiv key={section.title} className=\"smart-section\">\n \u003cdiv className=\"section-header\">\n \u003cspan className=\"section-icon\">{section.icon}\u003c/span>\n \u003cspan className=\"section-title\">{section.title}\u003c/span>\n {section.clearable && (\n \u003cbutton\n onClick={onClearRecent}\n className=\"clear-button\"\n >\n Clear\n \u003c/button>\n )}\n \u003c/div>\n\n \u003cul className=\"section-items\">\n {section.items.map((item) => (\n \u003cli\n key={item}\n onClick={() => onSelect(item)}\n className=\"smart-item\"\n >\n {item}\n \u003c/li>\n ))}\n \u003c/ul>\n \u003c/div>\n ))}\n \u003c/div>\n )}\n \u003c/div>\n );\n}\n```\n\n## Search-as-you-type Implementation\n\n### Real-time Search Results\n```tsx\ninterface SearchAsYouTypeProps {\n searchFunction: (query: string) => Promise\u003cSearchResult[]>;\n renderResult: (result: SearchResult) => React.ReactNode;\n minChars?: number;\n debounceMs?: number;\n}\n\ninterface SearchResult {\n id: string;\n title: string;\n description: string;\n category: string;\n url: string;\n}\n\nexport function SearchAsYouType({\n searchFunction,\n renderResult,\n minChars = 2,\n debounceMs = 200\n}: SearchAsYouTypeProps) {\n const [query, setQuery] = useState('');\n const [results, setResults] = useState\u003cSearchResult[]>([]);\n const [isSearching, setIsSearching] = useState(false);\n const [showResults, setShowResults] = useState(false);\n\n const performSearch = useCallback(\n debounce(async (searchQuery: string) => {\n if (searchQuery.length \u003c minChars) {\n setResults([]);\n setIsSearching(false);\n return;\n }\n\n setIsSearching(true);\n try {\n const searchResults = await searchFunction(searchQuery);\n setResults(searchResults);\n setShowResults(true);\n } catch (error) {\n console.error('Search failed:', error);\n setResults([]);\n } finally {\n setIsSearching(false);\n }\n }, debounceMs),\n [searchFunction, minChars]\n );\n\n useEffect(() => {\n performSearch(query);\n }, [query, performSearch]);\n\n return (\n \u003cdiv className=\"search-as-you-type\">\n \u003cdiv className=\"search-input-container\">\n \u003cinput\n type=\"search\"\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder=\"Start typing to search...\"\n aria-label=\"Search\"\n aria-describedby=\"search-status\"\n />\n\n {isSearching && (\n \u003cspan className=\"search-status\" id=\"search-status\">\n Searching...\n \u003c/span>\n )}\n \u003c/div>\n\n {showResults && results.length > 0 && (\n \u003cdiv className=\"instant-results\">\n \u003cdiv className=\"results-header\">\n Found {results.length} results for \"{query}\"\n \u003c/div>\n\n \u003cdiv className=\"results-list\">\n {results.map(result => (\n \u003cdiv key={result.id} className=\"result-item\">\n {renderResult(result)}\n \u003c/div>\n ))}\n \u003c/div>\n \u003c/div>\n )}\n\n {showResults && results.length === 0 && !isSearching && query.length >= minChars && (\n \u003cdiv className=\"no-results\">\n No results found for \"{query}\"\n \u003c/div>\n )}\n \u003c/div>\n );\n}\n```\n\n## Performance Optimization\n\n### Virtual Scrolling for Large Lists\n```tsx\nimport { FixedSizeList as List } from 'react-window';\n\ninterface VirtualAutocompleteProps {\n items: string[];\n itemHeight?: number;\n maxHeight?: number;\n onSelect: (item: string) => void;\n}\n\nexport function VirtualAutocomplete({\n items,\n itemHeight = 35,\n maxHeight = 300,\n onSelect\n}: VirtualAutocompleteProps) {\n const [inputValue, setInputValue] = useState('');\n\n const filteredItems = useMemo(() => {\n if (!inputValue) return items;\n return items.filter(item =>\n item.toLowerCase().includes(inputValue.toLowerCase())\n );\n }, [items, inputValue]);\n\n const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => (\n \u003cdiv\n style={style}\n className=\"virtual-item\"\n onClick={() => onSelect(filteredItems[index])}\n >\n \u003cHighlightMatch text={filteredItems[index]} query={inputValue} />\n \u003c/div>\n );\n\n return (\n \u003cdiv className=\"virtual-autocomplete\">\n \u003cinput\n type=\"text\"\n value={inputValue}\n onChange={(e) => setInputValue(e.target.value)}\n placeholder=\"Search from thousands of items...\"\n />\n\n {filteredItems.length > 0 && (\n \u003cList\n height={Math.min(maxHeight, filteredItems.length * itemHeight)}\n itemCount={filteredItems.length}\n itemSize={itemHeight}\n width=\"100%\"\n >\n {Row}\n \u003c/List>\n )}\n \u003c/div>\n );\n}\n```\n\n### Memoized Filtering\n```tsx\nimport { useMemo } from 'react';\n\nfunction useFuzzySearch\u003cT>(\n items: T[],\n searchQuery: string,\n options: {\n keys: string[];\n threshold?: number;\n includeScore?: boolean;\n }\n) {\n return useMemo(() => {\n if (!searchQuery) return items;\n\n // Simple fuzzy matching implementation\n const fuzzyMatch = (str: string, pattern: string) => {\n pattern = pattern.toLowerCase();\n str = str.toLowerCase();\n\n let patternIdx = 0;\n let strIdx = 0;\n let score = 0;\n\n while (patternIdx \u003c pattern.length && strIdx \u003c str.length) {\n if (pattern[patternIdx] === str[strIdx]) {\n score++;\n patternIdx++;\n }\n strIdx++;\n }\n\n return {\n matched: patternIdx === pattern.length,\n score: score / pattern.length\n };\n };\n\n return items\n .map(item => {\n const scores = options.keys.map(key => {\n const value = String(item[key]);\n return fuzzyMatch(value, searchQuery);\n });\n\n const bestMatch = scores.reduce((best, current) =>\n current.score > best.score ? current : best\n );\n\n return {\n item,\n ...bestMatch\n };\n })\n .filter(result => result.matched)\n .sort((a, b) => b.score - a.score)\n .map(result => options.includeScore ? result : result.item);\n }, [items, searchQuery, options]);\n}\n```\n\n## Accessibility Features\n\n### ARIA Live Regions\n```tsx\nfunction AccessibleAutocomplete() {\n const [results, setResults] = useState([]);\n const [announcement, setAnnouncement] = useState('');\n\n useEffect(() => {\n // Announce results to screen readers\n if (results.length > 0) {\n setAnnouncement(`${results.length} suggestions available`);\n } else {\n setAnnouncement('No suggestions available');\n }\n }, [results]);\n\n return (\n \u003c>\n \u003cdiv\n role=\"status\"\n aria-live=\"polite\"\n aria-atomic=\"true\"\n className=\"sr-only\"\n >\n {announcement}\n \u003c/div>\n\n \u003cdiv\n role=\"combobox\"\n aria-expanded={results.length > 0}\n aria-haspopup=\"listbox\"\n aria-owns=\"suggestions\"\n >\n \u003cinput\n type=\"text\"\n aria-autocomplete=\"list\"\n aria-controls=\"suggestions\"\n aria-describedby=\"instructions\"\n />\n\n \u003cspan id=\"instructions\" className=\"sr-only\">\n Type to search. Use arrow keys to navigate suggestions.\n \u003c/span>\n\n \u003cul\n id=\"suggestions\"\n role=\"listbox\"\n >\n {results.map((result, index) => (\n \u003cli\n key={index}\n role=\"option\"\n aria-selected={false}\n >\n {result}\n \u003c/li>\n ))}\n \u003c/ul>\n \u003c/div>\n \u003c/>\n );\n}\n```","content_type":"text/markdown; charset=utf-8","language":"markdown","size":20731,"content_sha256":"32e227f36eba1abcda6ae63e556bf2cb2fc6451ae83a9ccc36b400d8faeb0c1e"},{"filename":"references/database-querying.md","content":"# Database Querying Patterns\n\n\n## Table of Contents\n\n- [SQLAlchemy Dynamic Queries](#sqlalchemy-dynamic-queries)\n - [Basic Filter Building](#basic-filter-building)\n - [Advanced Text Search with PostgreSQL](#advanced-text-search-with-postgresql)\n - [Faceted Search with Aggregations](#faceted-search-with-aggregations)\n- [Django ORM Patterns](#django-orm-patterns)\n - [Django Filter Backend](#django-filter-backend)\n - [Django Full-Text Search](#django-full-text-search)\n- [Query Optimization](#query-optimization)\n - [Index Strategies](#index-strategies)\n - [Query Performance Monitoring](#query-performance-monitoring)\n - [Query Result Caching](#query-result-caching)\n- [Security Considerations](#security-considerations)\n - [SQL Injection Prevention](#sql-injection-prevention)\n\n## SQLAlchemy Dynamic Queries\n\n### Basic Filter Building\n```python\nfrom sqlalchemy.orm import Session\nfrom sqlalchemy import and_, or_, func\nfrom typing import Dict, List, Any, Optional\n\nclass SearchQueryBuilder:\n \"\"\"Build dynamic SQLAlchemy queries from search parameters.\"\"\"\n\n def __init__(self, model, session: Session):\n self.model = model\n self.session = session\n self.query = session.query(model)\n\n def add_text_search(self, search_term: str, columns: List[str]):\n \"\"\"Add text search across multiple columns.\"\"\"\n if not search_term:\n return self\n\n search_term = f\"%{search_term}%\"\n conditions = []\n\n for column in columns:\n if hasattr(self.model, column):\n conditions.append(\n getattr(self.model, column).ilike(search_term)\n )\n\n if conditions:\n self.query = self.query.filter(or_(*conditions))\n\n return self\n\n def add_filters(self, filters: Dict[str, Any]):\n \"\"\"Add exact match filters.\"\"\"\n for key, value in filters.items():\n if value is not None and hasattr(self.model, key):\n if isinstance(value, list):\n # IN clause for multiple values\n self.query = self.query.filter(\n getattr(self.model, key).in_(value)\n )\n else:\n # Exact match\n self.query = self.query.filter(\n getattr(self.model, key) == value\n )\n\n return self\n\n def add_range_filter(self, column: str, min_val: Any, max_val: Any):\n \"\"\"Add range filter (e.g., price range).\"\"\"\n if hasattr(self.model, column):\n if min_val is not None:\n self.query = self.query.filter(\n getattr(self.model, column) >= min_val\n )\n if max_val is not None:\n self.query = self.query.filter(\n getattr(self.model, column) \u003c= max_val\n )\n\n return self\n\n def add_sorting(self, sort_by: str, order: str = 'asc'):\n \"\"\"Add sorting to query.\"\"\"\n if hasattr(self.model, sort_by):\n column = getattr(self.model, sort_by)\n if order == 'desc':\n self.query = self.query.order_by(column.desc())\n else:\n self.query = self.query.order_by(column.asc())\n\n return self\n\n def paginate(self, page: int = 1, per_page: int = 20):\n \"\"\"Add pagination.\"\"\"\n offset = (page - 1) * per_page\n self.query = self.query.offset(offset).limit(per_page)\n return self\n\n def execute(self):\n \"\"\"Execute the query and return results.\"\"\"\n return self.query.all()\n\n def count(self):\n \"\"\"Get total count without pagination.\"\"\"\n return self.query.count()\n```\n\n### Advanced Text Search with PostgreSQL\n```python\nfrom sqlalchemy import text, func\nfrom sqlalchemy.dialects.postgresql import TSVECTOR\n\nclass PostgreSQLSearch:\n \"\"\"Full-text search using PostgreSQL.\"\"\"\n\n @staticmethod\n def create_search_vector(model):\n \"\"\"Create tsvector index for full-text search.\"\"\"\n # Add this to your model\n search_vector = func.to_tsvector(\n 'english',\n func.coalesce(model.title, '') + ' ' +\n func.coalesce(model.description, '') + ' ' +\n func.coalesce(model.tags, '')\n )\n return search_vector\n\n def search_products(self, session: Session, query: str, filters: Dict = None):\n \"\"\"Perform full-text search with ranking.\"\"\"\n from models import Product\n\n # Create tsquery\n search_query = func.plainto_tsquery('english', query)\n\n # Build base query with ranking\n q = session.query(\n Product,\n func.ts_rank(\n func.to_tsvector('english', Product.search_text),\n search_query\n ).label('rank')\n ).filter(\n func.to_tsvector('english', Product.search_text).match(search_query)\n )\n\n # Add additional filters\n if filters:\n for key, value in filters.items():\n if hasattr(Product, key):\n q = q.filter(getattr(Product, key) == value)\n\n # Order by relevance\n q = q.order_by(text('rank DESC'))\n\n return q.all()\n\n def create_search_index(self, session: Session):\n \"\"\"Create GIN index for better performance.\"\"\"\n sql = \"\"\"\n CREATE INDEX idx_product_search_vector\n ON products\n USING GIN (to_tsvector('english',\n COALESCE(title, '') || ' ' ||\n COALESCE(description, '') || ' ' ||\n COALESCE(tags, '')\n ));\n \"\"\"\n session.execute(text(sql))\n session.commit()\n```\n\n### Faceted Search with Aggregations\n```python\nfrom sqlalchemy import func, distinct\n\nclass FacetedSearch:\n \"\"\"Generate facets with counts for filters.\"\"\"\n\n def get_facets(self, session: Session, base_filters: Dict = None):\n \"\"\"Get available facets with counts.\"\"\"\n from models import Product\n\n facets = {}\n\n # Base query with existing filters\n base_query = session.query(Product)\n if base_filters:\n for key, value in base_filters.items():\n if key != 'category': # Don't apply the facet we're counting\n base_query = base_query.filter(\n getattr(Product, key) == value\n )\n\n # Category facet\n category_facets = base_query.with_entities(\n Product.category,\n func.count(Product.id).label('count')\n ).group_by(Product.category).all()\n\n facets['category'] = [\n {'value': cat, 'count': count}\n for cat, count in category_facets\n ]\n\n # Brand facet\n brand_facets = base_query.with_entities(\n Product.brand,\n func.count(Product.id).label('count')\n ).group_by(Product.brand).all()\n\n facets['brand'] = [\n {'value': brand, 'count': count}\n for brand, count in brand_facets\n ]\n\n # Price range facet\n price_ranges = [\n (0, 50, 'Under $50'),\n (50, 100, '$50 - $100'),\n (100, 200, '$100 - $200'),\n (200, None, 'Over $200')\n ]\n\n facets['price_range'] = []\n for min_price, max_price, label in price_ranges:\n q = base_query\n q = q.filter(Product.price >= min_price)\n if max_price:\n q = q.filter(Product.price \u003c max_price)\n\n count = q.count()\n if count > 0:\n facets['price_range'].append({\n 'value': f\"{min_price}-{max_price or 'inf'}\",\n 'label': label,\n 'count': count\n })\n\n return facets\n```\n\n## Django ORM Patterns\n\n### Django Filter Backend\n```python\nfrom django.db.models import Q, Count, Avg\nfrom django_filters import FilterSet, CharFilter, RangeFilter\nfrom rest_framework import filters\n\nclass ProductFilter(FilterSet):\n \"\"\"Django filter for product search.\"\"\"\n\n search = CharFilter(method='search_filter')\n price = RangeFilter()\n category = CharFilter(field_name='category__name', lookup_expr='iexact')\n brand = CharFilter(field_name='brand', lookup_expr='icontains')\n\n class Meta:\n model = Product\n fields = ['search', 'price', 'category', 'brand', 'in_stock']\n\n def search_filter(self, queryset, name, value):\n \"\"\"Custom search across multiple fields.\"\"\"\n return queryset.filter(\n Q(title__icontains=value) |\n Q(description__icontains=value) |\n Q(tags__icontains=value)\n )\n\nclass ProductViewSet(viewsets.ModelViewSet):\n \"\"\"ViewSet with search and filtering.\"\"\"\n\n queryset = Product.objects.all()\n serializer_class = ProductSerializer\n filter_backends = [\n filters.SearchFilter,\n filters.OrderingFilter,\n DjangoFilterBackend\n ]\n filterset_class = ProductFilter\n search_fields = ['title', 'description', 'tags']\n ordering_fields = ['price', 'created_at', 'rating']\n ordering = ['-created_at'] # Default ordering\n\n def get_queryset(self):\n \"\"\"Optimize query with select_related and prefetch_related.\"\"\"\n queryset = super().get_queryset()\n queryset = queryset.select_related('category', 'brand')\n queryset = queryset.prefetch_related('reviews', 'images')\n\n # Add annotations for computed fields\n queryset = queryset.annotate(\n avg_rating=Avg('reviews__rating'),\n review_count=Count('reviews')\n )\n\n return queryset\n```\n\n### Django Full-Text Search\n```python\nfrom django.contrib.postgres.search import (\n SearchVector, SearchQuery, SearchRank, TrigramSimilarity\n)\n\nclass PostgreSQLFullTextSearch:\n \"\"\"PostgreSQL full-text search in Django.\"\"\"\n\n def search_products(self, query: str):\n \"\"\"Perform full-text search with ranking.\"\"\"\n from products.models import Product\n\n # Create search vector\n search_vector = SearchVector(\n 'title', weight='A'\n ) + SearchVector(\n 'description', weight='B'\n ) + SearchVector(\n 'tags', weight='C'\n )\n\n # Create search query\n search_query = SearchQuery(query, config='english')\n\n # Perform search with ranking\n results = Product.objects.annotate(\n search=search_vector,\n rank=SearchRank(search_vector, search_query)\n ).filter(\n search=search_query\n ).order_by('-rank')\n\n return results\n\n def trigram_search(self, query: str):\n \"\"\"Use trigram similarity for fuzzy matching.\"\"\"\n from products.models import Product\n\n return Product.objects.annotate(\n similarity=TrigramSimilarity('title', query)\n ).filter(\n similarity__gt=0.1\n ).order_by('-similarity')\n\n def combined_search(self, query: str):\n \"\"\"Combine full-text and trigram search.\"\"\"\n from products.models import Product\n\n # Full-text search\n search_vector = SearchVector('title', 'description')\n search_query = SearchQuery(query)\n\n # Combine with trigram similarity\n results = Product.objects.annotate(\n search_rank=SearchRank(search_vector, search_query),\n title_similarity=TrigramSimilarity('title', query),\n combined_score=F('search_rank') + F('title_similarity')\n ).filter(\n Q(search=search_query) | Q(title_similarity__gt=0.1)\n ).order_by('-combined_score')\n\n return results\n```\n\n## Query Optimization\n\n### Index Strategies\n```python\n\"\"\"\nDatabase indexes for search optimization.\nAdd these to your models or migrations.\n\"\"\"\n\n# SQLAlchemy indexes\nfrom sqlalchemy import Index\n\nclass Product(Base):\n __tablename__ = 'products'\n\n id = Column(Integer, primary_key=True)\n title = Column(String, nullable=False)\n description = Column(Text)\n category = Column(String, index=True) # Single column index\n brand = Column(String, index=True)\n price = Column(Numeric(10, 2), index=True)\n created_at = Column(DateTime, index=True)\n\n # Composite indexes\n __table_args__ = (\n Index('idx_category_brand', 'category', 'brand'),\n Index('idx_price_category', 'price', 'category'),\n Index('idx_search_fields', 'title', 'description'), # For text search\n )\n\n# Django indexes\nclass Product(models.Model):\n title = models.CharField(max_length=200, db_index=True)\n description = models.TextField()\n category = models.CharField(max_length=50, db_index=True)\n price = models.DecimalField(max_digits=10, decimal_places=2, db_index=True)\n\n class Meta:\n indexes = [\n models.Index(fields=['category', 'brand']),\n models.Index(fields=['price', '-created_at']), # Compound index\n models.Index(fields=['title'], name='title_idx'),\n ]\n```\n\n### Query Performance Monitoring\n```python\nimport time\nfrom contextlib import contextmanager\nimport logging\n\nlogger = logging.getLogger(__name__)\n\n@contextmanager\ndef query_performance_monitor(operation_name: str):\n \"\"\"Monitor query execution time.\"\"\"\n start_time = time.time()\n\n try:\n yield\n finally:\n execution_time = time.time() - start_time\n\n if execution_time > 1.0: # Log slow queries\n logger.warning(\n f\"Slow query detected: {operation_name} \"\n f\"took {execution_time:.2f} seconds\"\n )\n else:\n logger.info(\n f\"Query {operation_name} \"\n f\"executed in {execution_time:.3f} seconds\"\n )\n\n# Usage\ndef search_products(query: str, filters: Dict):\n with query_performance_monitor(\"product_search\"):\n results = SearchQueryBuilder(Product, session)\\\n .add_text_search(query, ['title', 'description'])\\\n .add_filters(filters)\\\n .execute()\n\n return results\n```\n\n### Query Result Caching\n```python\nfrom functools import lru_cache\nimport hashlib\nimport json\n\nclass QueryCache:\n \"\"\"Simple query result caching.\"\"\"\n\n def __init__(self, ttl_seconds: int = 300):\n self.cache = {}\n self.ttl = ttl_seconds\n\n def _generate_key(self, query: str, filters: Dict):\n \"\"\"Generate cache key from query parameters.\"\"\"\n cache_data = {\n 'query': query,\n 'filters': filters\n }\n cache_string = json.dumps(cache_data, sort_keys=True)\n return hashlib.md5(cache_string.encode()).hexdigest()\n\n def get(self, query: str, filters: Dict):\n \"\"\"Get cached results if available.\"\"\"\n key = self._generate_key(query, filters)\n\n if key in self.cache:\n result, timestamp = self.cache[key]\n if time.time() - timestamp \u003c self.ttl:\n return result\n else:\n del self.cache[key]\n\n return None\n\n def set(self, query: str, filters: Dict, results):\n \"\"\"Cache query results.\"\"\"\n key = self._generate_key(query, filters)\n self.cache[key] = (results, time.time())\n\n def clear(self):\n \"\"\"Clear all cached results.\"\"\"\n self.cache.clear()\n\n# Usage\ncache = QueryCache(ttl_seconds=300)\n\ndef cached_search(query: str, filters: Dict):\n # Check cache\n cached = cache.get(query, filters)\n if cached:\n return cached\n\n # Perform search\n results = perform_search(query, filters)\n\n # Cache results\n cache.set(query, filters, results)\n\n return results\n```\n\n## Security Considerations\n\n### SQL Injection Prevention\n```python\nclass SecureQueryBuilder:\n \"\"\"Secure query building with input validation.\"\"\"\n\n @staticmethod\n def sanitize_search_term(term: str) -> str:\n \"\"\"Sanitize search input.\"\"\"\n # Remove SQL special characters\n dangerous_chars = [';', '--', '/*', '*/', 'xp_', 'sp_', '@@', '@']\n for char in dangerous_chars:\n term = term.replace(char, '')\n\n # Limit length\n return term[:100]\n\n @staticmethod\n def validate_column_name(column: str, allowed_columns: List[str]) -> bool:\n \"\"\"Validate column name against whitelist.\"\"\"\n return column in allowed_columns\n\n @staticmethod\n def validate_sort_order(order: str) -> str:\n \"\"\"Validate sort order.\"\"\"\n return 'desc' if order.lower() == 'desc' else 'asc'\n\n def build_safe_query(self, params: Dict):\n \"\"\"Build query with validation.\"\"\"\n allowed_columns = ['title', 'description', 'category', 'brand', 'price']\n\n # Validate and sanitize inputs\n if 'search' in params:\n params['search'] = self.sanitize_search_term(params['search'])\n\n if 'sort_by' in params:\n if not self.validate_column_name(params['sort_by'], allowed_columns):\n params['sort_by'] = 'created_at' # Default\n\n if 'order' in params:\n params['order'] = self.validate_sort_order(params['order'])\n\n # Build query safely using parameterized queries\n return self._build_query(params)\n```","content_type":"text/markdown; charset=utf-8","language":"markdown","size":17173,"content_sha256":"18957ce7ab475330bdec02cc0af76bf5c89978a4e82f532dad4e0a78c70c6ead"},{"filename":"references/elasticsearch-integration.md","content":"# Elasticsearch Integration Patterns\n\n\n## Table of Contents\n\n- [Python Elasticsearch Client Setup](#python-elasticsearch-client-setup)\n - [Basic Connection](#basic-connection)\n- [Index Design and Mappings](#index-design-and-mappings)\n - [Product Search Index](#product-search-index)\n- [Search Query Patterns](#search-query-patterns)\n - [Full-Text Search with Filters](#full-text-search-with-filters)\n - [Autocomplete/Suggestions](#autocompletesuggestions)\n- [Advanced Search Features](#advanced-search-features)\n - [Boolean Query Builder](#boolean-query-builder)\n - [Relevance Tuning](#relevance-tuning)\n- [Performance Optimization](#performance-optimization)\n - [Query Caching](#query-caching)\n - [Scroll API for Large Results](#scroll-api-for-large-results)\n- [Index Management](#index-management)\n - [Reindexing Strategy](#reindexing-strategy)\n- [Error Handling](#error-handling)\n - [Robust Search with Retries](#robust-search-with-retries)\n\n## Python Elasticsearch Client Setup\n\n### Basic Connection\n```python\nfrom elasticsearch import Elasticsearch\nfrom elasticsearch.helpers import bulk\nimport logging\n\nlogger = logging.getLogger(__name__)\n\nclass ElasticsearchClient:\n \"\"\"Elasticsearch client wrapper with connection management.\"\"\"\n\n def __init__(self, hosts=['localhost:9200'], **kwargs):\n \"\"\"Initialize Elasticsearch connection.\"\"\"\n self.es = Elasticsearch(\n hosts=hosts,\n # Authentication if needed\n http_auth=kwargs.get('http_auth'),\n # Connection parameters\n timeout=kwargs.get('timeout', 30),\n max_retries=kwargs.get('max_retries', 3),\n retry_on_timeout=kwargs.get('retry_on_timeout', True)\n )\n\n # Verify connection\n if not self.es.ping():\n raise ValueError(\"Connection to Elasticsearch failed\")\n\n logger.info(f\"Connected to Elasticsearch: {self.es.info()['version']['number']}\")\n\n def create_index(self, index_name: str, mappings: dict, settings: dict = None):\n \"\"\"Create an index with mappings.\"\"\"\n body = {}\n\n if settings:\n body['settings'] = settings\n\n if mappings:\n body['mappings'] = mappings\n\n if not self.es.indices.exists(index=index_name):\n self.es.indices.create(index=index_name, body=body)\n logger.info(f\"Created index: {index_name}\")\n else:\n logger.info(f\"Index {index_name} already exists\")\n\n def delete_index(self, index_name: str):\n \"\"\"Delete an index.\"\"\"\n if self.es.indices.exists(index=index_name):\n self.es.indices.delete(index=index_name)\n logger.info(f\"Deleted index: {index_name}\")\n```\n\n## Index Design and Mappings\n\n### Product Search Index\n```python\nclass ProductIndexManager:\n \"\"\"Manage product search index.\"\"\"\n\n PRODUCT_INDEX = 'products'\n\n PRODUCT_MAPPING = {\n 'properties': {\n 'id': {'type': 'keyword'},\n 'title': {\n 'type': 'text',\n 'analyzer': 'standard',\n 'fields': {\n 'keyword': {'type': 'keyword'},\n 'suggest': {\n 'type': 'search_as_you_type'\n }\n }\n },\n 'description': {\n 'type': 'text',\n 'analyzer': 'english'\n },\n 'category': {\n 'type': 'keyword',\n 'fields': {\n 'text': {'type': 'text'}\n }\n },\n 'brand': {'type': 'keyword'},\n 'price': {'type': 'float'},\n 'tags': {'type': 'keyword'},\n 'in_stock': {'type': 'boolean'},\n 'created_at': {'type': 'date'},\n 'rating': {'type': 'float'},\n 'review_count': {'type': 'integer'},\n 'image_url': {'type': 'keyword'},\n 'attributes': {\n 'type': 'nested',\n 'properties': {\n 'name': {'type': 'keyword'},\n 'value': {'type': 'keyword'}\n }\n }\n }\n }\n\n INDEX_SETTINGS = {\n 'number_of_shards': 1,\n 'number_of_replicas': 1,\n 'analysis': {\n 'analyzer': {\n 'autocomplete': {\n 'tokenizer': 'autocomplete',\n 'filter': ['lowercase']\n },\n 'autocomplete_search': {\n 'tokenizer': 'lowercase'\n }\n },\n 'tokenizer': {\n 'autocomplete': {\n 'type': 'edge_ngram',\n 'min_gram': 2,\n 'max_gram': 10,\n 'token_chars': ['letter', 'digit']\n }\n }\n }\n }\n\n def __init__(self, es_client: ElasticsearchClient):\n self.es = es_client.es\n\n def create_product_index(self):\n \"\"\"Create product index with optimized mappings.\"\"\"\n self.es_client.create_index(\n self.PRODUCT_INDEX,\n self.PRODUCT_MAPPING,\n self.INDEX_SETTINGS\n )\n\n def index_products(self, products: list):\n \"\"\"Bulk index products.\"\"\"\n actions = []\n\n for product in products:\n action = {\n '_index': self.PRODUCT_INDEX,\n '_id': product['id'],\n '_source': product\n }\n actions.append(action)\n\n success, failed = bulk(self.es, actions, raise_on_error=False)\n logger.info(f\"Indexed {success} products, {len(failed)} failed\")\n\n if failed:\n logger.error(f\"Failed to index: {failed}\")\n\n return success, failed\n```\n\n## Search Query Patterns\n\n### Full-Text Search with Filters\n```python\nfrom typing import Dict, List, Optional, Any\n\nclass ProductSearcher:\n \"\"\"Execute product searches with Elasticsearch.\"\"\"\n\n def __init__(self, es_client: ElasticsearchClient):\n self.es = es_client.es\n\n def search(\n self,\n query: str = None,\n filters: Dict[str, Any] = None,\n sort_by: str = None,\n page: int = 1,\n size: int = 20,\n facets: List[str] = None\n ):\n \"\"\"Perform product search with filters and facets.\"\"\"\n\n # Build Elasticsearch query\n es_query = self._build_query(query, filters)\n\n # Build request body\n body = {\n 'query': es_query,\n 'from': (page - 1) * size,\n 'size': size\n }\n\n # Add sorting\n if sort_by:\n body['sort'] = self._build_sort(sort_by)\n\n # Add aggregations for facets\n if facets:\n body['aggs'] = self._build_aggregations(facets)\n\n # Execute search\n response = self.es.search(\n index='products',\n body=body\n )\n\n return self._parse_response(response)\n\n def _build_query(self, query: str, filters: Dict[str, Any]):\n \"\"\"Build Elasticsearch query with filters.\"\"\"\n must = []\n filter_clauses = []\n\n # Text search\n if query:\n must.append({\n 'multi_match': {\n 'query': query,\n 'fields': [\n 'title^3', # Boost title matches\n 'description^2', # Medium boost for description\n 'tags',\n 'category.text',\n 'brand'\n ],\n 'type': 'best_fields',\n 'fuzziness': 'AUTO'\n }\n })\n\n # Apply filters\n if filters:\n for field, value in filters.items():\n if isinstance(value, list):\n # Multiple values - use terms query\n filter_clauses.append({\n 'terms': {field: value}\n })\n elif isinstance(value, dict):\n # Range filter\n if 'min' in value or 'max' in value:\n range_filter = {}\n if 'min' in value:\n range_filter['gte'] = value['min']\n if 'max' in value:\n range_filter['lte'] = value['max']\n\n filter_clauses.append({\n 'range': {field: range_filter}\n })\n else:\n # Exact match\n filter_clauses.append({\n 'term': {field: value}\n })\n\n # Combine queries\n if must or filter_clauses:\n return {\n 'bool': {\n 'must': must,\n 'filter': filter_clauses\n }\n }\n else:\n return {'match_all': {}}\n\n def _build_sort(self, sort_by: str):\n \"\"\"Build sort clause.\"\"\"\n sort_options = {\n 'relevance': ['_score'],\n 'price_asc': [{'price': 'asc'}],\n 'price_desc': [{'price': 'desc'}],\n 'newest': [{'created_at': 'desc'}],\n 'rating': [{'rating': 'desc'}],\n }\n\n return sort_options.get(sort_by, ['_score'])\n\n def _build_aggregations(self, facets: List[str]):\n \"\"\"Build aggregations for faceted search.\"\"\"\n aggs = {}\n\n for facet in facets:\n if facet == 'price':\n # Range aggregation for price\n aggs['price_ranges'] = {\n 'range': {\n 'field': 'price',\n 'ranges': [\n {'key': 'Under $50', 'to': 50},\n {'key': '$50-$100', 'from': 50, 'to': 100},\n {'key': '$100-$200', 'from': 100, 'to': 200},\n {'key': 'Over $200', 'from': 200}\n ]\n }\n }\n else:\n # Terms aggregation for categorical fields\n aggs[facet] = {\n 'terms': {\n 'field': facet,\n 'size': 20\n }\n }\n\n return aggs\n\n def _parse_response(self, response):\n \"\"\"Parse Elasticsearch response.\"\"\"\n results = {\n 'total': response['hits']['total']['value'],\n 'items': [],\n 'facets': {}\n }\n\n # Extract search results\n for hit in response['hits']['hits']:\n item = hit['_source']\n item['_score'] = hit['_score']\n results['items'].append(item)\n\n # Extract facets\n if 'aggregations' in response:\n for facet_name, facet_data in response['aggregations'].items():\n if 'buckets' in facet_data:\n results['facets'][facet_name] = [\n {\n 'value': bucket.get('key'),\n 'count': bucket.get('doc_count')\n }\n for bucket in facet_data['buckets']\n ]\n\n return results\n```\n\n### Autocomplete/Suggestions\n```python\nclass AutocompleteSearcher:\n \"\"\"Implement autocomplete with Elasticsearch.\"\"\"\n\n def __init__(self, es_client: ElasticsearchClient):\n self.es = es_client.es\n\n def suggest(self, prefix: str, size: int = 10):\n \"\"\"Get autocomplete suggestions.\"\"\"\n body = {\n 'query': {\n 'multi_match': {\n 'query': prefix,\n 'type': 'bool_prefix',\n 'fields': [\n 'title.suggest',\n 'title.suggest._2gram',\n 'title.suggest._3gram'\n ]\n }\n },\n 'size': size,\n '_source': ['title', 'category', 'brand']\n }\n\n response = self.es.search(index='products', body=body)\n\n suggestions = []\n for hit in response['hits']['hits']:\n suggestions.append({\n 'text': hit['_source']['title'],\n 'category': hit['_source'].get('category'),\n 'brand': hit['_source'].get('brand')\n })\n\n return suggestions\n\n def search_as_you_type(self, query: str):\n \"\"\"Real-time search suggestions.\"\"\"\n body = {\n 'suggest': {\n 'product-suggest': {\n 'prefix': query,\n 'completion': {\n 'field': 'title.suggest',\n 'size': 5,\n 'skip_duplicates': True\n }\n }\n }\n }\n\n response = self.es.search(index='products', body=body)\n\n suggestions = []\n for option in response['suggest']['product-suggest'][0]['options']:\n suggestions.append({\n 'text': option['text'],\n 'score': option['_score']\n })\n\n return suggestions\n```\n\n## Advanced Search Features\n\n### Boolean Query Builder\n```python\nclass BooleanQueryBuilder:\n \"\"\"Build complex boolean queries for Elasticsearch.\"\"\"\n\n def build_advanced_query(self, search_params: Dict):\n \"\"\"\n Build advanced query with AND/OR/NOT operators.\n\n Example params:\n {\n 'must': ['laptop', 'dell'],\n 'should': ['gaming', 'professional'],\n 'must_not': ['refurbished'],\n 'fields': {\n 'title': 'laptop',\n 'brand': 'dell'\n }\n }\n \"\"\"\n bool_query = {\n 'bool': {}\n }\n\n # Must clauses (AND)\n if 'must' in search_params:\n bool_query['bool']['must'] = [\n {'match': {'_all': term}}\n for term in search_params['must']\n ]\n\n # Should clauses (OR)\n if 'should' in search_params:\n bool_query['bool']['should'] = [\n {'match': {'_all': term}}\n for term in search_params['should']\n ]\n bool_query['bool']['minimum_should_match'] = 1\n\n # Must not clauses (NOT)\n if 'must_not' in search_params:\n bool_query['bool']['must_not'] = [\n {'match': {'_all': term}}\n for term in search_params['must_not']\n ]\n\n # Field-specific searches\n if 'fields' in search_params:\n if 'must' not in bool_query['bool']:\n bool_query['bool']['must'] = []\n\n for field, value in search_params['fields'].items():\n bool_query['bool']['must'].append({\n 'match': {field: value}\n })\n\n return bool_query\n```\n\n### Relevance Tuning\n```python\nclass RelevanceTuner:\n \"\"\"Tune search relevance with boosting and scoring.\"\"\"\n\n def search_with_boosting(self, query: str, user_context: Dict = None):\n \"\"\"Search with context-aware boosting.\"\"\"\n\n # Base query\n base_query = {\n 'multi_match': {\n 'query': query,\n 'fields': [\n 'title^3',\n 'description^2',\n 'tags'\n ]\n }\n }\n\n # Apply function score for personalization\n function_score = {\n 'function_score': {\n 'query': base_query,\n 'functions': []\n }\n }\n\n # Boost recent products\n function_score['function_score']['functions'].append({\n 'gauss': {\n 'created_at': {\n 'origin': 'now',\n 'scale': '30d',\n 'decay': 0.5\n }\n },\n 'weight': 1.5\n })\n\n # Boost highly rated products\n function_score['function_score']['functions'].append({\n 'field_value_factor': {\n 'field': 'rating',\n 'factor': 1.2,\n 'modifier': 'sqrt',\n 'missing': 1\n }\n })\n\n # User preference boosting\n if user_context and 'preferred_categories' in user_context:\n for category in user_context['preferred_categories']:\n function_score['function_score']['functions'].append({\n 'filter': {'term': {'category': category}},\n 'weight': 2.0\n })\n\n # Combine scores\n function_score['function_score']['score_mode'] = 'sum'\n function_score['function_score']['boost_mode'] = 'multiply'\n\n return function_score\n```\n\n## Performance Optimization\n\n### Query Caching\n```python\nfrom functools import lru_cache\nimport hashlib\nimport json\n\nclass ElasticsearchCache:\n \"\"\"Cache Elasticsearch queries for performance.\"\"\"\n\n def __init__(self, es_client: ElasticsearchClient):\n self.es = es_client.es\n self._cache = {}\n\n @lru_cache(maxsize=100)\n def _get_cache_key(self, query_str: str):\n \"\"\"Generate cache key from query.\"\"\"\n return hashlib.md5(query_str.encode()).hexdigest()\n\n def search_with_cache(self, index: str, body: dict, cache_ttl: int = 300):\n \"\"\"Execute search with caching.\"\"\"\n\n # Generate cache key\n query_str = json.dumps(body, sort_keys=True)\n cache_key = self._get_cache_key(query_str)\n\n # Check cache\n if cache_key in self._cache:\n cached_result, timestamp = self._cache[cache_key]\n if time.time() - timestamp \u003c cache_ttl:\n return cached_result\n\n # Execute query\n result = self.es.search(index=index, body=body)\n\n # Cache result\n self._cache[cache_key] = (result, time.time())\n\n return result\n```\n\n### Scroll API for Large Results\n```python\nclass ScrollSearcher:\n \"\"\"Handle large result sets with scroll API.\"\"\"\n\n def __init__(self, es_client: ElasticsearchClient):\n self.es = es_client.es\n\n def scroll_all_products(self, query: dict = None, batch_size: int = 1000):\n \"\"\"Scroll through all matching products.\"\"\"\n\n if query is None:\n query = {'match_all': {}}\n\n # Initialize scroll\n response = self.es.search(\n index='products',\n body={'query': query, 'size': batch_size},\n scroll='2m' # Keep scroll context for 2 minutes\n )\n\n scroll_id = response['_scroll_id']\n results = response['hits']['hits']\n\n # Yield first batch\n yield results\n\n # Continue scrolling\n while len(results) > 0:\n response = self.es.scroll(\n scroll_id=scroll_id,\n scroll='2m'\n )\n\n scroll_id = response['_scroll_id']\n results = response['hits']['hits']\n\n if results:\n yield results\n\n # Clear scroll context\n self.es.clear_scroll(scroll_id=scroll_id)\n```\n\n## Index Management\n\n### Reindexing Strategy\n```python\nclass IndexManager:\n \"\"\"Manage index lifecycle and reindexing.\"\"\"\n\n def reindex_with_zero_downtime(self, old_index: str, new_index: str, new_mapping: dict):\n \"\"\"Reindex with zero downtime using aliases.\"\"\"\n\n # 1. Create new index with updated mapping\n self.es.indices.create(index=new_index, body={'mappings': new_mapping})\n\n # 2. Reindex data\n self.es.reindex(\n body={\n 'source': {'index': old_index},\n 'dest': {'index': new_index}\n },\n wait_for_completion=False\n )\n\n # 3. Wait for reindex to complete\n task_id = response['task']\n self._wait_for_task(task_id)\n\n # 4. Verify document count\n old_count = self.es.count(index=old_index)['count']\n new_count = self.es.count(index=new_index)['count']\n\n if old_count != new_count:\n raise ValueError(f\"Document count mismatch: {old_count} vs {new_count}\")\n\n # 5. Switch alias atomically\n self.es.indices.update_aliases(\n body={\n 'actions': [\n {'remove': {'index': old_index, 'alias': 'products'}},\n {'add': {'index': new_index, 'alias': 'products'}}\n ]\n }\n )\n\n # 6. Delete old index (optional)\n # self.es.indices.delete(index=old_index)\n\n logger.info(f\"Successfully reindexed from {old_index} to {new_index}\")\n```\n\n## Error Handling\n\n### Robust Search with Retries\n```python\nfrom elasticsearch.exceptions import (\n ConnectionError,\n ConnectionTimeout,\n TransportError\n)\nimport time\n\nclass RobustSearcher:\n \"\"\"Elasticsearch search with error handling and retries.\"\"\"\n\n def __init__(self, es_client: ElasticsearchClient):\n self.es = es_client.es\n self.max_retries = 3\n self.retry_delay = 1 # seconds\n\n def search_with_retry(self, index: str, body: dict):\n \"\"\"Execute search with automatic retry on failure.\"\"\"\n\n last_exception = None\n\n for attempt in range(self.max_retries):\n try:\n return self.es.search(index=index, body=body)\n\n except ConnectionTimeout as e:\n logger.warning(f\"Search timeout (attempt {attempt + 1}): {e}\")\n last_exception = e\n time.sleep(self.retry_delay * (attempt + 1))\n\n except ConnectionError as e:\n logger.error(f\"Connection error (attempt {attempt + 1}): {e}\")\n last_exception = e\n time.sleep(self.retry_delay * (attempt + 1))\n\n except TransportError as e:\n if e.status_code == 429: # Too many requests\n logger.warning(f\"Rate limited, backing off...\")\n time.sleep(self.retry_delay * (attempt + 2))\n else:\n logger.error(f\"Transport error: {e}\")\n raise\n\n # All retries failed\n raise last_exception\n```","content_type":"text/markdown; charset=utf-8","language":"markdown","size":22143,"content_sha256":"473f311ec71265dd6333fc98b04147e23d45aa916acd71ea2266ecf14577a5bd"},{"filename":"references/filter-ui-patterns.md","content":"# Filter UI Patterns\n\n\n## Table of Contents\n\n- [Checkbox Filters](#checkbox-filters)\n - [Basic Multi-Select Filter](#basic-multi-select-filter)\n - [Collapsible Filter Groups](#collapsible-filter-groups)\n- [Range Filters](#range-filters)\n - [Price Range Slider](#price-range-slider)\n - [Date Range Picker](#date-range-picker)\n- [Dropdown Filters](#dropdown-filters)\n - [Single Select Dropdown](#single-select-dropdown)\n - [Searchable Dropdown with Downshift](#searchable-dropdown-with-downshift)\n- [Filter Chips](#filter-chips)\n - [Active Filter Display](#active-filter-display)\n- [Faceted Search](#faceted-search)\n - [Dynamic Count Updates](#dynamic-count-updates)\n- [Mobile Filter Patterns](#mobile-filter-patterns)\n - [Filter Drawer](#filter-drawer)\n- [Sort Options](#sort-options)\n - [Sort Dropdown](#sort-dropdown)\n- [Filter State Management](#filter-state-management)\n - [Using URL Parameters](#using-url-parameters)\n- [Accessibility Considerations](#accessibility-considerations)\n - [Filter Region ARIA](#filter-region-aria)\n - [Keyboard Navigation](#keyboard-navigation)\n\n## Checkbox Filters\n\n### Basic Multi-Select Filter\n```tsx\ninterface FilterOption {\n id: string;\n label: string;\n count?: number;\n}\n\ninterface CheckboxFilterProps {\n title: string;\n options: FilterOption[];\n selected: string[];\n onChange: (selected: string[]) => void;\n}\n\nexport function CheckboxFilter({\n title,\n options,\n selected,\n onChange\n}: CheckboxFilterProps) {\n const handleToggle = (optionId: string) => {\n if (selected.includes(optionId)) {\n onChange(selected.filter(id => id !== optionId));\n } else {\n onChange([...selected, optionId]);\n }\n };\n\n const handleSelectAll = () => {\n if (selected.length === options.length) {\n onChange([]);\n } else {\n onChange(options.map(opt => opt.id));\n }\n };\n\n return (\n \u003cdiv className=\"filter-group\">\n \u003ch3>{title}\u003c/h3>\n\n \u003cbutton\n onClick={handleSelectAll}\n className=\"select-all-btn\"\n >\n {selected.length === options.length ? 'Clear all' : 'Select all'}\n \u003c/button>\n\n {options.map(option => (\n \u003clabel key={option.id} className=\"checkbox-label\">\n \u003cinput\n type=\"checkbox\"\n checked={selected.includes(option.id)}\n onChange={() => handleToggle(option.id)}\n aria-label={`Filter by ${option.label}`}\n />\n \u003cspan>{option.label}\u003c/span>\n {option.count !== undefined && (\n \u003cspan className=\"count\">({option.count})\u003c/span>\n )}\n \u003c/label>\n ))}\n \u003c/div>\n );\n}\n```\n\n### Collapsible Filter Groups\n```tsx\nimport { ChevronDown, ChevronUp } from 'lucide-react';\n\nfunction CollapsibleFilter({ title, children, defaultOpen = true }) {\n const [isOpen, setIsOpen] = useState(defaultOpen);\n\n return (\n \u003cdiv className=\"filter-section\">\n \u003cbutton\n className=\"filter-header\"\n onClick={() => setIsOpen(!isOpen)}\n aria-expanded={isOpen}\n aria-controls={`filter-${title}`}\n >\n \u003cspan>{title}\u003c/span>\n {isOpen ? \u003cChevronUp /> : \u003cChevronDown />}\n \u003c/button>\n\n {isOpen && (\n \u003cdiv id={`filter-${title}`} className=\"filter-content\">\n {children}\n \u003c/div>\n )}\n \u003c/div>\n );\n}\n```\n\n## Range Filters\n\n### Price Range Slider\n```tsx\ninterface RangeFilterProps {\n min: number;\n max: number;\n value: [number, number];\n onChange: (value: [number, number]) => void;\n step?: number;\n prefix?: string;\n}\n\nexport function RangeFilter({\n min,\n max,\n value,\n onChange,\n step = 1,\n prefix = '

Search & Filter Implementation Implement search and filter interfaces with comprehensive frontend components and backend query optimization. Purpose This skill provides production-ready patterns for implementing search and filtering functionality across the full stack. It covers React/TypeScript components for the frontend (search inputs, filter UIs, autocomplete) and Python patterns for the backend (SQLAlchemy queries, Elasticsearch integration, API design). The skill emphasizes performance optimization, accessibility, and user experience. When to Use - Building product search with category…

\n}: RangeFilterProps) {\n const [localValue, setLocalValue] = useState(value);\n\n useEffect(() => {\n const timeoutId = setTimeout(() => {\n onChange(localValue);\n }, 500); // Debounce\n\n return () => clearTimeout(timeoutId);\n }, [localValue]);\n\n return (\n \u003cdiv className=\"range-filter\">\n \u003cdiv className=\"range-inputs\">\n \u003cinput\n type=\"number\"\n value={localValue[0]}\n onChange={(e) => setLocalValue([+e.target.value, localValue[1]])}\n min={min}\n max={localValue[1]}\n aria-label=\"Minimum price\"\n />\n \u003cspan>to\u003c/span>\n \u003cinput\n type=\"number\"\n value={localValue[1]}\n onChange={(e) => setLocalValue([localValue[0], +e.target.value])}\n min={localValue[0]}\n max={max}\n aria-label=\"Maximum price\"\n />\n \u003c/div>\n\n \u003cdiv className=\"range-slider\">\n \u003cinput\n type=\"range\"\n min={min}\n max={max}\n value={localValue[0]}\n onChange={(e) => setLocalValue([+e.target.value, localValue[1]])}\n step={step}\n />\n \u003cinput\n type=\"range\"\n min={min}\n max={max}\n value={localValue[1]}\n onChange={(e) => setLocalValue([localValue[0], +e.target.value])}\n step={step}\n />\n \u003c/div>\n\n \u003cdiv className=\"range-labels\">\n \u003cspan>{prefix}{min}\u003c/span>\n \u003cspan>{prefix}{max}\u003c/span>\n \u003c/div>\n \u003c/div>\n );\n}\n```\n\n### Date Range Picker\n```tsx\nimport { Calendar } from 'lucide-react';\n\nfunction DateRangeFilter({ value, onChange }) {\n const [startDate, endDate] = value;\n\n return (\n \u003cdiv className=\"date-range-filter\">\n \u003cdiv className=\"date-input\">\n \u003cCalendar size={16} />\n \u003cinput\n type=\"date\"\n value={startDate}\n onChange={(e) => onChange([e.target.value, endDate])}\n aria-label=\"Start date\"\n />\n \u003c/div>\n\n \u003cspan className=\"separator\">to\u003c/span>\n\n \u003cdiv className=\"date-input\">\n \u003cCalendar size={16} />\n \u003cinput\n type=\"date\"\n value={endDate}\n onChange={(e) => onChange([startDate, e.target.value])}\n min={startDate}\n aria-label=\"End date\"\n />\n \u003c/div>\n \u003c/div>\n );\n}\n```\n\n## Dropdown Filters\n\n### Single Select Dropdown\n```tsx\ninterface DropdownFilterProps {\n label: string;\n options: { value: string; label: string }[];\n value: string;\n onChange: (value: string) => void;\n placeholder?: string;\n}\n\nexport function DropdownFilter({\n label,\n options,\n value,\n onChange,\n placeholder = 'Select...'\n}: DropdownFilterProps) {\n return (\n \u003cdiv className=\"dropdown-filter\">\n \u003clabel htmlFor={`filter-${label}`}>{label}\u003c/label>\n \u003cselect\n id={`filter-${label}`}\n value={value}\n onChange={(e) => onChange(e.target.value)}\n >\n \u003coption value=\"\">{placeholder}\u003c/option>\n {options.map(option => (\n \u003coption key={option.value} value={option.value}>\n {option.label}\n \u003c/option>\n ))}\n \u003c/select>\n \u003c/div>\n );\n}\n```\n\n### Searchable Dropdown with Downshift\n```tsx\nimport { useCombobox } from 'downshift';\n\nfunction SearchableDropdown({ items, onSelect, placeholder }) {\n const [inputItems, setInputItems] = useState(items);\n\n const {\n isOpen,\n getToggleButtonProps,\n getLabelProps,\n getMenuProps,\n getInputProps,\n highlightedIndex,\n getItemProps,\n selectedItem,\n } = useCombobox({\n items: inputItems,\n onInputValueChange: ({ inputValue }) => {\n setInputItems(\n items.filter(item =>\n item.toLowerCase().includes(inputValue.toLowerCase())\n )\n );\n },\n onSelectedItemChange: ({ selectedItem }) => {\n onSelect(selectedItem);\n },\n });\n\n return (\n \u003cdiv className=\"searchable-dropdown\">\n \u003clabel {...getLabelProps()}>Choose an element:\u003c/label>\n\n \u003cdiv className=\"input-wrapper\">\n \u003cinput\n {...getInputProps()}\n placeholder={placeholder}\n />\n \u003cbutton\n type=\"button\"\n {...getToggleButtonProps()}\n aria-label=\"toggle menu\"\n >\n ↓\n \u003c/button>\n \u003c/div>\n\n \u003cul {...getMenuProps()} className=\"dropdown-menu\">\n {isOpen &&\n inputItems.map((item, index) => (\n \u003cli\n className={highlightedIndex === index ? 'highlighted' : ''}\n key={`${item}${index}`}\n {...getItemProps({ item, index })}\n >\n {item}\n \u003c/li>\n ))}\n \u003c/ul>\n \u003c/div>\n );\n}\n```\n\n## Filter Chips\n\n### Active Filter Display\n```tsx\nimport { X } from 'lucide-react';\n\ninterface FilterChip {\n id: string;\n label: string;\n value: string;\n}\n\ninterface ActiveFiltersProps {\n filters: FilterChip[];\n onRemove: (filterId: string) => void;\n onClearAll: () => void;\n}\n\nexport function ActiveFilters({\n filters,\n onRemove,\n onClearAll\n}: ActiveFiltersProps) {\n if (filters.length === 0) return null;\n\n return (\n \u003cdiv className=\"active-filters\">\n \u003cspan className=\"filters-label\">Active filters:\u003c/span>\n\n {filters.map(filter => (\n \u003cdiv key={filter.id} className=\"filter-chip\">\n \u003cspan>{filter.label}: {filter.value}\u003c/span>\n \u003cbutton\n onClick={() => onRemove(filter.id)}\n aria-label={`Remove ${filter.label} filter`}\n >\n \u003cX size={14} />\n \u003c/button>\n \u003c/div>\n ))}\n\n \u003cbutton\n onClick={onClearAll}\n className=\"clear-all-btn\"\n >\n Clear all\n \u003c/button>\n \u003c/div>\n );\n}\n```\n\n## Faceted Search\n\n### Dynamic Count Updates\n```tsx\ninterface FacetedSearchProps {\n facets: {\n category: string;\n options: Array\u003c{\n value: string;\n label: string;\n count: number;\n disabled?: boolean;\n }>;\n }[];\n selected: Record\u003cstring, string[]>;\n onChange: (category: string, values: string[]) => void;\n}\n\nexport function FacetedSearch({\n facets,\n selected,\n onChange\n}: FacetedSearchProps) {\n return (\n \u003cdiv className=\"faceted-search\">\n {facets.map(facet => (\n \u003cdiv key={facet.category} className=\"facet-group\">\n \u003ch3>{facet.category}\u003c/h3>\n\n {facet.options.map(option => (\n \u003clabel\n key={option.value}\n className={`facet-option ${option.disabled ? 'disabled' : ''}`}\n >\n \u003cinput\n type=\"checkbox\"\n checked={selected[facet.category]?.includes(option.value)}\n onChange={(e) => {\n const current = selected[facet.category] || [];\n if (e.target.checked) {\n onChange(facet.category, [...current, option.value]);\n } else {\n onChange(\n facet.category,\n current.filter(v => v !== option.value)\n );\n }\n }}\n disabled={option.disabled}\n />\n \u003cspan className=\"facet-label\">{option.label}\u003c/span>\n \u003cspan className=\"facet-count\">({option.count})\u003c/span>\n \u003c/label>\n ))}\n \u003c/div>\n ))}\n \u003c/div>\n );\n}\n```\n\n## Mobile Filter Patterns\n\n### Filter Drawer\n```tsx\nimport { Filter, X } from 'lucide-react';\n\nfunction MobileFilterDrawer({ children, filterCount = 0 }) {\n const [isOpen, setIsOpen] = useState(false);\n\n return (\n \u003c>\n \u003cbutton\n onClick={() => setIsOpen(true)}\n className=\"filter-trigger\"\n >\n \u003cFilter />\n Filters\n {filterCount > 0 && (\n \u003cspan className=\"filter-badge\">{filterCount}\u003c/span>\n )}\n \u003c/button>\n\n {isOpen && (\n \u003c>\n \u003cdiv\n className=\"drawer-overlay\"\n onClick={() => setIsOpen(false)}\n />\n\n \u003cdiv className=\"filter-drawer\">\n \u003cdiv className=\"drawer-header\">\n \u003ch2>Filters\u003c/h2>\n \u003cbutton onClick={() => setIsOpen(false)}>\n \u003cX />\n \u003c/button>\n \u003c/div>\n\n \u003cdiv className=\"drawer-content\">\n {children}\n \u003c/div>\n\n \u003cdiv className=\"drawer-footer\">\n \u003cbutton onClick={() => setIsOpen(false)}>\n Apply Filters\n \u003c/button>\n \u003c/div>\n \u003c/div>\n \u003c/>\n )}\n \u003c/>\n );\n}\n```\n\n## Sort Options\n\n### Sort Dropdown\n```tsx\ninterface SortOption {\n value: string;\n label: string;\n}\n\nconst sortOptions: SortOption[] = [\n { value: 'relevance', label: 'Most Relevant' },\n { value: 'price-asc', label: 'Price: Low to High' },\n { value: 'price-desc', label: 'Price: High to Low' },\n { value: 'rating', label: 'Highest Rated' },\n { value: 'newest', label: 'Newest First' },\n];\n\nexport function SortDropdown({ value, onChange }) {\n return (\n \u003cdiv className=\"sort-dropdown\">\n \u003clabel htmlFor=\"sort\">Sort by:\u003c/label>\n \u003cselect\n id=\"sort\"\n value={value}\n onChange={(e) => onChange(e.target.value)}\n >\n {sortOptions.map(option => (\n \u003coption key={option.value} value={option.value}>\n {option.label}\n \u003c/option>\n ))}\n \u003c/select>\n \u003c/div>\n );\n}\n```\n\n## Filter State Management\n\n### Using URL Parameters\n```tsx\nimport { useSearchParams } from 'react-router-dom';\n\nfunction useFilterState() {\n const [searchParams, setSearchParams] = useSearchParams();\n\n const getFilters = () => {\n const filters: Record\u003cstring, string[]> = {};\n\n searchParams.forEach((value, key) => {\n if (!filters[key]) {\n filters[key] = [];\n }\n filters[key].push(value);\n });\n\n return filters;\n };\n\n const updateFilter = (key: string, values: string[]) => {\n const newParams = new URLSearchParams(searchParams);\n\n // Remove existing\n newParams.delete(key);\n\n // Add new values\n values.forEach(value => {\n newParams.append(key, value);\n });\n\n setSearchParams(newParams);\n };\n\n const clearFilters = () => {\n setSearchParams(new URLSearchParams());\n };\n\n return {\n filters: getFilters(),\n updateFilter,\n clearFilters,\n };\n}\n```\n\n## Accessibility Considerations\n\n### Filter Region ARIA\n```tsx\n\u003cdiv\n role=\"region\"\n aria-label=\"Product filters\"\n className=\"filter-panel\"\n>\n \u003ch2 id=\"filter-heading\">Filter Products\u003c/h2>\n\n \u003cdiv\n role=\"group\"\n aria-labelledby=\"filter-heading\"\n >\n {/* Filter groups */}\n \u003c/div>\n\n \u003cdiv aria-live=\"polite\" aria-atomic=\"true\">\n {resultCount} products found\n \u003c/div>\n\u003c/div>\n```\n\n### Keyboard Navigation\n```tsx\n// Ensure all interactive elements are keyboard accessible\n// Tab order should be logical\n// Provide skip links for long filter lists\n\n\u003ca href=\"#results\" className=\"skip-link\">\n Skip to results\n\u003c/a>\n```","content_type":"text/markdown; charset=utf-8","language":"markdown","size":14482,"content_sha256":"0503dddf0d1fc5ae37843d0284492621954bb19ea920748bb9bd7d56dc7f3c51"},{"filename":"references/library-comparison.md","content":"# Search & Filter Library Comparison\n\n\n## Table of Contents\n\n- [Frontend Libraries](#frontend-libraries)\n - [Autocomplete/Combobox Libraries](#autocompletecombobox-libraries)\n - [Search UI Frameworks](#search-ui-frameworks)\n- [Backend Search Technologies](#backend-search-technologies)\n - [Database Full-Text Search](#database-full-text-search)\n - [Dedicated Search Engines](#dedicated-search-engines)\n- [Python Search Libraries](#python-search-libraries)\n - [ORM Integration](#orm-integration)\n - [Elasticsearch Clients](#elasticsearch-clients)\n- [Detailed Library Analysis](#detailed-library-analysis)\n - [Downshift (Recommended for Autocomplete)](#downshift-recommended-for-autocomplete)\n - [React Select (Alternative for Quick Implementation)](#react-select-alternative-for-quick-implementation)\n - [Elasticsearch vs Alternatives](#elasticsearch-vs-alternatives)\n- [Decision Matrix](#decision-matrix)\n - [Choose Downshift when:](#choose-downshift-when)\n - [Choose React Select when:](#choose-react-select-when)\n - [Choose PostgreSQL FTS when:](#choose-postgresql-fts-when)\n - [Choose Elasticsearch when:](#choose-elasticsearch-when)\n - [Choose Algolia when:](#choose-algolia-when)\n - [Choose MeiliSearch when:](#choose-meilisearch-when)\n- [Performance Benchmarks](#performance-benchmarks)\n - [Autocomplete Response Times](#autocomplete-response-times)\n - [Search Engine Query Times](#search-engine-query-times)\n- [Migration Paths](#migration-paths)\n - [From React Autosuggest to Downshift](#from-react-autosuggest-to-downshift)\n - [From PostgreSQL to Elasticsearch](#from-postgresql-to-elasticsearch)\n- [Recommendations by Project Type](#recommendations-by-project-type)\n - [Small E-commerce (\u003c 10K products)](#small-e-commerce-10k-products)\n - [Medium E-commerce (10K - 100K products)](#medium-e-commerce-10k-100k-products)\n - [Large E-commerce (> 100K products)](#large-e-commerce-100k-products)\n - [Internal Dashboard](#internal-dashboard)\n - [SaaS Application](#saas-application)\n\n## Frontend Libraries\n\n### Autocomplete/Combobox Libraries\n\n| Library | Bundle Size | TypeScript | Accessibility | Key Features | Best For |\n|---------|------------|------------|---------------|--------------|----------|\n| **Downshift** | 40KB | ✅ Excellent | ⭐⭐⭐⭐⭐ WAI-ARIA | Headless, flexible, hooks | Custom designs |\n| **React Select** | 160KB | ✅ Native | ⭐⭐⭐⭐ Good | Feature-rich, styled | Quick implementation |\n| **React Autosuggest** | 14KB | ✅ Good | ⭐⭐⭐⭐ Good | Lightweight, simple | Basic autocomplete |\n| **@reach/combobox** | 20KB | ✅ Native | ⭐⭐⭐⭐⭐ Excellent | Accessible, minimal | Accessibility focus |\n| **Headless UI** | 25KB | ✅ Native | ⭐⭐⭐⭐⭐ Excellent | Tailwind integration | Tailwind projects |\n\n### Search UI Frameworks\n\n| Framework | Use Case | Learning Curve | Flexibility | Performance |\n|-----------|----------|----------------|-------------|-------------|\n| **InstantSearch** (Algolia) | Algolia search | Low | Medium | ⭐⭐⭐⭐⭐ |\n| **SearchKit** | Elasticsearch | Medium | High | ⭐⭐⭐⭐ |\n| **Reactive Search** | Elasticsearch | Low | Medium | ⭐⭐⭐⭐ |\n| **MeiliSearch UI** | MeiliSearch | Low | Medium | ⭐⭐⭐⭐⭐ |\n\n## Backend Search Technologies\n\n### Database Full-Text Search\n\n| Database | Setup Complexity | Performance | Features | Best For |\n|----------|-----------------|-------------|----------|----------|\n| **PostgreSQL FTS** | Low | ⭐⭐⭐⭐ Good | Decent, built-in | Small-medium datasets |\n| **MySQL FULLTEXT** | Low | ⭐⭐⭐ Moderate | Basic | Simple searches |\n| **MongoDB Text** | Low | ⭐⭐⭐ Moderate | Basic text search | Document stores |\n| **SQLite FTS5** | Low | ⭐⭐⭐ Good | Surprisingly capable | Embedded/mobile |\n\n### Dedicated Search Engines\n\n| Engine | Performance | Scalability | Setup | Cost | Best For |\n|--------|------------|-------------|-------|------|----------|\n| **Elasticsearch** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | Complex | High | Enterprise search |\n| **Algolia** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | Simple | $/month | SaaS, instant search |\n| **MeiliSearch** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | Simple | Free/OSS | Modern alternative |\n| **Typesense** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | Simple | Free/OSS | Typo-tolerant search |\n| **Sonic** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | Simple | Free/OSS | Lightweight, fast |\n\n## Python Search Libraries\n\n### ORM Integration\n\n| Library | ORM | Features | Performance | Use Case |\n|---------|-----|----------|-------------|----------|\n| **Django Filter** | Django | Declarative filters | ⭐⭐⭐⭐ | Django REST APIs |\n| **SQLAlchemy-Searchable** | SQLAlchemy | PostgreSQL FTS | ⭐⭐⭐⭐ | Flask/FastAPI |\n| **Django Haystack** | Django | Multi-backend | ⭐⭐⭐ | Django + ES/Solr |\n| **Whoosh** | Any | Pure Python | ⭐⭐⭐ | Small projects |\n\n### Elasticsearch Clients\n\n| Client | Abstraction Level | Learning Curve | Features |\n|--------|------------------|----------------|----------|\n| **elasticsearch-py** | Low | High | Full API access |\n| **elasticsearch-dsl** | Medium | Medium | Pythonic queries |\n| **elastic-apm** | N/A | Low | Performance monitoring |\n\n## Detailed Library Analysis\n\n### Downshift (Recommended for Autocomplete)\n\n**Pros:**\n- Fully accessible (WAI-ARIA compliant)\n- Headless - complete control over styling\n- Excellent TypeScript support\n- Hooks-based API\n- Small bundle size for features offered\n- Active maintenance\n\n**Cons:**\n- Requires more setup than pre-styled solutions\n- Need to implement visual design\n- Learning curve for advanced features\n\n**Installation:**\n```bash\nnpm install downshift\n```\n\n**Basic Example:**\n```tsx\nimport { useCombobox } from 'downshift';\n\nfunction Autocomplete({ items, onSelect }) {\n const {\n isOpen,\n getToggleButtonProps,\n getMenuProps,\n getInputProps,\n highlightedIndex,\n getItemProps,\n } = useCombobox({\n items,\n onSelectedItemChange: ({ selectedItem }) => onSelect(selectedItem)\n });\n\n // Render UI with spread props\n}\n```\n\n### React Select (Alternative for Quick Implementation)\n\n**Pros:**\n- Feature-rich out of the box\n- Pre-styled with theming support\n- Async/creatable/multi-select variants\n- Good documentation\n- Large community\n\n**Cons:**\n- Large bundle size (160KB)\n- Opinionated styling\n- Harder to customize deeply\n- Some accessibility issues in edge cases\n\n**Installation:**\n```bash\nnpm install react-select\n```\n\n### Elasticsearch vs Alternatives\n\n**Elasticsearch:**\n- ✅ Industry standard\n- ✅ Powerful query DSL\n- ✅ Excellent performance\n- ❌ Resource intensive\n- ❌ Complex setup and maintenance\n- ❌ Expensive at scale\n\n**MeiliSearch:**\n- ✅ Simple setup\n- ✅ Typo-tolerant by default\n- ✅ Fast indexing\n- ✅ Lower resource usage\n- ❌ Fewer advanced features\n- ❌ Smaller ecosystem\n\n**Algolia:**\n- ✅ Fastest search responses\n- ✅ Zero infrastructure\n- ✅ Excellent developer experience\n- ❌ Expensive for large datasets\n- ❌ Vendor lock-in\n- ❌ Data leaves your infrastructure\n\n## Decision Matrix\n\n### Choose Downshift when:\n- Accessibility is critical\n- Need full control over UI\n- Building a design system\n- Want minimal bundle size\n- Using TypeScript\n\n### Choose React Select when:\n- Need quick implementation\n- OK with larger bundle\n- Want pre-built features\n- Don't need deep customization\n\n### Choose PostgreSQL FTS when:\n- Data already in PostgreSQL\n- \u003c 1 million searchable records\n- Simple search requirements\n- Want to avoid additional infrastructure\n\n### Choose Elasticsearch when:\n- > 1 million records\n- Need complex search features\n- Multi-language support required\n- Faceted search is critical\n- Have DevOps resources\n\n### Choose Algolia when:\n- Need instant global search\n- SaaS/e-commerce application\n- Can afford the pricing\n- Want zero infrastructure\n\n### Choose MeiliSearch when:\n- Want Algolia-like experience\n- Need on-premise solution\n- Cost is a concern\n- Moderate scale (\u003c 10M records)\n\n## Performance Benchmarks\n\n### Autocomplete Response Times\n| Library | First Render | Typing Lag | 1K Items | 10K Items |\n|---------|--------------|------------|----------|-----------|\n| Downshift | 15ms | \u003c5ms | 20ms | 150ms* |\n| React Select | 45ms | 10ms | 35ms | 400ms |\n| Native datalist | 5ms | 0ms | 50ms | 500ms |\n\n*With virtualization\n\n### Search Engine Query Times\n| Engine | Simple Query | Complex Query | Faceted Search | 1M Records |\n|--------|--------------|---------------|----------------|------------|\n| PostgreSQL | 10ms | 50ms | 100ms | 200ms |\n| Elasticsearch | 5ms | 15ms | 20ms | 25ms |\n| MeiliSearch | 3ms | 10ms | 15ms | 20ms |\n| Algolia | 2ms | 5ms | 8ms | 10ms |\n\n## Migration Paths\n\n### From React Autosuggest to Downshift\n```tsx\n// React Autosuggest\n\u003cAutosuggest\n suggestions={suggestions}\n onSuggestionsFetchRequested={onFetch}\n getSuggestionValue={getValue}\n renderSuggestion={renderItem}\n/>\n\n// Downshift equivalent\nconst {...props} = useCombobox({\n items: suggestions,\n onInputValueChange: ({ inputValue }) => onFetch(inputValue),\n itemToString: getValue\n});\n// Custom render with props\n```\n\n### From PostgreSQL to Elasticsearch\n```python\n# PostgreSQL FTS\nquery = session.query(Product).filter(\n func.to_tsvector('english', Product.title).match(search_term)\n)\n\n# Elasticsearch equivalent\nresults = es.search(\n index='products',\n body={\n 'query': {\n 'match': {\n 'title': search_term\n }\n }\n }\n)\n```\n\n## Recommendations by Project Type\n\n### Small E-commerce (\u003c 10K products)\n- **Frontend**: Downshift + React Query\n- **Backend**: PostgreSQL FTS\n- **API**: REST with query parameters\n\n### Medium E-commerce (10K - 100K products)\n- **Frontend**: Downshift + SWR\n- **Backend**: MeiliSearch or PostgreSQL with indexes\n- **API**: GraphQL or REST with pagination\n\n### Large E-commerce (> 100K products)\n- **Frontend**: InstantSearch or custom with Downshift\n- **Backend**: Elasticsearch or Algolia\n- **API**: REST with CDN caching\n\n### Internal Dashboard\n- **Frontend**: React Select (faster development)\n- **Backend**: Database full-text search\n- **API**: Simple REST\n\n### SaaS Application\n- **Frontend**: Downshift with custom design\n- **Backend**: MeiliSearch or Typesense\n- **API**: REST with rate limiting","content_type":"text/markdown; charset=utf-8","language":"markdown","size":10282,"content_sha256":"132e0d438b84c6a9736f5d7bd451299a9497524c35aabdb71f6c6f512800f7d1"},{"filename":"references/performance-optimization.md","content":"# Search Performance Optimization\n\n\n## Table of Contents\n\n- [Frontend Performance](#frontend-performance)\n - [Debouncing and Throttling](#debouncing-and-throttling)\n - [Request Cancellation](#request-cancellation)\n - [Result Caching](#result-caching)\n- [Backend Performance](#backend-performance)\n - [Database Index Optimization](#database-index-optimization)\n - [Query Optimization Patterns](#query-optimization-patterns)\n - [Elasticsearch Performance Tuning](#elasticsearch-performance-tuning)\n- [Monitoring and Metrics](#monitoring-and-metrics)\n - [Performance Tracking](#performance-tracking)\n\n## Frontend Performance\n\n### Debouncing and Throttling\n```typescript\nimport { useCallback, useRef } from 'react';\n\n/**\n * Custom debounce hook with cancellation\n */\nexport function useDebounce\u003cT extends (...args: any[]) => any>(\n callback: T,\n delay: number\n): [T, () => void] {\n const timeoutRef = useRef\u003cNodeJS.Timeout>();\n\n const cancel = useCallback(() => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current);\n }\n }, []);\n\n const debouncedCallback = useCallback(\n (...args: Parameters\u003cT>) => {\n cancel();\n timeoutRef.current = setTimeout(() => {\n callback(...args);\n }, delay);\n },\n [callback, delay, cancel]\n ) as T;\n\n return [debouncedCallback, cancel];\n}\n\n/**\n * Custom throttle hook\n */\nexport function useThrottle\u003cT extends (...args: any[]) => any>(\n callback: T,\n limit: number\n): T {\n const inThrottle = useRef(false);\n\n const throttledCallback = useCallback(\n (...args: Parameters\u003cT>) => {\n if (!inThrottle.current) {\n callback(...args);\n inThrottle.current = true;\n setTimeout(() => {\n inThrottle.current = false;\n }, limit);\n }\n },\n [callback, limit]\n ) as T;\n\n return throttledCallback;\n}\n\n// Adaptive debouncing based on input speed\nexport function useAdaptiveDebounce(\n callback: (value: string) => void,\n minDelay = 200,\n maxDelay = 500\n) {\n const lastInputTime = useRef\u003cnumber>(Date.now());\n const inputSpeed = useRef\u003cnumber[]>([]);\n const timeoutRef = useRef\u003cNodeJS.Timeout>();\n\n const calculateDelay = () => {\n if (inputSpeed.current.length \u003c 2) return maxDelay;\n\n const avgSpeed = inputSpeed.current.reduce((a, b) => a + b, 0) / inputSpeed.current.length;\n\n // Faster typing = shorter delay\n if (avgSpeed \u003c 100) return minDelay;\n if (avgSpeed \u003c 200) return (minDelay + maxDelay) / 2;\n return maxDelay;\n };\n\n return useCallback((value: string) => {\n const now = Date.now();\n const timeSinceLastInput = now - lastInputTime.current;\n lastInputTime.current = now;\n\n // Track input speed\n inputSpeed.current.push(timeSinceLastInput);\n if (inputSpeed.current.length > 5) {\n inputSpeed.current.shift();\n }\n\n // Clear existing timeout\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current);\n }\n\n // Set new timeout with adaptive delay\n const delay = calculateDelay();\n timeoutRef.current = setTimeout(() => {\n callback(value);\n }, delay);\n }, [callback, minDelay, maxDelay]);\n}\n```\n\n### Request Cancellation\n```typescript\nclass SearchRequestManager {\n private abortController: AbortController | null = null;\n\n /**\n * Execute search with automatic cancellation of previous requests\n */\n async search(query: string, filters: any): Promise\u003cany> {\n // Cancel previous request\n this.cancel();\n\n // Create new abort controller\n this.abortController = new AbortController();\n\n try {\n const response = await fetch('/api/search', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ query, filters }),\n signal: this.abortController.signal\n });\n\n if (!response.ok) {\n throw new Error(`Search failed: ${response.status}`);\n }\n\n return await response.json();\n } catch (error) {\n if (error.name === 'AbortError') {\n // Request was cancelled, return null\n return null;\n }\n throw error;\n }\n }\n\n /**\n * Cancel current request\n */\n cancel(): void {\n if (this.abortController) {\n this.abortController.abort();\n this.abortController = null;\n }\n }\n}\n\n// React hook for request management\nexport function useSearchRequest() {\n const requestManager = useRef(new SearchRequestManager());\n\n useEffect(() => {\n return () => {\n // Cancel any pending request on unmount\n requestManager.current.cancel();\n };\n }, []);\n\n const search = useCallback(async (query: string, filters: any) => {\n return requestManager.current.search(query, filters);\n }, []);\n\n const cancel = useCallback(() => {\n requestManager.current.cancel();\n }, []);\n\n return { search, cancel };\n}\n```\n\n### Result Caching\n```typescript\ninterface CacheEntry\u003cT> {\n data: T;\n timestamp: number;\n hits: number;\n}\n\nclass SearchCache\u003cT> {\n private cache = new Map\u003cstring, CacheEntry\u003cT>>();\n private maxSize: number;\n private ttl: number; // Time to live in milliseconds\n\n constructor(maxSize = 50, ttl = 5 * 60 * 1000) {\n this.maxSize = maxSize;\n this.ttl = ttl;\n }\n\n /**\n * Generate cache key from search parameters\n */\n private getCacheKey(params: any): string {\n return JSON.stringify(params, Object.keys(params).sort());\n }\n\n /**\n * Get cached result if available and not expired\n */\n get(params: any): T | null {\n const key = this.getCacheKey(params);\n const entry = this.cache.get(key);\n\n if (!entry) return null;\n\n // Check if expired\n if (Date.now() - entry.timestamp > this.ttl) {\n this.cache.delete(key);\n return null;\n }\n\n // Update hit count for LRU\n entry.hits++;\n return entry.data;\n }\n\n /**\n * Store result in cache\n */\n set(params: any, data: T): void {\n const key = this.getCacheKey(params);\n\n // Check cache size and evict if needed\n if (this.cache.size >= this.maxSize && !this.cache.has(key)) {\n this.evictLRU();\n }\n\n this.cache.set(key, {\n data,\n timestamp: Date.now(),\n hits: 0\n });\n }\n\n /**\n * Evict least recently used entry\n */\n private evictLRU(): void {\n let minHits = Infinity;\n let lruKey = '';\n\n for (const [key, entry] of this.cache) {\n if (entry.hits \u003c minHits) {\n minHits = entry.hits;\n lruKey = key;\n }\n }\n\n if (lruKey) {\n this.cache.delete(lruKey);\n }\n }\n\n /**\n * Clear entire cache\n */\n clear(): void {\n this.cache.clear();\n }\n\n /**\n * Get cache statistics\n */\n getStats() {\n return {\n size: this.cache.size,\n maxSize: this.maxSize,\n entries: Array.from(this.cache.entries()).map(([key, entry]) => ({\n key,\n age: Date.now() - entry.timestamp,\n hits: entry.hits\n }))\n };\n }\n}\n\n// React hook for cached search\nexport function useCachedSearch\u003cT>() {\n const cache = useRef(new SearchCache\u003cT>());\n const [stats, setStats] = useState(cache.current.getStats());\n\n const search = useCallback(async (\n params: any,\n fetcher: () => Promise\u003cT>\n ): Promise\u003cT> => {\n // Check cache first\n const cached = cache.current.get(params);\n if (cached !== null) {\n console.log('Cache hit for:', params);\n return cached;\n }\n\n // Fetch and cache\n console.log('Cache miss, fetching:', params);\n const result = await fetcher();\n cache.current.set(params, result);\n\n // Update stats\n setStats(cache.current.getStats());\n\n return result;\n }, []);\n\n const clearCache = useCallback(() => {\n cache.current.clear();\n setStats(cache.current.getStats());\n }, []);\n\n return { search, clearCache, stats };\n}\n```\n\n## Backend Performance\n\n### Database Index Optimization\n```sql\n-- PostgreSQL indexes for search optimization\n\n-- Single column indexes\nCREATE INDEX idx_products_title ON products USING gin(to_tsvector('english', title));\nCREATE INDEX idx_products_description ON products USING gin(to_tsvector('english', description));\nCREATE INDEX idx_products_category ON products(category);\nCREATE INDEX idx_products_brand ON products(brand);\nCREATE INDEX idx_products_price ON products(price);\nCREATE INDEX idx_products_created_at ON products(created_at DESC);\nCREATE INDEX idx_products_rating ON products(rating DESC);\n\n-- Composite indexes for common filter combinations\nCREATE INDEX idx_products_category_price ON products(category, price);\nCREATE INDEX idx_products_brand_price ON products(brand, price);\nCREATE INDEX idx_products_category_brand ON products(category, brand);\n\n-- Partial indexes for common conditions\nCREATE INDEX idx_products_in_stock ON products(id) WHERE in_stock = true;\nCREATE INDEX idx_products_featured ON products(id) WHERE featured = true;\nCREATE INDEX idx_products_on_sale ON products(id) WHERE sale_price IS NOT NULL;\n\n-- Full-text search index\nCREATE INDEX idx_products_search_vector ON products\nUSING gin((\n setweight(to_tsvector('english', coalesce(title, '')), 'A') ||\n setweight(to_tsvector('english', coalesce(description, '')), 'B') ||\n setweight(to_tsvector('english', coalesce(tags, '')), 'C')\n));\n\n-- BRIN index for time-series data\nCREATE INDEX idx_products_created_brin ON products USING brin(created_at);\n```\n\n### Query Optimization Patterns\n```python\nfrom sqlalchemy import select, func, and_, or_\nfrom sqlalchemy.orm import selectinload, joinedload\n\nclass OptimizedSearchQueries:\n \"\"\"Optimized database query patterns.\"\"\"\n\n @staticmethod\n def search_with_pagination(session, query, filters, page=1, per_page=20):\n \"\"\"Optimized search with count query separation.\"\"\"\n\n # Build base query\n base_query = session.query(Product)\n\n # Apply filters\n if query:\n base_query = base_query.filter(\n or_(\n Product.title.ilike(f'%{query}%'),\n Product.description.ilike(f'%{query}%')\n )\n )\n\n if filters.get('category'):\n base_query = base_query.filter(Product.category.in_(filters['category']))\n\n if filters.get('min_price'):\n base_query = base_query.filter(Product.price >= filters['min_price'])\n\n # Separate count query (without joins/eager loading)\n count_query = base_query.with_entities(func.count(Product.id))\n total = count_query.scalar()\n\n # Main query with eager loading\n results_query = base_query.options(\n selectinload(Product.images),\n selectinload(Product.reviews)\n )\n\n # Apply pagination\n offset = (page - 1) * per_page\n results = results_query.offset(offset).limit(per_page).all()\n\n return {\n 'results': results,\n 'total': total,\n 'page': page,\n 'per_page': per_page\n }\n\n @staticmethod\n def get_facet_counts(session, base_filters=None):\n \"\"\"Get facet counts with single query using window functions.\"\"\"\n\n # Use CTE for base filtered results\n base_query = session.query(Product.id)\n\n if base_filters:\n # Apply base filters\n pass\n\n base_cte = base_query.cte('base_products')\n\n # Get all facets in single query using UNION ALL\n facets_query = session.query(\n literal('category').label('facet_type'),\n Product.category.label('facet_value'),\n func.count(Product.id).label('count')\n ).join(\n base_cte, Product.id == base_cte.c.id\n ).group_by(Product.category)\n\n # Add more facets\n facets_query = facets_query.union_all(\n session.query(\n literal('brand'),\n Product.brand,\n func.count(Product.id)\n ).join(\n base_cte, Product.id == base_cte.c.id\n ).group_by(Product.brand)\n )\n\n results = facets_query.all()\n\n # Group by facet type\n facets = {}\n for facet_type, facet_value, count in results:\n if facet_type not in facets:\n facets[facet_type] = []\n facets[facet_type].append({\n 'value': facet_value,\n 'count': count\n })\n\n return facets\n```\n\n### Elasticsearch Performance Tuning\n```python\nclass ElasticsearchOptimization:\n \"\"\"Elasticsearch performance optimization strategies.\"\"\"\n\n @staticmethod\n def create_optimized_mapping():\n \"\"\"Create mapping optimized for search performance.\"\"\"\n return {\n 'settings': {\n 'number_of_shards': 2,\n 'number_of_replicas': 1,\n 'index': {\n 'refresh_interval': '5s', # Reduce refresh frequency\n 'max_result_window': 10000, # Limit deep pagination\n 'max_inner_result_window': 100,\n 'search': {\n 'slowlog': {\n 'threshold': {\n 'query': {\n 'warn': '10s',\n 'info': '5s'\n }\n }\n }\n }\n },\n 'analysis': {\n 'analyzer': {\n 'search_analyzer': {\n 'type': 'custom',\n 'tokenizer': 'standard',\n 'filter': [\n 'lowercase',\n 'stop',\n 'snowball',\n 'synonym_filter'\n ]\n }\n },\n 'filter': {\n 'synonym_filter': {\n 'type': 'synonym',\n 'synonyms': [\n 'laptop,notebook',\n 'phone,mobile,cell',\n 'tv,television'\n ]\n }\n }\n }\n },\n 'mappings': {\n 'properties': {\n 'title': {\n 'type': 'text',\n 'analyzer': 'search_analyzer',\n 'search_analyzer': 'search_analyzer',\n 'fields': {\n 'keyword': {\n 'type': 'keyword',\n 'ignore_above': 256\n },\n 'ngram': {\n 'type': 'text',\n 'analyzer': 'ngram_analyzer'\n }\n }\n },\n 'description': {\n 'type': 'text',\n 'analyzer': 'search_analyzer',\n 'index_options': 'offsets' # For highlighting\n },\n 'category': {\n 'type': 'keyword',\n 'eager_global_ordinals': True # For aggregations\n },\n 'price': {\n 'type': 'scaled_float',\n 'scaling_factor': 100 # Store as cents\n },\n 'suggest': {\n 'type': 'completion', # For autocomplete\n 'analyzer': 'simple'\n }\n }\n }\n }\n\n @staticmethod\n def search_with_request_cache(es, query, use_cache=True):\n \"\"\"Use request cache for aggregations.\"\"\"\n body = {\n 'query': query,\n 'aggs': {\n 'categories': {\n 'terms': {\n 'field': 'category',\n 'size': 20\n }\n }\n },\n 'request_cache': use_cache # Enable request cache\n }\n\n return es.search(index='products', body=body)\n\n @staticmethod\n def bulk_index_optimized(es, documents, batch_size=500):\n \"\"\"Optimized bulk indexing.\"\"\"\n from elasticsearch.helpers import bulk, parallel_bulk\n\n def generate_actions():\n for doc in documents:\n yield {\n '_index': 'products',\n '_id': doc['id'],\n '_source': doc,\n '_op_type': 'index' # Use 'create' to avoid updates\n }\n\n # Use parallel bulk for large datasets\n if len(documents) > 10000:\n for success, info in parallel_bulk(\n es,\n generate_actions(),\n chunk_size=batch_size,\n thread_count=4,\n raise_on_error=False\n ):\n if not success:\n print(f\"Failed to index: {info}\")\n else:\n bulk(es, generate_actions(), chunk_size=batch_size)\n```\n\n## Monitoring and Metrics\n\n### Performance Tracking\n```typescript\nclass PerformanceMonitor {\n private metrics: Map\u003cstring, number[]> = new Map();\n\n /**\n * Measure operation performance\n */\n async measure\u003cT>(\n operation: string,\n fn: () => Promise\u003cT>\n ): Promise\u003cT> {\n const start = performance.now();\n\n try {\n const result = await fn();\n const duration = performance.now() - start;\n\n this.recordMetric(operation, duration);\n\n // Log slow operations\n if (duration > 1000) {\n console.warn(`Slow operation: ${operation} took ${duration.toFixed(2)}ms`);\n }\n\n return result;\n } catch (error) {\n const duration = performance.now() - start;\n this.recordMetric(`${operation}_error`, duration);\n throw error;\n }\n }\n\n /**\n * Record metric\n */\n private recordMetric(operation: string, duration: number): void {\n if (!this.metrics.has(operation)) {\n this.metrics.set(operation, []);\n }\n\n const values = this.metrics.get(operation)!;\n values.push(duration);\n\n // Keep only last 100 measurements\n if (values.length > 100) {\n values.shift();\n }\n }\n\n /**\n * Get performance statistics\n */\n getStats(operation?: string): any {\n if (operation) {\n const values = this.metrics.get(operation);\n if (!values || values.length === 0) {\n return null;\n }\n\n return this.calculateStats(values);\n }\n\n // Get stats for all operations\n const allStats: any = {};\n for (const [op, values] of this.metrics) {\n allStats[op] = this.calculateStats(values);\n }\n\n return allStats;\n }\n\n /**\n * Calculate statistics from values\n */\n private calculateStats(values: number[]) {\n const sorted = [...values].sort((a, b) => a - b);\n const sum = values.reduce((a, b) => a + b, 0);\n\n return {\n count: values.length,\n mean: sum / values.length,\n median: sorted[Math.floor(sorted.length / 2)],\n min: sorted[0],\n max: sorted[sorted.length - 1],\n p95: sorted[Math.floor(sorted.length * 0.95)],\n p99: sorted[Math.floor(sorted.length * 0.99)]\n };\n }\n\n /**\n * Send metrics to analytics service\n */\n report(): void {\n const stats = this.getStats();\n\n // Send to analytics service\n if (typeof window !== 'undefined' && window.gtag) {\n Object.entries(stats).forEach(([operation, metrics]: [string, any]) => {\n window.gtag('event', 'performance', {\n event_category: 'search',\n event_label: operation,\n value: Math.round(metrics.mean)\n });\n });\n }\n }\n}\n\n// Usage\nconst monitor = new PerformanceMonitor();\n\nexport async function searchWithMonitoring(query: string, filters: any) {\n return monitor.measure('search_request', async () => {\n const response = await fetch('/api/search', {\n method: 'POST',\n body: JSON.stringify({ query, filters })\n });\n\n return monitor.measure('search_parse', async () => {\n return response.json();\n });\n });\n}\n```","content_type":"text/markdown; charset=utf-8","language":"markdown","size":20109,"content_sha256":"15ea9bdbb82b70dcb52a1a8fb92e04f11d846148353823f21cc2d6dc1f99c75d"},{"filename":"references/query-parameter-management.md","content":"# Query Parameter Management\n\n\n## Table of Contents\n\n- [URL State Synchronization](#url-state-synchronization)\n - [React Router Integration](#react-router-integration)\n - [Next.js URL Management](#nextjs-url-management)\n- [Complex Query Compression](#complex-query-compression)\n - [Base64 Encoding for Complex Filters](#base64-encoding-for-complex-filters)\n- [Shareable Search URLs](#shareable-search-urls)\n - [Creating Shareable Links](#creating-shareable-links)\n- [History Management](#history-management)\n - [Search History with Local Storage](#search-history-with-local-storage)\n- [Deep Linking Support](#deep-linking-support)\n - [Handling Deep Links](#handling-deep-links)\n- [Validation and Sanitization](#validation-and-sanitization)\n - [URL Parameter Validation](#url-parameter-validation)\n\n## URL State Synchronization\n\n### React Router Integration\n```tsx\nimport { useSearchParams, useNavigate } from 'react-router-dom';\nimport { useEffect, useState } from 'react';\n\ninterface FilterState {\n query?: string;\n categories?: string[];\n minPrice?: number;\n maxPrice?: number;\n sortBy?: string;\n page?: number;\n}\n\nexport function useUrlFilters() {\n const [searchParams, setSearchParams] = useSearchParams();\n const navigate = useNavigate();\n\n // Parse URL parameters to filter state\n const getFiltersFromUrl = (): FilterState => {\n const filters: FilterState = {};\n\n // Parse query\n const query = searchParams.get('q');\n if (query) filters.query = query;\n\n // Parse array parameters\n const categories = searchParams.getAll('category');\n if (categories.length > 0) filters.categories = categories;\n\n // Parse number parameters\n const minPrice = searchParams.get('min_price');\n if (minPrice) filters.minPrice = parseFloat(minPrice);\n\n const maxPrice = searchParams.get('max_price');\n if (maxPrice) filters.maxPrice = parseFloat(maxPrice);\n\n // Parse other parameters\n const sortBy = searchParams.get('sort');\n if (sortBy) filters.sortBy = sortBy;\n\n const page = searchParams.get('page');\n if (page) filters.page = parseInt(page, 10);\n\n return filters;\n };\n\n // Update URL with new filters\n const setFiltersToUrl = (filters: FilterState, replace = false) => {\n const params = new URLSearchParams();\n\n // Add query\n if (filters.query) {\n params.set('q', filters.query);\n }\n\n // Add array parameters\n if (filters.categories && filters.categories.length > 0) {\n filters.categories.forEach(cat => params.append('category', cat));\n }\n\n // Add number parameters\n if (filters.minPrice !== undefined) {\n params.set('min_price', filters.minPrice.toString());\n }\n\n if (filters.maxPrice !== undefined) {\n params.set('max_price', filters.maxPrice.toString());\n }\n\n // Add other parameters\n if (filters.sortBy) {\n params.set('sort', filters.sortBy);\n }\n\n if (filters.page && filters.page > 1) {\n params.set('page', filters.page.toString());\n }\n\n // Update URL\n if (replace) {\n navigate({ search: params.toString() }, { replace: true });\n } else {\n setSearchParams(params);\n }\n };\n\n // Update single filter\n const updateFilter = (key: keyof FilterState, value: any) => {\n const currentFilters = getFiltersFromUrl();\n const newFilters = { ...currentFilters };\n\n if (value === null || value === undefined ||\n (Array.isArray(value) && value.length === 0)) {\n delete newFilters[key];\n } else {\n newFilters[key] = value;\n }\n\n // Reset page when filters change\n if (key !== 'page') {\n delete newFilters.page;\n }\n\n setFiltersToUrl(newFilters);\n };\n\n // Clear all filters\n const clearFilters = () => {\n setSearchParams(new URLSearchParams());\n };\n\n return {\n filters: getFiltersFromUrl(),\n updateFilter,\n setFilters: setFiltersToUrl,\n clearFilters\n };\n}\n```\n\n### Next.js URL Management\n```tsx\nimport { useRouter } from 'next/router';\nimport { ParsedUrlQuery } from 'querystring';\n\nexport function useNextUrlFilters() {\n const router = useRouter();\n\n // Parse query object to filter state\n const parseQuery = (query: ParsedUrlQuery): FilterState => {\n const filters: FilterState = {};\n\n if (query.q && typeof query.q === 'string') {\n filters.query = query.q;\n }\n\n if (query.category) {\n filters.categories = Array.isArray(query.category)\n ? query.category\n : [query.category];\n }\n\n if (query.min_price && typeof query.min_price === 'string') {\n filters.minPrice = parseFloat(query.min_price);\n }\n\n if (query.max_price && typeof query.max_price === 'string') {\n filters.maxPrice = parseFloat(query.max_price);\n }\n\n if (query.sort && typeof query.sort === 'string') {\n filters.sortBy = query.sort;\n }\n\n if (query.page && typeof query.page === 'string') {\n filters.page = parseInt(query.page, 10);\n }\n\n return filters;\n };\n\n // Build query object from filters\n const buildQuery = (filters: FilterState): ParsedUrlQuery => {\n const query: ParsedUrlQuery = {};\n\n if (filters.query) query.q = filters.query;\n if (filters.categories && filters.categories.length > 0) {\n query.category = filters.categories;\n }\n if (filters.minPrice !== undefined) {\n query.min_price = filters.minPrice.toString();\n }\n if (filters.maxPrice !== undefined) {\n query.max_price = filters.maxPrice.toString();\n }\n if (filters.sortBy) query.sort = filters.sortBy;\n if (filters.page && filters.page > 1) {\n query.page = filters.page.toString();\n }\n\n return query;\n };\n\n // Update URL with new filters\n const updateFilters = (filters: FilterState, options?: { shallow?: boolean }) => {\n const query = buildQuery(filters);\n\n router.push(\n {\n pathname: router.pathname,\n query\n },\n undefined,\n { shallow: options?.shallow ?? true }\n );\n };\n\n return {\n filters: parseQuery(router.query),\n updateFilters,\n clearFilters: () => updateFilters({})\n };\n}\n```\n\n## Complex Query Compression\n\n### Base64 Encoding for Complex Filters\n```typescript\ninterface ComplexFilter {\n query?: string;\n filters?: {\n [key: string]: any;\n };\n advanced?: {\n must?: string[];\n should?: string[];\n mustNot?: string[];\n };\n dateRange?: {\n start: Date;\n end: Date;\n };\n}\n\nclass QueryCompressor {\n /**\n * Compress complex filter object to URL-safe string\n */\n static compress(filters: ComplexFilter): string {\n try {\n // Convert to JSON string\n const jsonString = JSON.stringify(filters);\n\n // Compress using base64\n const base64 = btoa(encodeURIComponent(jsonString));\n\n // Make URL-safe\n return base64\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n .replace(/=/g, '');\n } catch (error) {\n console.error('Failed to compress filters:', error);\n return '';\n }\n }\n\n /**\n * Decompress URL string back to filter object\n */\n static decompress(compressed: string): ComplexFilter | null {\n try {\n // Restore base64 padding\n const padding = '='.repeat((4 - (compressed.length % 4)) % 4);\n const base64 = compressed\n .replace(/-/g, '+')\n .replace(/_/g, '/')\n + padding;\n\n // Decode from base64\n const jsonString = decodeURIComponent(atob(base64));\n\n // Parse JSON\n return JSON.parse(jsonString);\n } catch (error) {\n console.error('Failed to decompress filters:', error);\n return null;\n }\n }\n}\n\n// Usage with React hook\nexport function useCompressedFilters() {\n const [searchParams, setSearchParams] = useSearchParams();\n\n const getFilters = (): ComplexFilter => {\n const compressed = searchParams.get('f');\n if (!compressed) return {};\n\n return QueryCompressor.decompress(compressed) || {};\n };\n\n const setFilters = (filters: ComplexFilter) => {\n const compressed = QueryCompressor.compress(filters);\n const params = new URLSearchParams();\n\n if (compressed) {\n params.set('f', compressed);\n }\n\n setSearchParams(params);\n };\n\n return { filters: getFilters(), setFilters };\n}\n```\n\n## Shareable Search URLs\n\n### Creating Shareable Links\n```tsx\ninterface ShareableSearchProps {\n filters: FilterState;\n baseUrl?: string;\n}\n\nexport function ShareableSearch({ filters, baseUrl = window.location.origin }: ShareableSearchProps) {\n const [shareUrl, setShareUrl] = useState('');\n const [copied, setCopied] = useState(false);\n\n // Generate shareable URL\n const generateShareUrl = () => {\n const params = new URLSearchParams();\n\n // Add all active filters\n Object.entries(filters).forEach(([key, value]) => {\n if (value !== undefined && value !== null) {\n if (Array.isArray(value)) {\n value.forEach(v => params.append(key, v.toString()));\n } else {\n params.set(key, value.toString());\n }\n }\n });\n\n const url = `${baseUrl}/search?${params.toString()}`;\n setShareUrl(url);\n return url;\n };\n\n // Copy to clipboard\n const copyToClipboard = async () => {\n const url = generateShareUrl();\n\n try {\n await navigator.clipboard.writeText(url);\n setCopied(true);\n setTimeout(() => setCopied(false), 2000);\n } catch (error) {\n console.error('Failed to copy:', error);\n }\n };\n\n // Share via Web Share API\n const share = async () => {\n const url = generateShareUrl();\n\n if (navigator.share) {\n try {\n await navigator.share({\n title: 'Search Results',\n text: 'Check out these search results',\n url\n });\n } catch (error) {\n console.error('Share failed:', error);\n }\n } else {\n copyToClipboard();\n }\n };\n\n return (\n \u003cdiv className=\"shareable-search\">\n \u003cbutton onClick={copyToClipboard} className=\"copy-btn\">\n {copied ? '✅ Copied!' : '📋 Copy Link'}\n \u003c/button>\n\n \u003cbutton onClick={share} className=\"share-btn\">\n 📤 Share\n \u003c/button>\n\n {shareUrl && (\n \u003cinput\n type=\"text\"\n value={shareUrl}\n readOnly\n className=\"share-url-input\"\n />\n )}\n \u003c/div>\n );\n}\n```\n\n## History Management\n\n### Search History with Local Storage\n```tsx\ninterface SearchHistoryEntry {\n id: string;\n query: string;\n filters: FilterState;\n timestamp: Date;\n resultCount?: number;\n}\n\nclass SearchHistory {\n private static readonly STORAGE_KEY = 'search_history';\n private static readonly MAX_ENTRIES = 20;\n\n /**\n * Save search to history\n */\n static save(entry: Omit\u003cSearchHistoryEntry, 'id' | 'timestamp'>): void {\n const history = this.getAll();\n\n const newEntry: SearchHistoryEntry = {\n ...entry,\n id: Date.now().toString(),\n timestamp: new Date()\n };\n\n // Add to beginning of array\n history.unshift(newEntry);\n\n // Limit history size\n if (history.length > this.MAX_ENTRIES) {\n history.pop();\n }\n\n // Save to local storage\n localStorage.setItem(this.STORAGE_KEY, JSON.stringify(history));\n }\n\n /**\n * Get all history entries\n */\n static getAll(): SearchHistoryEntry[] {\n try {\n const stored = localStorage.getItem(this.STORAGE_KEY);\n if (!stored) return [];\n\n const history = JSON.parse(stored);\n // Parse dates\n return history.map((entry: any) => ({\n ...entry,\n timestamp: new Date(entry.timestamp)\n }));\n } catch (error) {\n console.error('Failed to load search history:', error);\n return [];\n }\n }\n\n /**\n * Get recent searches\n */\n static getRecent(count = 5): SearchHistoryEntry[] {\n return this.getAll().slice(0, count);\n }\n\n /**\n * Clear all history\n */\n static clear(): void {\n localStorage.removeItem(this.STORAGE_KEY);\n }\n\n /**\n * Remove specific entry\n */\n static remove(id: string): void {\n const history = this.getAll().filter(entry => entry.id !== id);\n localStorage.setItem(this.STORAGE_KEY, JSON.stringify(history));\n }\n}\n\n// React hook for search history\nexport function useSearchHistory() {\n const [history, setHistory] = useState\u003cSearchHistoryEntry[]>([]);\n\n useEffect(() => {\n setHistory(SearchHistory.getAll());\n }, []);\n\n const saveSearch = (query: string, filters: FilterState, resultCount?: number) => {\n SearchHistory.save({ query, filters, resultCount });\n setHistory(SearchHistory.getAll());\n };\n\n const clearHistory = () => {\n SearchHistory.clear();\n setHistory([]);\n };\n\n const removeEntry = (id: string) => {\n SearchHistory.remove(id);\n setHistory(SearchHistory.getAll());\n };\n\n return {\n history,\n recentSearches: SearchHistory.getRecent(),\n saveSearch,\n clearHistory,\n removeEntry\n };\n}\n```\n\n## Deep Linking Support\n\n### Handling Deep Links\n```tsx\nimport { useEffect } from 'react';\nimport { useLocation } from 'react-router-dom';\n\nexport function useDeepLinking(onSearch: (filters: FilterState) => void) {\n const location = useLocation();\n\n useEffect(() => {\n // Parse deep link parameters on mount\n const params = new URLSearchParams(location.search);\n\n if (params.toString()) {\n const filters: FilterState = {};\n\n // Parse all parameters\n params.forEach((value, key) => {\n switch (key) {\n case 'q':\n filters.query = value;\n break;\n case 'category':\n if (!filters.categories) filters.categories = [];\n filters.categories.push(value);\n break;\n case 'min_price':\n filters.minPrice = parseFloat(value);\n break;\n case 'max_price':\n filters.maxPrice = parseFloat(value);\n break;\n case 'sort':\n filters.sortBy = value;\n break;\n case 'page':\n filters.page = parseInt(value, 10);\n break;\n }\n });\n\n // Execute search with deep link parameters\n onSearch(filters);\n }\n }, [location.search, onSearch]);\n}\n```\n\n## Validation and Sanitization\n\n### URL Parameter Validation\n```typescript\nclass UrlValidator {\n /**\n * Validate and sanitize search query\n */\n static validateQuery(query: string): string {\n // Remove special characters that could break URLs\n const sanitized = query\n .replace(/[\u003c>]/g, '') // Remove HTML tags\n .replace(/[^\\w\\s-.,]/g, '') // Keep only safe characters\n .trim()\n .substring(0, 200); // Limit length\n\n return sanitized;\n }\n\n /**\n * Validate numeric range\n */\n static validateRange(min?: number, max?: number): { min?: number; max?: number } {\n const result: { min?: number; max?: number } = {};\n\n if (min !== undefined && !isNaN(min) && min >= 0) {\n result.min = min;\n }\n\n if (max !== undefined && !isNaN(max) && max >= 0) {\n result.max = max;\n }\n\n // Ensure min \u003c= max\n if (result.min !== undefined && result.max !== undefined && result.min > result.max) {\n [result.min, result.max] = [result.max, result.min];\n }\n\n return result;\n }\n\n /**\n * Validate sort parameter\n */\n static validateSort(sort: string, allowedValues: string[]): string | undefined {\n return allowedValues.includes(sort) ? sort : undefined;\n }\n\n /**\n * Validate page number\n */\n static validatePage(page: any): number {\n const parsed = parseInt(page, 10);\n return isNaN(parsed) || parsed \u003c 1 ? 1 : Math.min(parsed, 100);\n }\n}\n\n// Usage in component\nexport function useValidatedUrlFilters() {\n const [searchParams, setSearchParams] = useSearchParams();\n\n const getValidatedFilters = (): FilterState => {\n const filters: FilterState = {};\n\n // Validate query\n const query = searchParams.get('q');\n if (query) {\n filters.query = UrlValidator.validateQuery(query);\n }\n\n // Validate price range\n const minPrice = searchParams.get('min_price');\n const maxPrice = searchParams.get('max_price');\n const range = UrlValidator.validateRange(\n minPrice ? parseFloat(minPrice) : undefined,\n maxPrice ? parseFloat(maxPrice) : undefined\n );\n\n if (range.min !== undefined) filters.minPrice = range.min;\n if (range.max !== undefined) filters.maxPrice = range.max;\n\n // Validate sort\n const sort = searchParams.get('sort');\n if (sort) {\n const validSort = UrlValidator.validateSort(sort, [\n 'relevance',\n 'price_asc',\n 'price_desc',\n 'newest',\n 'rating'\n ]);\n if (validSort) filters.sortBy = validSort;\n }\n\n // Validate page\n const page = searchParams.get('page');\n if (page) {\n filters.page = UrlValidator.validatePage(page);\n }\n\n return filters;\n };\n\n return getValidatedFilters();\n}\n```","content_type":"text/markdown; charset=utf-8","language":"markdown","size":16719,"content_sha256":"9d47fd7793425f330169e3b2fdebe2ec7433fd71d015f1de8bcdd0fddd5665aa"},{"filename":"references/search-input-patterns.md","content":"# Search Input Patterns\n\n\n## Table of Contents\n\n- [Basic Search Input](#basic-search-input)\n - [Minimal Implementation](#minimal-implementation)\n- [Advanced Search Input](#advanced-search-input)\n - [With Clear Button and Loading State](#with-clear-button-and-loading-state)\n- [Search with Keyboard Shortcuts](#search-with-keyboard-shortcuts)\n - [Global Search Hotkey (Cmd/Ctrl + K)](#global-search-hotkey-cmdctrl-k)\n- [Debouncing Strategies](#debouncing-strategies)\n - [Custom Debounce Hook](#custom-debounce-hook)\n - [Cancellable Search Requests](#cancellable-search-requests)\n- [Search Input States](#search-input-states)\n - [Visual States](#visual-states)\n- [Mobile Search Patterns](#mobile-search-patterns)\n - [Expandable Search](#expandable-search)\n - [Full-Screen Search Modal](#full-screen-search-modal)\n- [Accessibility Patterns](#accessibility-patterns)\n - [ARIA Attributes](#aria-attributes)\n - [Announcing Results](#announcing-results)\n- [Performance Metrics](#performance-metrics)\n - [Optimal Debounce Timing](#optimal-debounce-timing)\n - [Search Latency Targets](#search-latency-targets)\n- [Error Handling](#error-handling)\n - [User-Friendly Error Messages](#user-friendly-error-messages)\n\n## Basic Search Input\n\n### Minimal Implementation\n```tsx\nimport { useState, useCallback } from 'react';\nimport { debounce } from 'lodash';\n\nfunction SearchInput({ onSearch }) {\n const [value, setValue] = useState('');\n\n const debouncedSearch = useCallback(\n debounce((query) => onSearch(query), 300),\n [onSearch]\n );\n\n const handleChange = (e) => {\n const newValue = e.target.value;\n setValue(newValue);\n debouncedSearch(newValue);\n };\n\n return (\n \u003cdiv role=\"search\">\n \u003cinput\n type=\"search\"\n value={value}\n onChange={handleChange}\n placeholder=\"Search...\"\n aria-label=\"Search\"\n />\n \u003c/div>\n );\n}\n```\n\n## Advanced Search Input\n\n### With Clear Button and Loading State\n```tsx\nimport { useState, useCallback } from 'react';\nimport { Search, X, Loader2 } from 'lucide-react';\n\ninterface SearchInputProps {\n onSearch: (query: string) => void;\n isLoading?: boolean;\n placeholder?: string;\n}\n\nexport function SearchInput({\n onSearch,\n isLoading = false,\n placeholder = \"Search products...\"\n}: SearchInputProps) {\n const [value, setValue] = useState('');\n const [isFocused, setIsFocused] = useState(false);\n\n const handleClear = () => {\n setValue('');\n onSearch('');\n };\n\n return (\n \u003cdiv className=\"search-container\">\n \u003cdiv className=\"search-icon\">\n {isLoading ? (\n \u003cLoader2 className=\"animate-spin\" />\n ) : (\n \u003cSearch />\n )}\n \u003c/div>\n\n \u003cinput\n type=\"search\"\n value={value}\n onChange={(e) => setValue(e.target.value)}\n onFocus={() => setIsFocused(true)}\n onBlur={() => setIsFocused(false)}\n placeholder={placeholder}\n className=\"search-input\"\n aria-label=\"Search\"\n aria-busy={isLoading}\n />\n\n {value && (\n \u003cbutton\n onClick={handleClear}\n className=\"clear-button\"\n aria-label=\"Clear search\"\n >\n \u003cX />\n \u003c/button>\n )}\n \u003c/div>\n );\n}\n```\n\n## Search with Keyboard Shortcuts\n\n### Global Search Hotkey (Cmd/Ctrl + K)\n```tsx\nimport { useEffect, useRef } from 'react';\n\nexport function GlobalSearch() {\n const inputRef = useRef\u003cHTMLInputElement>(null);\n const [isOpen, setIsOpen] = useState(false);\n\n useEffect(() => {\n const handleKeyDown = (e: KeyboardEvent) => {\n // Cmd+K (Mac) or Ctrl+K (Windows/Linux)\n if ((e.metaKey || e.ctrlKey) && e.key === 'k') {\n e.preventDefault();\n setIsOpen(true);\n inputRef.current?.focus();\n }\n\n // Escape to close\n if (e.key === 'Escape') {\n setIsOpen(false);\n }\n };\n\n window.addEventListener('keydown', handleKeyDown);\n return () => window.removeEventListener('keydown', handleKeyDown);\n }, []);\n\n if (!isOpen) return null;\n\n return (\n \u003cdiv className=\"search-modal\">\n \u003cinput\n ref={inputRef}\n type=\"search\"\n placeholder=\"Type to search...\"\n autoFocus\n />\n \u003c/div>\n );\n}\n```\n\n## Debouncing Strategies\n\n### Custom Debounce Hook\n```tsx\nimport { useEffect, useState } from 'react';\n\nfunction useDebounce\u003cT>(value: T, delay: number): T {\n const [debouncedValue, setDebouncedValue] = useState\u003cT>(value);\n\n useEffect(() => {\n const handler = setTimeout(() => {\n setDebouncedValue(value);\n }, delay);\n\n return () => clearTimeout(handler);\n }, [value, delay]);\n\n return debouncedValue;\n}\n\n// Usage\nfunction SearchComponent() {\n const [searchTerm, setSearchTerm] = useState('');\n const debouncedSearchTerm = useDebounce(searchTerm, 300);\n\n useEffect(() => {\n if (debouncedSearchTerm) {\n // Perform search\n performSearch(debouncedSearchTerm);\n }\n }, [debouncedSearchTerm]);\n}\n```\n\n### Cancellable Search Requests\n```tsx\nimport { useRef, useCallback } from 'react';\n\nfunction useSearchAPI() {\n const abortControllerRef = useRef\u003cAbortController | null>(null);\n\n const search = useCallback(async (query: string) => {\n // Cancel previous request\n if (abortControllerRef.current) {\n abortControllerRef.current.abort();\n }\n\n // Create new abort controller\n abortControllerRef.current = new AbortController();\n\n try {\n const response = await fetch(`/api/search?q=${query}`, {\n signal: abortControllerRef.current.signal\n });\n\n if (!response.ok) throw new Error('Search failed');\n\n return await response.json();\n } catch (error) {\n if (error.name === 'AbortError') {\n // Request was cancelled, ignore\n return null;\n }\n throw error;\n }\n }, []);\n\n return { search };\n}\n```\n\n## Search Input States\n\n### Visual States\n```css\n/* Base state */\n.search-input {\n border: 1px solid var(--search-input-border);\n background: var(--search-input-bg);\n padding: var(--search-padding);\n border-radius: var(--search-border-radius);\n transition: all 0.2s ease;\n}\n\n/* Focus state */\n.search-input:focus {\n outline: none;\n border-color: var(--search-input-focus-border);\n box-shadow: 0 0 0 3px var(--search-input-focus-ring);\n}\n\n/* Loading state */\n.search-input[aria-busy=\"true\"] {\n background-image: url('data:image/svg+xml;...');\n background-position: right 12px center;\n background-repeat: no-repeat;\n}\n\n/* Empty state */\n.search-input:placeholder-shown {\n color: var(--search-placeholder-color);\n}\n\n/* Error state */\n.search-input[aria-invalid=\"true\"] {\n border-color: var(--color-error);\n}\n```\n\n## Mobile Search Patterns\n\n### Expandable Search\n```tsx\nfunction MobileSearch() {\n const [isExpanded, setIsExpanded] = useState(false);\n\n return (\n \u003cdiv className={`mobile-search ${isExpanded ? 'expanded' : ''}`}>\n \u003cbutton\n onClick={() => setIsExpanded(!isExpanded)}\n aria-label=\"Toggle search\"\n >\n \u003cSearch />\n \u003c/button>\n\n {isExpanded && (\n \u003cinput\n type=\"search\"\n placeholder=\"Search...\"\n autoFocus\n onBlur={() => setIsExpanded(false)}\n />\n )}\n \u003c/div>\n );\n}\n```\n\n### Full-Screen Search Modal\n```tsx\nfunction FullScreenSearch() {\n const [isOpen, setIsOpen] = useState(false);\n\n return (\n \u003c>\n \u003cbutton onClick={() => setIsOpen(true)}>\n Search\n \u003c/button>\n\n {isOpen && (\n \u003cdiv className=\"fullscreen-search\">\n \u003cdiv className=\"search-header\">\n \u003cinput\n type=\"search\"\n placeholder=\"What are you looking for?\"\n autoFocus\n />\n \u003cbutton onClick={() => setIsOpen(false)}>\n Cancel\n \u003c/button>\n \u003c/div>\n\n \u003cdiv className=\"search-suggestions\">\n {/* Recent searches, trending, etc */}\n \u003c/div>\n \u003c/div>\n )}\n \u003c/>\n );\n}\n```\n\n## Accessibility Patterns\n\n### ARIA Attributes\n```tsx\n\u003cdiv role=\"search\" aria-label=\"Site search\">\n \u003clabel htmlFor=\"search-input\" className=\"sr-only\">\n Search products\n \u003c/label>\n\n \u003cinput\n id=\"search-input\"\n type=\"search\"\n aria-describedby=\"search-hint\"\n aria-controls=\"search-results\"\n aria-expanded={hasResults}\n aria-autocomplete=\"list\"\n aria-busy={isSearching}\n />\n\n \u003cspan id=\"search-hint\" className=\"sr-only\">\n Type to search, use arrow keys to navigate suggestions\n \u003c/span>\n\n \u003cdiv\n id=\"search-results\"\n role=\"region\"\n aria-live=\"polite\"\n aria-relevant=\"additions removals\"\n >\n {/* Results */}\n \u003c/div>\n\u003c/div>\n```\n\n### Announcing Results\n```tsx\nfunction SearchResults({ results, query }) {\n return (\n \u003cdiv role=\"region\" aria-live=\"polite\">\n \u003cdiv className=\"sr-only\">\n {results.length > 0\n ? `${results.length} results found for ${query}`\n : `No results found for ${query}`\n }\n \u003c/div>\n\n {results.map(result => (\n \u003cSearchResult key={result.id} {...result} />\n ))}\n \u003c/div>\n );\n}\n```\n\n## Performance Metrics\n\n### Optimal Debounce Timing\n- **Fast typists**: 200-250ms\n- **Average typists**: 300-350ms\n- **Slow typists**: 400-500ms\n- **Mobile users**: 500-750ms\n\n### Search Latency Targets\n- **Autocomplete**: \u003c100ms\n- **Instant search**: \u003c200ms\n- **Full search**: \u003c500ms\n- **Complex search**: \u003c1000ms\n\n## Error Handling\n\n### User-Friendly Error Messages\n```tsx\nfunction SearchError({ error, query }) {\n const getErrorMessage = () => {\n switch(error.type) {\n case 'NETWORK':\n return 'Unable to search. Please check your connection.';\n case 'TIMEOUT':\n return 'Search is taking longer than expected...';\n case 'INVALID_QUERY':\n return 'Please enter a valid search term.';\n case 'NO_RESULTS':\n return `No results found for \"${query}\". Try different keywords.`;\n default:\n return 'Something went wrong. Please try again.';\n }\n };\n\n return (\n \u003cdiv role=\"alert\" className=\"search-error\">\n {getErrorMessage()}\n \u003c/div>\n );\n}\n```","content_type":"text/markdown; charset=utf-8","language":"markdown","size":10054,"content_sha256":"2c71a6448af41b9ec5c0c632392fc46c3365f29c08a3faae545fddca3e504eb7"},{"filename":"scripts/generate_filter_query.py","content":"#!/usr/bin/env python3\n\"\"\"\nGenerate optimized SQL and Elasticsearch queries from filter parameters.\n\nThis script generates database queries dynamically based on search filters,\nhandling both SQL (PostgreSQL/MySQL) and Elasticsearch query generation.\n\"\"\"\n\nimport json\nimport argparse\nfrom typing import Dict, List, Any, Optional\nfrom datetime import datetime, timedelta\n\n\nclass SQLQueryBuilder:\n \"\"\"Build SQL queries dynamically from filter parameters.\"\"\"\n\n def __init__(self, dialect: str = 'postgresql'):\n self.dialect = dialect\n self.query_parts = {\n 'select': [],\n 'from': '',\n 'join': [],\n 'where': [],\n 'group_by': [],\n 'having': [],\n 'order_by': [],\n 'limit': None,\n 'offset': None\n }\n\n def build_search_query(self, filters: Dict[str, Any]) -> str:\n \"\"\"Build a complete search query from filters.\"\"\"\n\n # Base query\n self.query_parts['select'] = ['p.*']\n self.query_parts['from'] = 'products p'\n\n # Text search\n if filters.get('query'):\n self._add_text_search(filters['query'])\n\n # Category filter\n if filters.get('categories'):\n self._add_category_filter(filters['categories'])\n\n # Price range\n if filters.get('min_price') or filters.get('max_price'):\n self._add_price_filter(\n filters.get('min_price'),\n filters.get('max_price')\n )\n\n # Brand filter\n if filters.get('brands'):\n self._add_brand_filter(filters['brands'])\n\n # Stock filter\n if filters.get('in_stock'):\n self.query_parts['where'].append('p.in_stock = TRUE')\n\n # Date range\n if filters.get('date_from') or filters.get('date_to'):\n self._add_date_filter(\n filters.get('date_from'),\n filters.get('date_to')\n )\n\n # Sorting\n self._add_sorting(filters.get('sort_by', 'relevance'))\n\n # Pagination\n self._add_pagination(\n filters.get('page', 1),\n filters.get('per_page', 20)\n )\n\n return self._build_query_string()\n\n def _add_text_search(self, query: str):\n \"\"\"Add full-text search condition.\"\"\"\n if self.dialect == 'postgresql':\n # PostgreSQL full-text search\n search_vector = \"\"\"\n to_tsvector('english', COALESCE(p.title, '') || ' ' ||\n COALESCE(p.description, '') || ' ' ||\n COALESCE(p.tags, ''))\n \"\"\"\n self.query_parts['where'].append(\n f\"{search_vector} @@ plainto_tsquery('english', '{query}')\"\n )\n\n # Add relevance score\n self.query_parts['select'].append(\n f\"ts_rank({search_vector}, plainto_tsquery('english', '{query}')) AS relevance\"\n )\n else:\n # MySQL FULLTEXT\n self.query_parts['where'].append(\n f\"MATCH(p.title, p.description) AGAINST('{query}' IN NATURAL LANGUAGE MODE)\"\n )\n\n def _add_category_filter(self, categories: List[str]):\n \"\"\"Add category filter.\"\"\"\n placeholders = ', '.join([f\"'{cat}'\" for cat in categories])\n self.query_parts['where'].append(f\"p.category IN ({placeholders})\")\n\n def _add_price_filter(self, min_price: Optional[float], max_price: Optional[float]):\n \"\"\"Add price range filter.\"\"\"\n if min_price is not None:\n self.query_parts['where'].append(f\"p.price >= {min_price}\")\n if max_price is not None:\n self.query_parts['where'].append(f\"p.price \u003c= {max_price}\")\n\n def _add_brand_filter(self, brands: List[str]):\n \"\"\"Add brand filter.\"\"\"\n placeholders = ', '.join([f\"'{brand}'\" for brand in brands])\n self.query_parts['where'].append(f\"p.brand IN ({placeholders})\")\n\n def _add_date_filter(self, date_from: Optional[str], date_to: Optional[str]):\n \"\"\"Add date range filter.\"\"\"\n if date_from:\n self.query_parts['where'].append(f\"p.created_at >= '{date_from}'\")\n if date_to:\n self.query_parts['where'].append(f\"p.created_at \u003c= '{date_to}'\")\n\n def _add_sorting(self, sort_by: str):\n \"\"\"Add sorting clause.\"\"\"\n sort_options = {\n 'relevance': 'relevance DESC' if 'relevance' in str(self.query_parts['select']) else 'p.created_at DESC',\n 'price_asc': 'p.price ASC',\n 'price_desc': 'p.price DESC',\n 'newest': 'p.created_at DESC',\n 'oldest': 'p.created_at ASC',\n 'rating': 'p.rating DESC',\n 'popularity': 'p.view_count DESC'\n }\n\n self.query_parts['order_by'] = [sort_options.get(sort_by, 'p.created_at DESC')]\n\n def _add_pagination(self, page: int, per_page: int):\n \"\"\"Add pagination.\"\"\"\n self.query_parts['limit'] = per_page\n self.query_parts['offset'] = (page - 1) * per_page\n\n def _build_query_string(self) -> str:\n \"\"\"Build final SQL query string.\"\"\"\n query = f\"SELECT {', '.join(self.query_parts['select'])}\\n\"\n query += f\"FROM {self.query_parts['from']}\\n\"\n\n if self.query_parts['join']:\n query += '\\n'.join(self.query_parts['join']) + '\\n'\n\n if self.query_parts['where']:\n query += f\"WHERE {' AND '.join(self.query_parts['where'])}\\n\"\n\n if self.query_parts['group_by']:\n query += f\"GROUP BY {', '.join(self.query_parts['group_by'])}\\n\"\n\n if self.query_parts['having']:\n query += f\"HAVING {' AND '.join(self.query_parts['having'])}\\n\"\n\n if self.query_parts['order_by']:\n query += f\"ORDER BY {', '.join(self.query_parts['order_by'])}\\n\"\n\n if self.query_parts['limit']:\n query += f\"LIMIT {self.query_parts['limit']}\\n\"\n\n if self.query_parts['offset']:\n query += f\"OFFSET {self.query_parts['offset']}\\n\"\n\n return query\n\n\nclass ElasticsearchQueryBuilder:\n \"\"\"Build Elasticsearch queries from filter parameters.\"\"\"\n\n def build_search_query(self, filters: Dict[str, Any]) -> Dict:\n \"\"\"Build Elasticsearch query DSL from filters.\"\"\"\n\n query = {\n 'query': {\n 'bool': {\n 'must': [],\n 'filter': [],\n 'should': [],\n 'must_not': []\n }\n }\n }\n\n # Text search\n if filters.get('query'):\n query['query']['bool']['must'].append({\n 'multi_match': {\n 'query': filters['query'],\n 'fields': ['title^3', 'description^2', 'tags'],\n 'type': 'best_fields',\n 'fuzziness': 'AUTO'\n }\n })\n\n # Category filter\n if filters.get('categories'):\n query['query']['bool']['filter'].append({\n 'terms': {'category.keyword': filters['categories']}\n })\n\n # Price range\n if filters.get('min_price') or filters.get('max_price'):\n price_range = {}\n if filters.get('min_price'):\n price_range['gte'] = filters['min_price']\n if filters.get('max_price'):\n price_range['lte'] = filters['max_price']\n\n query['query']['bool']['filter'].append({\n 'range': {'price': price_range}\n })\n\n # Brand filter\n if filters.get('brands'):\n query['query']['bool']['filter'].append({\n 'terms': {'brand.keyword': filters['brands']}\n })\n\n # Stock filter\n if filters.get('in_stock'):\n query['query']['bool']['filter'].append({\n 'term': {'in_stock': True}\n })\n\n # Date range\n if filters.get('date_from') or filters.get('date_to'):\n date_range = {}\n if filters.get('date_from'):\n date_range['gte'] = filters['date_from']\n if filters.get('date_to'):\n date_range['lte'] = filters['date_to']\n\n query['query']['bool']['filter'].append({\n 'range': {'created_at': date_range}\n })\n\n # Sorting\n query['sort'] = self._get_sort_clause(filters.get('sort_by', 'relevance'))\n\n # Pagination\n page = filters.get('page', 1)\n per_page = filters.get('per_page', 20)\n query['from'] = (page - 1) * per_page\n query['size'] = per_page\n\n # Aggregations for facets\n if filters.get('include_facets', True):\n query['aggs'] = self._build_aggregations()\n\n # Clean up empty sections\n if not query['query']['bool']['must']:\n del query['query']['bool']['must']\n if not query['query']['bool']['filter']:\n del query['query']['bool']['filter']\n if not query['query']['bool']['should']:\n del query['query']['bool']['should']\n if not query['query']['bool']['must_not']:\n del query['query']['bool']['must_not']\n\n # If no conditions, use match_all\n if not query['query']['bool']:\n query['query'] = {'match_all': {}}\n\n return query\n\n def _get_sort_clause(self, sort_by: str) -> List[Dict]:\n \"\"\"Get Elasticsearch sort clause.\"\"\"\n sort_options = {\n 'relevance': [{'_score': 'desc'}],\n 'price_asc': [{'price': 'asc'}],\n 'price_desc': [{'price': 'desc'}],\n 'newest': [{'created_at': 'desc'}],\n 'oldest': [{'created_at': 'asc'}],\n 'rating': [{'rating': 'desc'}],\n 'popularity': [{'view_count': 'desc'}]\n }\n\n return sort_options.get(sort_by, [{'_score': 'desc'}])\n\n def _build_aggregations(self) -> Dict:\n \"\"\"Build aggregations for faceted search.\"\"\"\n return {\n 'categories': {\n 'terms': {\n 'field': 'category.keyword',\n 'size': 20\n }\n },\n 'brands': {\n 'terms': {\n 'field': 'brand.keyword',\n 'size': 20\n }\n },\n 'price_ranges': {\n 'range': {\n 'field': 'price',\n 'ranges': [\n {'key': 'Under $50', 'to': 50},\n {'key': '$50-$100', 'from': 50, 'to': 100},\n {'key': '$100-$200', 'from': 100, 'to': 200},\n {'key': 'Over $200', 'from': 200}\n ]\n }\n },\n 'avg_price': {\n 'avg': {'field': 'price'}\n },\n 'in_stock_count': {\n 'filter': {'term': {'in_stock': True}}\n }\n }\n\n\ndef main():\n \"\"\"Main function to generate queries from command line.\"\"\"\n parser = argparse.ArgumentParser(\n description='Generate search queries from filter parameters'\n )\n\n parser.add_argument(\n '--type',\n choices=['sql', 'elasticsearch'],\n default='sql',\n help='Query type to generate'\n )\n\n parser.add_argument(\n '--dialect',\n choices=['postgresql', 'mysql'],\n default='postgresql',\n help='SQL dialect (for SQL queries)'\n )\n\n parser.add_argument(\n '--filters',\n type=str,\n required=True,\n help='JSON string of filter parameters'\n )\n\n parser.add_argument(\n '--pretty',\n action='store_true',\n help='Pretty print output'\n )\n\n args = parser.parse_args()\n\n try:\n filters = json.loads(args.filters)\n except json.JSONDecodeError as e:\n print(f\"Error parsing filters JSON: {e}\")\n return 1\n\n if args.type == 'sql':\n builder = SQLQueryBuilder(dialect=args.dialect)\n query = builder.build_search_query(filters)\n print(query)\n else:\n builder = ElasticsearchQueryBuilder()\n query = builder.build_search_query(filters)\n\n if args.pretty:\n print(json.dumps(query, indent=2))\n else:\n print(json.dumps(query))\n\n return 0\n\n\nif __name__ == '__main__':\n exit(main())","content_type":"text/x-python; charset=utf-8","language":"python","size":12353,"content_sha256":"e6b44e1154b9653e543164442b1f0decb5e77dfc2dd9f57eeade6c5c86c5a189"},{"filename":"scripts/validate_search_params.py","content":"#!/usr/bin/env python3\n\"\"\"\nValidate and sanitize search parameters to prevent injection attacks and ensure data integrity.\n\nThis script validates search inputs, filters, and pagination parameters\nto ensure they meet security and business logic requirements.\n\"\"\"\n\nimport re\nimport json\nimport argparse\nfrom typing import Dict, List, Any, Optional, Tuple\nfrom datetime import datetime, date\n\n\nclass SearchParamValidator:\n \"\"\"Validate and sanitize search parameters.\"\"\"\n\n # Define validation rules\n RULES = {\n 'query': {\n 'type': str,\n 'min_length': 0,\n 'max_length': 200,\n 'pattern': r'^[a-zA-Z0-9\\s\\-\\.\\,\\!\\?\\'\\\"\\&]+

Search & Filter Implementation Implement search and filter interfaces with comprehensive frontend components and backend query optimization. Purpose This skill provides production-ready patterns for implementing search and filtering functionality across the full stack. It covers React/TypeScript components for the frontend (search inputs, filter UIs, autocomplete) and Python patterns for the backend (SQLAlchemy queries, Elasticsearch integration, API design). The skill emphasizes performance optimization, accessibility, and user experience. When to Use - Building product search with category…

, # Alphanumeric + common punctuation\n 'sanitize': True\n },\n 'categories': {\n 'type': list,\n 'max_items': 20,\n 'item_type': str,\n 'allowed_values': None # Will be set in __init__ if needed\n },\n 'brands': {\n 'type': list,\n 'max_items': 20,\n 'item_type': str\n },\n 'min_price': {\n 'type': (int, float),\n 'min_value': 0,\n 'max_value': 1000000\n },\n 'max_price': {\n 'type': (int, float),\n 'min_value': 0,\n 'max_value': 1000000\n },\n 'sort_by': {\n 'type': str,\n 'allowed_values': [\n 'relevance', 'price_asc', 'price_desc',\n 'newest', 'oldest', 'rating', 'popularity'\n ]\n },\n 'page': {\n 'type': int,\n 'min_value': 1,\n 'max_value': 100\n },\n 'per_page': {\n 'type': int,\n 'min_value': 1,\n 'max_value': 100,\n 'default': 20\n },\n 'in_stock': {\n 'type': bool\n },\n 'date_from': {\n 'type': str,\n 'date_format': '%Y-%m-%d'\n },\n 'date_to': {\n 'type': str,\n 'date_format': '%Y-%m-%d'\n }\n }\n\n # SQL injection patterns to block\n SQL_INJECTION_PATTERNS = [\n r'(\\b(SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|EXEC|EXECUTE)\\b)',\n r'(--|\\/\\*|\\*\\/|xp_|sp_|@@)',\n r'(\\bunion\\b.*\\bselect\\b)',\n r'(;.*\\b(SELECT|INSERT|UPDATE|DELETE)\\b)',\n r'(\\bOR\\b.*=.*)',\n r\"('.*\\bOR\\b.*'=')\",\n ]\n\n def __init__(self, allowed_categories: Optional[List[str]] = None):\n \"\"\"Initialize validator with optional allowed categories.\"\"\"\n if allowed_categories:\n self.RULES['categories']['allowed_values'] = allowed_categories\n\n def validate(self, params: Dict[str, Any]) -> Tuple[bool, Dict[str, Any], List[str]]:\n \"\"\"\n Validate search parameters.\n\n Returns:\n Tuple of (is_valid, cleaned_params, error_messages)\n \"\"\"\n cleaned = {}\n errors = []\n\n for param_name, param_value in params.items():\n if param_value is None:\n continue\n\n if param_name not in self.RULES:\n # Unknown parameter - skip but log warning\n errors.append(f\"Unknown parameter: {param_name}\")\n continue\n\n rule = self.RULES[param_name]\n result = self._validate_param(param_name, param_value, rule)\n\n if result['valid']:\n cleaned[param_name] = result['value']\n else:\n errors.extend(result['errors'])\n\n # Additional cross-field validation\n cross_errors = self._cross_validate(cleaned)\n errors.extend(cross_errors)\n\n # Apply defaults for missing required params\n cleaned = self._apply_defaults(cleaned)\n\n return len(errors) == 0, cleaned, errors\n\n def _validate_param(self, name: str, value: Any, rule: Dict) -> Dict:\n \"\"\"Validate a single parameter.\"\"\"\n result = {'valid': True, 'value': value, 'errors': []}\n\n # Type validation\n expected_type = rule.get('type')\n if expected_type and not isinstance(value, expected_type):\n result['valid'] = False\n result['errors'].append(\n f\"{name}: Expected {expected_type.__name__}, got {type(value).__name__}\"\n )\n return result\n\n # String validation\n if isinstance(value, str):\n validated = self._validate_string(name, value, rule)\n result.update(validated)\n\n # List validation\n elif isinstance(value, list):\n validated = self._validate_list(name, value, rule)\n result.update(validated)\n\n # Number validation\n elif isinstance(value, (int, float)):\n validated = self._validate_number(name, value, rule)\n result.update(validated)\n\n # Boolean validation\n elif isinstance(value, bool):\n result['value'] = value\n\n # Date validation\n if rule.get('date_format'):\n validated = self._validate_date(name, str(value), rule['date_format'])\n result.update(validated)\n\n return result\n\n def _validate_string(self, name: str, value: str, rule: Dict) -> Dict:\n \"\"\"Validate string parameter.\"\"\"\n result = {'valid': True, 'value': value, 'errors': []}\n\n # Check for SQL injection attempts\n for pattern in self.SQL_INJECTION_PATTERNS:\n if re.search(pattern, value, re.IGNORECASE):\n result['valid'] = False\n result['errors'].append(\n f\"{name}: Potential SQL injection detected\"\n )\n return result\n\n # Length validation\n min_len = rule.get('min_length', 0)\n max_len = rule.get('max_length', float('inf'))\n\n if len(value) \u003c min_len:\n result['valid'] = False\n result['errors'].append(\n f\"{name}: Must be at least {min_len} characters\"\n )\n\n if len(value) > max_len:\n result['valid'] = False\n result['errors'].append(\n f\"{name}: Must be at most {max_len} characters\"\n )\n\n # Pattern validation\n pattern = rule.get('pattern')\n if pattern and not re.match(pattern, value):\n result['valid'] = False\n result['errors'].append(\n f\"{name}: Contains invalid characters\"\n )\n\n # Allowed values validation\n allowed = rule.get('allowed_values')\n if allowed and value not in allowed:\n result['valid'] = False\n result['errors'].append(\n f\"{name}: Must be one of {allowed}\"\n )\n\n # Sanitization\n if rule.get('sanitize') and result['valid']:\n result['value'] = self._sanitize_string(value)\n\n return result\n\n def _validate_list(self, name: str, value: List, rule: Dict) -> Dict:\n \"\"\"Validate list parameter.\"\"\"\n result = {'valid': True, 'value': value, 'errors': []}\n\n # Max items check\n max_items = rule.get('max_items', float('inf'))\n if len(value) > max_items:\n result['valid'] = False\n result['errors'].append(\n f\"{name}: Cannot have more than {max_items} items\"\n )\n\n # Item type validation\n item_type = rule.get('item_type')\n if item_type:\n for i, item in enumerate(value):\n if not isinstance(item, item_type):\n result['valid'] = False\n result['errors'].append(\n f\"{name}[{i}]: Expected {item_type.__name__}\"\n )\n\n # Allowed values for items\n allowed = rule.get('allowed_values')\n if allowed:\n invalid_items = [item for item in value if item not in allowed]\n if invalid_items:\n result['valid'] = False\n result['errors'].append(\n f\"{name}: Invalid items: {invalid_items}\"\n )\n\n # Sanitize string items\n if item_type == str and result['valid']:\n result['value'] = [self._sanitize_string(item) for item in value]\n\n return result\n\n def _validate_number(self, name: str, value: float, rule: Dict) -> Dict:\n \"\"\"Validate numeric parameter.\"\"\"\n result = {'valid': True, 'value': value, 'errors': []}\n\n min_val = rule.get('min_value', float('-inf'))\n max_val = rule.get('max_value', float('inf'))\n\n if value \u003c min_val:\n result['valid'] = False\n result['errors'].append(\n f\"{name}: Must be at least {min_val}\"\n )\n\n if value > max_val:\n result['valid'] = False\n result['errors'].append(\n f\"{name}: Must be at most {max_val}\"\n )\n\n return result\n\n def _validate_date(self, name: str, value: str, date_format: str) -> Dict:\n \"\"\"Validate date parameter.\"\"\"\n result = {'valid': True, 'value': value, 'errors': []}\n\n try:\n parsed_date = datetime.strptime(value, date_format)\n result['value'] = parsed_date.strftime(date_format)\n\n # Check if date is not in future (for most cases)\n if parsed_date.date() > date.today():\n result['errors'].append(\n f\"{name}: Date cannot be in the future\"\n )\n except ValueError:\n result['valid'] = False\n result['errors'].append(\n f\"{name}: Invalid date format (expected {date_format})\"\n )\n\n return result\n\n def _sanitize_string(self, value: str) -> str:\n \"\"\"Sanitize string to prevent XSS and injection.\"\"\"\n # Remove HTML tags\n value = re.sub(r'\u003c[^>]+>', '', value)\n\n # Escape special characters\n value = value.replace('&', '&')\n value = value.replace('\u003c', '<')\n value = value.replace('>', '>')\n value = value.replace('\"', '"')\n value = value.replace(\"'\", ''')\n\n # Normalize whitespace\n value = ' '.join(value.split())\n\n return value.strip()\n\n def _cross_validate(self, params: Dict) -> List[str]:\n \"\"\"Perform cross-field validation.\"\"\"\n errors = []\n\n # Price range validation\n min_price = params.get('min_price')\n max_price = params.get('max_price')\n\n if min_price is not None and max_price is not None:\n if min_price > max_price:\n errors.append(\"min_price cannot be greater than max_price\")\n\n # Date range validation\n date_from = params.get('date_from')\n date_to = params.get('date_to')\n\n if date_from and date_to:\n try:\n from_date = datetime.strptime(date_from, '%Y-%m-%d')\n to_date = datetime.strptime(date_to, '%Y-%m-%d')\n\n if from_date > to_date:\n errors.append(\"date_from cannot be after date_to\")\n except ValueError:\n pass # Already handled in individual validation\n\n return errors\n\n def _apply_defaults(self, params: Dict) -> Dict:\n \"\"\"Apply default values for missing parameters.\"\"\"\n defaults = {\n 'page': 1,\n 'per_page': 20,\n 'sort_by': 'relevance'\n }\n\n for key, default_value in defaults.items():\n if key not in params:\n rule = self.RULES.get(key, {})\n if 'default' in rule:\n params[key] = rule['default']\n elif key in defaults:\n params[key] = default_value\n\n return params\n\n\ndef main():\n \"\"\"Main function for command-line usage.\"\"\"\n parser = argparse.ArgumentParser(\n description='Validate search parameters'\n )\n\n parser.add_argument(\n '--params',\n type=str,\n required=True,\n help='JSON string of search parameters'\n )\n\n parser.add_argument(\n '--categories',\n type=str,\n help='Comma-separated list of allowed categories'\n )\n\n parser.add_argument(\n '--strict',\n action='store_true',\n help='Fail on any validation error'\n )\n\n args = parser.parse_args()\n\n try:\n params = json.loads(args.params)\n except json.JSONDecodeError as e:\n print(f\"Error parsing parameters JSON: {e}\")\n return 1\n\n # Parse allowed categories if provided\n allowed_categories = None\n if args.categories:\n allowed_categories = [c.strip() for c in args.categories.split(',')]\n\n # Validate parameters\n validator = SearchParamValidator(allowed_categories)\n is_valid, cleaned_params, errors = validator.validate(params)\n\n # Output results\n result = {\n 'valid': is_valid,\n 'cleaned_params': cleaned_params,\n 'errors': errors\n }\n\n print(json.dumps(result, indent=2, default=str))\n\n # Exit code based on validation result\n if args.strict and not is_valid:\n return 1\n\n return 0\n\n\nif __name__ == '__main__':\n exit(main())","content_type":"text/x-python; charset=utf-8","language":"python","size":13094,"content_sha256":"a2f29541efb032870881b0163446094d48dbfd35a8109d2bdf960253832f523e"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"Search & Filter Implementation","type":"text"}]},{"type":"paragraph","content":[{"text":"Implement search and filter interfaces with comprehensive frontend components and backend query optimization.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Purpose","type":"text"}]},{"type":"paragraph","content":[{"text":"This skill provides production-ready patterns for implementing search and filtering functionality across the full stack. It covers React/TypeScript components for the frontend (search inputs, filter UIs, autocomplete) and Python patterns for the backend (SQLAlchemy queries, Elasticsearch integration, API design). The skill emphasizes performance optimization, accessibility, and user experience.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"When to Use","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Building product search with category and price filters","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Implementing autocomplete/typeahead search","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Creating faceted search interfaces with dynamic counts","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Adding search to data tables or lists","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Building advanced boolean search for power users","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Implementing backend search with SQLAlchemy or Django ORM","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Integrating Elasticsearch for full-text search","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Optimizing search performance with debouncing and caching","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Creating accessible search experiences","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Core Components","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Frontend Search Patterns","type":"text"}]},{"type":"paragraph","content":[{"text":"Search Input with Debouncing","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Implement 300ms debounce for performance","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Show loading states during search","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Clear button (X) for resetting","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Keyboard shortcuts (Cmd/Ctrl+K)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"See ","type":"text"},{"text":"references/search-input-patterns.md","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"paragraph","content":[{"text":"Autocomplete/Typeahead","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Suggestion dropdown with keyboard navigation","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Highlight matched text in suggestions","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Recent searches and popular items","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Prevent request flooding with debouncing","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"See ","type":"text"},{"text":"references/autocomplete-patterns.md","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"paragraph","content":[{"text":"Filter UI Components","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Checkbox filters for multi-select","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Range sliders for numerical values","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Dropdown filters for single selection","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Filter chips showing active selections","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"See ","type":"text"},{"text":"references/filter-ui-patterns.md","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Backend Query Patterns","type":"text"}]},{"type":"paragraph","content":[{"text":"Database Query Building","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Dynamic query construction with SQLAlchemy","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Django ORM filter chaining","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Index optimization for search columns","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Full-text search in PostgreSQL","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"See ","type":"text"},{"text":"references/database-querying.md","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"paragraph","content":[{"text":"Elasticsearch Integration","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Document indexing strategies","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Query DSL for complex searches","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Faceted aggregations","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Relevance scoring and boosting","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"See ","type":"text"},{"text":"references/elasticsearch-integration.md","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"paragraph","content":[{"text":"API Design","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"RESTful search endpoints","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Query parameter validation","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Pagination with cursor/offset","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Response caching strategies","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"See ","type":"text"},{"text":"references/api-design.md","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Implementation Workflows","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Client-Side Search (\u003c1000 items)","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Load data into memory","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Implement filter functions in JavaScript","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Apply debounced search on text input","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Update results instantly","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Maintain filter state in React","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Server-Side Search (>1000 items)","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Design search API endpoint","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Validate and sanitize query parameters","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Build database query dynamically","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Apply pagination","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Return results with metadata","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Cache frequent queries","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Hybrid Approach","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use client-side filtering for immediate feedback","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Fetch server results in background","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Merge and deduplicate results","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Update UI progressively","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Cache recent searches locally","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Performance Optimization","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Frontend Optimization","type":"text"}]},{"type":"paragraph","content":[{"text":"Debouncing Implementation","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use ","type":"text"},{"text":"debounce","type":"text","marks":[{"type":"code_inline"}]},{"text":" from lodash or custom implementation","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Cancel pending requests on new input","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Show skeleton loaders during fetch","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Script: ","type":"text"},{"text":"scripts/debounce_calculator.js","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"paragraph","content":[{"text":"Query Parameter Management","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Sync filters with URL for shareable searches","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use React Router or Next.js for URL state","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Compress complex queries","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"See ","type":"text"},{"text":"references/query-parameter-management.md","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Backend Optimization","type":"text"}]},{"type":"paragraph","content":[{"text":"Query Optimization","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Create appropriate database indexes","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use query analyzers to identify bottlenecks","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Implement query result caching","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Script: ","type":"text"},{"text":"scripts/generate_filter_query.py","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"paragraph","content":[{"text":"Validation & Security","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Sanitize all search inputs","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Prevent SQL injection","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Rate limit search endpoints","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Script: ","type":"text"},{"text":"scripts/validate_search_params.py","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Accessibility Requirements","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"ARIA Patterns","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use ","type":"text"},{"text":"role=\"search\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" for search regions","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Implement ","type":"text"},{"text":"aria-live","type":"text","marks":[{"type":"code_inline"}]},{"text":" for result updates","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Provide clear labels for filters","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Support keyboard-only navigation","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Keyboard Support","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Tab through all interactive elements","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Arrow keys for autocomplete navigation","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Escape to close dropdowns","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Enter to select/submit","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Technology Stack","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Frontend Libraries","type":"text"}]},{"type":"paragraph","content":[{"text":"Primary: Downshift (Autocomplete)","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Accessible autocomplete primitives","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Headless/unstyled for flexibility","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"WAI-ARIA compliant","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Install: ","type":"text"},{"text":"npm install downshift","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"paragraph","content":[{"text":"Alternative: React Select","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Full-featured select/filter component","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Built-in async search","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Multi-select support","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Backend Technologies","type":"text"}]},{"type":"paragraph","content":[{"text":"Python/SQLAlchemy","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Dynamic query building","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Relationship loading optimization","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Query result pagination","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Python/Django","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Django Filter backend","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Django REST Framework filters","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Full-text search with PostgreSQL","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Elasticsearch (Python)","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"elasticsearch-py client","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"elasticsearch-dsl for query building","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Bundled Resources","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"References","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/search-input-patterns.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Input implementations","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/autocomplete-patterns.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Typeahead patterns","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/filter-ui-patterns.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Filter components","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/database-querying.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" - SQL query patterns","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/elasticsearch-integration.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Elasticsearch setup","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/api-design.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" - API endpoint patterns","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/performance-optimization.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Performance tips","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/library-comparison.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Library evaluation","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Scripts","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"scripts/generate_filter_query.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Build SQL/ES queries","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"scripts/validate_search_params.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Validate inputs","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"scripts/debounce_calculator.js","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Calculate debounce timing","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Examples","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"examples/product-search.tsx","type":"text","marks":[{"type":"code_inline"}]},{"text":" - E-commerce search","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"examples/autocomplete-search.tsx","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Autocomplete implementation","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"examples/sqlalchemy_search.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" - SQLAlchemy patterns","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"examples/fastapi_search.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" - FastAPI search endpoint","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"examples/django_filter_backend.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Django filters","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Assets","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"assets/filter-config-schema.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Filter configuration","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"assets/search-api-spec.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" - OpenAPI specification","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}}]},"metadata":{"date":"2026-06-05","name":"implementing-search-filter","author":"@skillopedia","source":{"stars":368,"repo_name":"ai-design-components","origin_url":"https://github.com/ancoleman/ai-design-components/blob/HEAD/skills/implementing-search-filter/SKILL.md","repo_owner":"ancoleman","body_sha256":"f7d359828f44b89f6e0ec7b6f59c394197eeb16ce243602f91d1625864de332c","cluster_key":"77188208434feff37e70bf0135d38ba538510acf98973edfb68445e8af7664ba","clean_bundle":{"format":"clean-skill-bundle-v1","source":"ancoleman/ai-design-components/skills/implementing-search-filter/SKILL.md","attachments":[{"id":"e5ce6c3e-31da-5592-adb6-19aae7812eff","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/e5ce6c3e-31da-5592-adb6-19aae7812eff/attachment.json","path":"assets/filter-config-schema.json","size":11908,"sha256":"1ef3fb5b912c3ee685002c34dc6d4ca6a74a1d2e2818df082885f3d504a1d4c3","contentType":"application/json; charset=utf-8"},{"id":"9bcfa021-5df7-547f-b3d1-3bd81f076f66","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/9bcfa021-5df7-547f-b3d1-3bd81f076f66/attachment.json","path":"assets/search-api-spec.json","size":16645,"sha256":"ecbab1b580a417fe7216d3d0f2b8f1d02fa6953abfbfaf683e690cd77e63fcdd","contentType":"application/json; charset=utf-8"},{"id":"63d4a65b-34f8-5ca4-b0d9-19cbf0f9b341","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/63d4a65b-34f8-5ca4-b0d9-19cbf0f9b341/attachment.tsx","path":"examples/autocomplete-search.tsx","size":12766,"sha256":"3d067eb6c404fe970eb08ab1d18d26063f68609dbb22df91e08efcc5741be3c5","contentType":"text/typescript; charset=utf-8"},{"id":"b6178574-fee0-50b4-a434-3547cf43540d","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/b6178574-fee0-50b4-a434-3547cf43540d/attachment.py","path":"examples/django_filter_backend.py","size":16314,"sha256":"df8bba7212c43e1f98dd7d06f468e32d6d4bac9950181f39e72db9085d316e2f","contentType":"text/x-python; charset=utf-8"},{"id":"a7919094-cff3-55ee-8992-67e3fa526580","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/a7919094-cff3-55ee-8992-67e3fa526580/attachment.py","path":"examples/fastapi_search.py","size":13579,"sha256":"5c7399f73089afb4177291e722933c69e5b40b37236a559d26afc678e34e87c8","contentType":"text/x-python; charset=utf-8"},{"id":"063ae7b7-1822-5ae2-85e7-c86d20689c8d","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/063ae7b7-1822-5ae2-85e7-c86d20689c8d/attachment.tsx","path":"examples/product-search.tsx","size":12443,"sha256":"4129324bca0af63d587bca1b5c87c43baa70ebb4bee289577da4a3c3f022efe9","contentType":"text/typescript; charset=utf-8"},{"id":"5129ec96-0efe-5a80-95ff-4b6d5413ce50","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/5129ec96-0efe-5a80-95ff-4b6d5413ce50/attachment.py","path":"examples/sqlalchemy_search.py","size":13616,"sha256":"b7439791547cd53d04a4ba6d0d20449d4db8c0257c4eda3e2063ce5cf9d7c972","contentType":"text/x-python; charset=utf-8"},{"id":"00e2c997-a773-5471-96a3-894c90121b07","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/00e2c997-a773-5471-96a3-894c90121b07/attachment.yaml","path":"outputs.yaml","size":11094,"sha256":"8adfeaaf552faed8030cae51eb955e776c84eb55aad3781205e0be07d3a04ab3","contentType":"application/yaml; charset=utf-8"},{"id":"1d9bc561-ab53-56e3-b784-a9bd3ab88404","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/1d9bc561-ab53-56e3-b784-a9bd3ab88404/attachment.md","path":"references/api-design.md","size":19613,"sha256":"f45f9a99576ee14eb985064da6f33959922ca9faf9c495adba07019e88701a8f","contentType":"text/markdown; charset=utf-8"},{"id":"a43564e4-a585-5df7-8371-1ee319b140b3","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/a43564e4-a585-5df7-8371-1ee319b140b3/attachment.md","path":"references/autocomplete-patterns.md","size":20731,"sha256":"32e227f36eba1abcda6ae63e556bf2cb2fc6451ae83a9ccc36b400d8faeb0c1e","contentType":"text/markdown; charset=utf-8"},{"id":"5ed7b606-4bda-57a1-9003-9d2443545cea","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/5ed7b606-4bda-57a1-9003-9d2443545cea/attachment.md","path":"references/database-querying.md","size":17173,"sha256":"18957ce7ab475330bdec02cc0af76bf5c89978a4e82f532dad4e0a78c70c6ead","contentType":"text/markdown; charset=utf-8"},{"id":"65a45a8c-da95-5c98-8509-950df7b1fe8c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/65a45a8c-da95-5c98-8509-950df7b1fe8c/attachment.md","path":"references/elasticsearch-integration.md","size":22143,"sha256":"473f311ec71265dd6333fc98b04147e23d45aa916acd71ea2266ecf14577a5bd","contentType":"text/markdown; charset=utf-8"},{"id":"dfd4581e-a4ea-512f-abb0-f8e21a17718c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/dfd4581e-a4ea-512f-abb0-f8e21a17718c/attachment.md","path":"references/filter-ui-patterns.md","size":14482,"sha256":"0503dddf0d1fc5ae37843d0284492621954bb19ea920748bb9bd7d56dc7f3c51","contentType":"text/markdown; charset=utf-8"},{"id":"d928d52e-2267-547f-983a-a26e356c8941","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/d928d52e-2267-547f-983a-a26e356c8941/attachment.md","path":"references/library-comparison.md","size":10282,"sha256":"132e0d438b84c6a9736f5d7bd451299a9497524c35aabdb71f6c6f512800f7d1","contentType":"text/markdown; charset=utf-8"},{"id":"e50c21bf-4548-5be0-bf94-3a914b14caf2","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/e50c21bf-4548-5be0-bf94-3a914b14caf2/attachment.md","path":"references/performance-optimization.md","size":20109,"sha256":"15ea9bdbb82b70dcb52a1a8fb92e04f11d846148353823f21cc2d6dc1f99c75d","contentType":"text/markdown; charset=utf-8"},{"id":"dd7685d5-969f-5fca-bb04-2cbab04c822d","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/dd7685d5-969f-5fca-bb04-2cbab04c822d/attachment.md","path":"references/query-parameter-management.md","size":16719,"sha256":"9d47fd7793425f330169e3b2fdebe2ec7433fd71d015f1de8bcdd0fddd5665aa","contentType":"text/markdown; charset=utf-8"},{"id":"6472cfc6-8e9c-5990-acbc-829b0c8207d0","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/6472cfc6-8e9c-5990-acbc-829b0c8207d0/attachment.md","path":"references/search-input-patterns.md","size":10054,"sha256":"2c71a6448af41b9ec5c0c632392fc46c3365f29c08a3faae545fddca3e504eb7","contentType":"text/markdown; charset=utf-8"},{"id":"b1374026-0e31-54df-8ba6-8b7192a70312","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/b1374026-0e31-54df-8ba6-8b7192a70312/attachment.js","path":"scripts/debounce_calculator.js","size":13463,"sha256":"5b847ae304e47bc9e71fc2e92c97532884184e33532538b8d400164bbf55e4df","contentType":"application/javascript; charset=utf-8"},{"id":"ecf5e73b-149b-54c8-a224-788f767add7c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/ecf5e73b-149b-54c8-a224-788f767add7c/attachment.py","path":"scripts/generate_filter_query.py","size":12353,"sha256":"e6b44e1154b9653e543164442b1f0decb5e77dfc2dd9f57eeade6c5c86c5a189","contentType":"text/x-python; charset=utf-8"},{"id":"0e911b1c-954c-5617-9cf1-7a077346c7bf","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/0e911b1c-954c-5617-9cf1-7a077346c7bf/attachment.py","path":"scripts/validate_search_params.py","size":13094,"sha256":"a2f29541efb032870881b0163446094d48dbfd35a8109d2bdf960253832f523e","contentType":"text/x-python; charset=utf-8"}],"bundle_sha256":"bd6cf95de29b967e168ba27aea9d321131bbfed466643858180bd44591ef4cc2","attachment_count":20,"text_attachments":20,"attachment_storage":"skillopedia-attachments-v1","binary_attachments":0,"excluded_attachments":[]},"cluster_size":1,"skill_md_path":"skills/implementing-search-filter/SKILL.md","import_metadata":{"date":"2026-06-05","author":"@skillopedia","version":"v1","category":"web-development","category_label":"Web"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"web-development","import_tag":"clean-skills-v1","description":"Implements search and filter interfaces for both frontend (React/TypeScript) and backend (Python) with debouncing, query management, and database integration. Use when adding search functionality, building filter UIs, implementing faceted search, or optimizing search performance."}},"renderedAt":1782979943713}

Search & Filter Implementation Implement search and filter interfaces with comprehensive frontend components and backend query optimization. Purpose This skill provides production-ready patterns for implementing search and filtering functionality across the full stack. It covers React/TypeScript components for the frontend (search inputs, filter UIs, autocomplete) and Python patterns for the backend (SQLAlchemy queries, Elasticsearch integration, API design). The skill emphasizes performance optimization, accessibility, and user experience. When to Use - Building product search with category…