UNPKG

@jungle-commerce/typesense-react

Version:

React hooks and components for building search interfaces with Typesense

1,462 lines (1,255 loc) • 39.6 kB
# @jungle-commerce/typesense-react A powerful, headless React library for building search experiences with Typesense. This package provides comprehensive search state management, advanced filtering, multi-collection search, and automatic schema discovery - all without UI dependencies. ## Features - šŸš€ **React Hooks** - Simple hooks like `useSearch` and `useAdvancedFacets` for instant search - šŸ”§ **TypeScript First** - Full type safety and IntelliSense support - ⚔ **Performance Optimized** - Request debouncing, caching, and smart re-rendering - šŸŽØ **UI Agnostic** - Works with any React UI library (Material-UI, Ant Design, etc.) - šŸ” **Advanced Search** - Filtering, faceting, sorting, and geo-search out of the box - šŸ“± **Multi-Collection** - Search across multiple collections simultaneously - šŸ¤– **AI Ready** - Claude MCP integration for AI-powered search assistance - 🧠 **Schema Discovery** - Automatically configure search based on your data ## Documentation šŸ“š **[Full Documentation](./docs/README.md)** - Comprehensive guides, API reference, and examples ### Quick Links - **Getting Started** - [Installation and Setup](./docs/getting-started/01-installation.md) - [Hello World](./docs/getting-started/02-hello-world.md) - [Basic Search](./docs/getting-started/03-basic-search.md) - [Adding Filters](./docs/getting-started/04-adding-filters.md) - [Pagination](./docs/getting-started/05-pagination.md) - **API Reference** - [Core API Reference](./docs/api-reference/core-api-reference.md) - [Hooks API Reference](./docs/api-reference/hooks-api-reference.md) - [Providers Guide](./docs/api-reference/providers-guide.md) - [Context API](./docs/api-reference/context-api.md) - [TypeScript Reference](./docs/api-reference/typescript-reference.md) - [Migration Guide](./docs/api-reference/migration-guide.md) - **Guides & Tutorials** - [Basic Search Tutorial](./docs/guides/basic-search-tutorial.md) - [Faceted Search Tutorial](./docs/guides/faceted-search-tutorial.md) - [Multi-Collection Search](./docs/guides/multi-collection-tutorial.md) - [Filter Builder Guide](./docs/guides/filter-builder-guide.md) - [Sort Builder Guide](./docs/guides/sort-builder-guide.md) - [Date Helpers Guide](./docs/guides/date-helpers-guide.md) - [Utilities Reference](./docs/guides/utilities-reference.md) - [Testing Guide](./docs/guides/testing-guide.md) - [Integration Patterns](./docs/guides/integration-patterns.md) - [Troubleshooting](./docs/guides/troubleshooting.md) - **Examples** - [Minimal Search](./examples/01-minimal-search/) - Simplest possible search implementation - [Product Search](./examples/02-product-search/) - E-commerce product search with filters - [Documentation Search](./examples/03-documentation-search/) - Search through documentation - [Multi-Collection Search](./examples/04-multi-collection/) - Search across multiple collections - [Basic Search App](./examples/basic-search/) - Basic search with TypeScript - [Advanced Filtering](./examples/advanced-filtering/) - Complex filtering examples - [Multi-Collection Demo](./examples/multi-collection/) - Another multi-collection example ### Additional Documentation - **Testing Documentation** - [Testing Guide](./docs/TESTING.md) - Comprehensive testing documentation - [Integration Testing](./docs/INTEGRATION_TESTING.md) - Integration test setup - [Test Infrastructure](./docs/TEST_INFRASTRUCTURE.md) - Test infrastructure details - **Code Examples** - [Feature Examples](./docs/feature-examples.md) - Examples of all features - [Error Handling Examples](./docs/error-handling-examples.md) - Error handling patterns - [Date Filtering Examples](./docs/date-filtering-examples.md) - Date filter examples - [Performance Optimization Examples](./docs/performance-optimization-examples.md) - Performance tips - **API Pattern Documentation** - [Core Patterns](./docs/api-reference/core-patterns.md) - Core API patterns - [Hooks Patterns](./docs/api-reference/hooks-patterns.md) - Hook usage patterns - [Core Examples](./docs/api-reference/core-examples.md) - Core API examples - [Hooks Examples](./docs/api-reference/hooks-examples.md) - Hook examples - [Provider Examples](./docs/api-reference/provider-examples.md) - Provider examples - [Type Examples](./docs/api-reference/type-examples.md) - TypeScript type examples ## Installation ```bash npm install @jungle-commerce/typesense-react typesense ``` ## Quick Start Examples ### Connecting to Typesense ```tsx import { TypesenseConfig } from '@jungle-commerce/typesense-react'; const typesenseConfig: TypesenseConfig = { nodes: [{ host: 'localhost', port: 8108, protocol: 'http' }], apiKey: 'your-search-api-key', connectionTimeoutSeconds: 2, cacheSearchResultsForSeconds: 60 // 1 minute cache }; ``` ### Retrieving a Schema ```tsx import { TypesenseSearchClient } from '@jungle-commerce/typesense-react'; const client = new TypesenseSearchClient(typesenseConfig); const schema = await client.retrieveSchema('products'); console.log(schema); ``` ## Static/Fixed Configuration Examples ### Simple Query ```tsx import React from 'react'; import { SearchProvider, useSearch } from '@jungle-commerce/typesense-react'; function App() { return ( <SearchProvider config={typesenseConfig} collection="products" searchOnMount={true} > <SearchInterface /> </SearchProvider> ); } function SearchInterface() { const { state, actions } = useSearch(); return ( <div> <input value={state.query} onChange={(e) => actions.setQuery(e.target.value)} placeholder="Search products..." /> {state.loading && <p>Searching...</p>} {state.results?.hits.map(hit => ( <div key={hit.document.id}> <h3>{hit.document.name}</h3> <p>{hit.document.description}</p> </div> ))} </div> ); } ``` ### Filtered Query ```tsx import { SearchProvider, useSearch, useAdvancedFacets } from '@jungle-commerce/typesense-react'; function App() { const facetConfig = [ { field: 'category', label: 'Category', type: 'checkbox', disjunctive: true // Allow multiple selections with OR logic }, { field: 'price', label: 'Price Range', type: 'numeric', numericDisplay: 'range' } ]; return ( <SearchProvider config={typesenseConfig} collection="products" facets={facetConfig} searchOnMount={true} > <FilteredSearch /> </SearchProvider> ); } function FilteredSearch() { const { state } = useSearch(); const facets = useAdvancedFacets(); return ( <div> {/* Category Filter */} <div> <h4>Categories</h4> {state.results?.facet_counts?.find(f => f.field_name === 'category')?.counts.map(item => ( <label key={item.value}> <input type="checkbox" checked={facets.disjunctiveFacets.category?.includes(item.value)} onChange={() => facets.actions.toggleFacetValue('category', item.value)} /> {item.value} ({item.count}) </label> ))} </div> {/* Price Range Filter */} <div> <h4>Price Range</h4> <input type="range" min="0" max="1000" value={facets.numericFilters.price?.min || 0} onChange={(e) => facets.actions.setNumericFilter('price', Number(e.target.value), facets.numericFilters.price?.max || 1000 )} /> </div> {/* Results */} <div> {state.results?.hits.map(hit => ( <div key={hit.document.id}> <h3>{hit.document.name}</h3> <p>Category: {hit.document.category}</p> <p>Price: ${hit.document.price}</p> </div> ))} </div> </div> ); } ``` ### Query with Sorting ```tsx function SortedSearch() { const { state, actions } = useSearch(); return ( <div> <select value={state.sortBy} onChange={(e) => actions.setSortBy(e.target.value)} > <option value="">Relevance</option> <option value="price:asc">Price: Low to High</option> <option value="price:desc">Price: High to Low</option> <option value="created_at:desc">Newest First</option> </select> {/* Results sorted according to selection */} {state.results?.hits.map(hit => ( <div key={hit.document.id}>{hit.document.name}</div> ))} </div> ); } ``` ### Query with Faceting ```tsx function FacetedSearch() { const facetConfig = [ { field: 'brand', label: 'Brand', type: 'checkbox', disjunctive: true }, { field: 'color', label: 'Color', type: 'select' }, { field: 'in_stock', label: 'Availability', type: 'checkbox' } ]; return ( <SearchProvider config={typesenseConfig} collection="products" facets={facetConfig} searchOnMount={true} enableDisjunctiveFacetQueries={true} // Enable OR logic for disjunctive facets > <FacetInterface /> </SearchProvider> ); } function FacetInterface() { const { state } = useSearch(); const facets = useAdvancedFacets(); return ( <div> {state.facets.map(facetConfig => { const facetResults = state.results?.facet_counts?.find( f => f.field_name === facetConfig.field ); if (facetConfig.type === 'select') { return ( <select key={facetConfig.field} value={facets.selectiveFilters[facetConfig.field] || ''} onChange={(e) => facets.actions.setSelectiveFilter( facetConfig.field, e.target.value )} > <option value="">All {facetConfig.label}</option> {facetResults?.counts.map(item => ( <option key={item.value} value={item.value}> {item.value} ({item.count}) </option> ))} </select> ); } return ( <div key={facetConfig.field}> <h4>{facetConfig.label}</h4> {facetResults?.counts.map(item => ( <label key={item.value}> <input type="checkbox" checked={facets.disjunctiveFacets[facetConfig.field]?.includes(item.value)} onChange={() => facets.actions.toggleFacetValue(facetConfig.field, item.value)} /> {item.value} ({item.count}) </label> ))} </div> ); })} </div> ); } ``` ### Query Utilizing All Functionality ```tsx function AdvancedSearch() { const facetConfig = [ { field: 'category', label: 'Category', type: 'checkbox', disjunctive: true }, { field: 'price', label: 'Price', type: 'numeric', numericDisplay: 'range' }, { field: 'rating', label: 'Rating', type: 'numeric', numericDisplay: 'checkbox' }, { field: 'release_date', label: 'Release Date', type: 'date' }, { field: 'status', label: 'Status', type: 'select' } ]; return ( <SearchProvider config={typesenseConfig} collection="products" facets={facetConfig} searchOnMount={true} accumulateFacets={true} // Remember facet values across searches initialState={{ perPage: 24, sortBy: 'popularity:desc', additionalFilters: 'in_stock:true', // Native Typesense filter multiSortBy: [ // Multi-field sorting { field: 'popularity', order: 'desc' }, { field: 'price', order: 'asc' } ] }} > <FullFeaturedSearch /> </SearchProvider> ); } function FullFeaturedSearch() { const { state, actions } = useSearch(); const facets = useAdvancedFacets(); // Calculate derived values const totalResults = state.results?.found || 0; const totalPages = Math.ceil(totalResults / state.perPage); const hasNextPage = state.page < totalPages; const hasPreviousPage = state.page > 1; return ( <div> {/* Search Input */} <input value={state.query} onChange={(e) => actions.setQuery(e.target.value)} placeholder="Search..." /> {/* Additional Filters */} <button onClick={() => actions.setAdditionalFilters('featured:true && discount:>0')}> Show Featured & Discounted </button> {/* Multi-field Sort */} <button onClick={() => actions.setMultiSortBy([ { field: 'rating', order: 'desc' }, { field: 'price', order: 'asc' }, { field: 'name', order: 'asc' } ])}> Sort by Rating, then Price, then Name </button> {/* All Facets */} <div> <h3>Filters ({facets.activeFilterCount} active)</h3> {/* Render all configured facets */} {/* ... facet rendering code ... */} <button onClick={() => facets.actions.clearAllFilters()}> Clear All Filters </button> </div> {/* Results */} <div> <p>Found {totalResults} results (Page {state.page} of {totalPages})</p> {state.results?.hits.map(hit => ( <div key={hit.document.id}> {/* Highlighted fields */} <h3 dangerouslySetInnerHTML={{ __html: hit.highlight?.name?.snippet || hit.document.name }} /> </div> ))} {/* Pagination */} <button onClick={() => actions.setPage(state.page - 1)} disabled={!hasPreviousPage} > Previous </button> <button onClick={() => actions.setPage(state.page + 1)} disabled={!hasNextPage} > Next </button> </div> </div> ); } ``` ## Schema Discovery Examples ### Discovery with Specific Field Names ```tsx import { useSchemaDiscovery, SearchProvider } from '@jungle-commerce/typesense-react'; function SchemaBasedSearch() { const { schema, facetConfigs, searchableFields, sortableFields } = useSchemaDiscovery({ // Exclude certain fields from faceting excludeFields: ['internal_id', 'created_by'], maxFacets: 10, includeNumericFacets: true, includeDateFacets: true, onSchemaLoad: (schema) => { console.log('Schema loaded:', schema); } }); if (!schema) return <div>Loading schema...</div>; return ( <SearchProvider config={typesenseConfig} collection="products" facets={facetConfigs} // Auto-generated facet config initialSearchParams={{ query_by: searchableFields.join(','), // Auto-detected searchable fields sort_by: sortableFields[0]?.field // First sortable field }} > <SearchInterface /> </SearchProvider> ); } ``` ### Discovery with Pattern Matching ```tsx function PatternBasedDiscovery() { const { schema, facetConfigs } = useSchemaDiscovery({ // Exclude fields by pattern patterns: { excludePatterns: [ { pattern: 'internal', matchType: 'contains' }, { pattern: '_id', matchType: 'endsWith' } ] }, // Other configuration maxFacets: 8, includeNumericFacets: true, // Override specific field types facetOverrides: { price: { type: 'numeric', numericDisplay: 'range' }, status: { type: 'select' }, created_at: { type: 'date' } } }); return ( <SearchProvider config={typesenseConfig} collection="products" facets={facetConfigs} > <SearchInterface /> </SearchProvider> ); } ``` ## Dynamic Queries with Unknown Fields ### Simple Dynamic Query ```tsx function DynamicSearch() { const { schema, facetConfigs, searchableFields } = useSchemaDiscovery(); if (!schema) return <div>Discovering schema...</div>; return ( <SearchProvider config={typesenseConfig} collection="products" facets={facetConfigs} initialSearchParams={{ query_by: searchableFields.join(',') || '*' }} searchOnMount={true} > <GenericSearchInterface /> </SearchProvider> ); } function GenericSearchInterface() { const { state, actions } = useSearch(); return ( <div> <input value={state.query} onChange={(e) => actions.setQuery(e.target.value)} placeholder={`Search in ${state.schema?.fields.length} fields...`} /> {state.results?.hits.map(hit => ( <div key={hit.document.id}> {/* Dynamically render all fields */} {Object.entries(hit.document).map(([key, value]) => ( <p key={key}><strong>{key}:</strong> {JSON.stringify(value)}</p> ))} </div> ))} </div> ); } ``` ### Dynamic Filtered Query ```tsx function DynamicFilteredSearch() { const { schema, facetConfigs } = useSchemaDiscovery({ // Auto-detect facetable fields maxFacets: 15, includeNumericFacets: true, includeDateFacets: true }); return ( <SearchProvider config={typesenseConfig} collection="products" facets={facetConfigs} > <DynamicFilters /> </SearchProvider> ); } function DynamicFilters() { const { state } = useSearch(); const facets = useAdvancedFacets(); return ( <div> {state.facets.map(facetConfig => { const facetResult = state.results?.facet_counts?.find( f => f.field_name === facetConfig.field ); // Render appropriate UI based on facet type switch (facetConfig.type) { case 'numeric': return <NumericFacet key={facetConfig.field} config={facetConfig} />; case 'date': return <DateFacet key={facetConfig.field} config={facetConfig} />; case 'select': return <SelectFacet key={facetConfig.field} config={facetConfig} />; default: return <CheckboxFacet key={facetConfig.field} config={facetConfig} />; } })} </div> ); } ``` ### Dynamic Sorting ```tsx function DynamicSortedSearch() { const { schema, sortableFields } = useSchemaDiscovery({ collection: 'products', enabled: true }); return ( <SearchProvider config={typesenseConfig} collection="products" initialState={{ sortBy: sortableFields[0]?.field ? `${sortableFields[0].field}:desc` : '' }} > <DynamicSortInterface sortableFields={sortableFields} /> </SearchProvider> ); } function DynamicSortInterface({ sortableFields }) { const { state, actions } = useSearch(); return ( <div> <select value={state.sortBy} onChange={(e) => actions.setSortBy(e.target.value)}> <option value="">Relevance</option> {sortableFields.map(field => ( <> <option key={`${field.field}-asc`} value={`${field.field}:asc`}> {field.label} (Low to High) </option> <option key={`${field.field}-desc`} value={`${field.field}:desc`}> {field.label} (High to Low) </option> </> ))} </select> </div> ); } ``` ### Dynamic Faceting ```tsx function DynamicFacetedSearch() { const { schema, facetConfigs } = useSchemaDiscovery({ // Maximum facets to generate maxFacets: 10, includeNumericFacets: true, includeDateFacets: true, // Custom type overrides facetOverrides: { status: { type: 'select' }, type: { type: 'select' }, created_at: { type: 'date' }, updated_at: { type: 'date' } } }); return ( <SearchProvider config={typesenseConfig} collection="products" facets={facetConfigs} accumulateFacets={true} > <AutoFacetInterface /> </SearchProvider> ); } ``` ### Dynamic Query with All Features ```tsx function FullDynamicSearch() { const { schema, facetConfigs, searchableFields, sortableFields } = useSchemaDiscovery({ maxFacets: 20, includeNumericFacets: true, includeDateFacets: true }); // Derive field groups from schema const fieldGroups = React.useMemo(() => { if (!schema?.fields) return { numericFields: [], booleanFields: [] }; return { numericFields: schema.fields .filter(f => ['int32', 'int64', 'float'].includes(f.type)) .map(f => f.name), booleanFields: schema.fields .filter(f => f.type === 'bool') .map(f => f.name) }; }, [schema]); return ( <SearchProvider config={typesenseConfig} collection="products" facets={facetConfigs} searchOnMount={true} accumulateFacets={true} initialSearchParams={{ query_by: searchableFields.join(','), sort_by: sortableFields[0]?.field }} initialState={{ // Dynamic multi-sort based on field types multiSortBy: [ // First by any boolean "featured" field ...fieldGroups.booleanFields .filter(f => f.includes('featured')) .map(f => ({ field: f, order: 'desc' as const })), // Then by any numeric "score" or "rating" field ...fieldGroups.numericFields .filter(f => f.includes('score') || f.includes('rating')) .map(f => ({ field: f, order: 'desc' as const })), // Finally by name { field: 'name', order: 'asc' as const } ] }} > <DynamicSearchInterface /> </SearchProvider> ); } ``` ## Autocomplete Implementation ### Simple Autocomplete ```tsx function Autocomplete() { const [suggestions, setSuggestions] = useState([]); const [query, setQuery] = useState(''); return ( <SearchProvider config={typesenseConfig} collection="products" searchOnMount={false} > <AutocompleteInput value={query} onChange={setQuery} suggestions={suggestions} onSuggestionsChange={setSuggestions} /> </SearchProvider> ); } function AutocompleteInput({ value, onChange, suggestions, onSuggestionsChange }) { const { state, actions } = useSearch(); React.useEffect(() => { if (state.results && !state.loading) { // Extract suggestions from results const newSuggestions = state.results.hits.slice(0, 5).map(hit => ({ id: hit.document.id, text: hit.document.name, highlight: hit.highlight?.name?.snippet })); onSuggestionsChange(newSuggestions); } }, [state.results, state.loading, onSuggestionsChange]); React.useEffect(() => { if (value.length >= 2) { actions.setQuery(value); } else { onSuggestionsChange([]); } }, [value, actions, onSuggestionsChange]); return ( <div className="autocomplete"> <input type="text" value={value} onChange={(e) => onChange(e.target.value)} placeholder="Start typing..." /> {suggestions.length > 0 && ( <div className="suggestions"> {suggestions.map(suggestion => ( <div key={suggestion.id} onClick={() => onChange(suggestion.text)} dangerouslySetInnerHTML={{ __html: suggestion.highlight || suggestion.text }} /> ))} </div> )} </div> ); } ``` ## Search-as-you-type Implementation ```tsx function SearchAsYouType() { return ( <SearchProvider config={typesenseConfig} collection="products" searchOnMount={false} initialSearchParams={{ query_by: 'name,description', prefix: true, // Enable prefix search num_typos: 2, // Allow typos min_len_1typo: 4, // Minimum length for 1 typo min_len_2typo: 7 // Minimum length for 2 typos }} > <InstantSearch /> </SearchProvider> ); } function InstantSearch() { const { state, actions } = useSearch(); return ( <div> <input value={state.query} onChange={(e) => actions.setQuery(e.target.value)} placeholder="Search instantly..." autoFocus /> {state.query && ( <div className="instant-results"> {state.loading && <div className="typing-indicator">Searching...</div>} {!state.loading && state.results?.found === 0 && ( <div>No results for "{state.query}"</div> )} {state.results?.hits.map(hit => ( <div key={hit.document.id} className="instant-result"> <strong dangerouslySetInnerHTML={{ __html: hit.highlight?.name?.snippet || hit.document.name }} /> <small dangerouslySetInnerHTML={{ __html: hit.highlight?.description?.snippet || hit.document.description }} /> </div> ))} </div> )} </div> ); } ``` ## Multi-Collection Search ### Basic Multi-Collection Search ```tsx import { MultiCollectionProvider, useMultiCollectionContext } from '@jungle-commerce/typesense-react'; function MultiSearch() { const collections = [ { collection: 'products', queryBy: 'name,description,brand', weight: 2.0, // Products are twice as important maxResults: 20 }, { collection: 'categories', queryBy: 'name,description', weight: 1.0, maxResults: 5 }, { collection: 'brands', queryBy: 'name', weight: 1.0, maxResults: 5 } ]; return ( <MultiCollectionProvider config={typesenseConfig} defaultCollections={collections} > <UnifiedSearch /> </MultiCollectionProvider> ); } function UnifiedSearch() { const { state, search, setQuery } = useMultiCollectionContext(); const handleSearch = (query: string) => { setQuery(query); search({ query, mergeStrategy: 'relevance', // Merge by relevance score normalizeScores: true, // Normalize scores across collections resultMode: 'interleaved' // Mix results together }); }; return ( <div> <input value={state.query} onChange={(e) => handleSearch(e.target.value)} placeholder="Search everything..." /> {state.results?.hits.map(hit => ( <div key={`${hit._collection}-${hit.document.id}`}> <span className="collection-badge">{hit._collection}</span> <h3>{hit.document.name || hit.document.title}</h3> <p>Score: {hit._normalizedScore.toFixed(2)}</p> </div> ))} </div> ); } ``` ### Multi-Collection with Schema Discovery ```tsx function DynamicMultiSearch() { const [collections, setCollections] = useState([]); // Discover collections dynamically useEffect(() => { async function discoverCollections() { const client = new TypesenseSearchClient(typesenseConfig); const allCollections = await client.collections().retrieve(); // Configure each collection based on its schema const configured = await Promise.all( allCollections.map(async (col) => { const schema = await client.collections(col.name).retrieve(); const searchableFields = schema.fields .filter(f => f.type === 'string' || f.type === 'string[]') .map(f => f.name); return { collection: col.name, queryBy: searchableFields.join(','), weight: col.name === 'products' ? 2.0 : 1.0, maxResults: 10 }; }) ); setCollections(configured); } discoverCollections(); }, []); if (collections.length === 0) return <div>Loading collections...</div>; return ( <MultiCollectionProvider config={typesenseConfig} defaultCollections={collections} > <DynamicUnifiedSearch /> </MultiCollectionProvider> ); } ``` ### Multi-Collection Autocomplete ```tsx function MultiCollectionAutocomplete() { const collections = [ { collection: 'products', queryBy: 'name,sku', maxResults: 3, namespace: 'product' }, { collection: 'categories', queryBy: 'name,path', maxResults: 2, namespace: 'category' }, { collection: 'brands', queryBy: 'name', maxResults: 2, namespace: 'brand' } ]; return ( <MultiCollectionProvider config={typesenseConfig} defaultCollections={collections} searchOptions={{ debounceMs: 150, searchOnMount: false }} > <MultiAutocomplete /> </MultiCollectionProvider> ); } function MultiAutocomplete() { const { state, search, setQuery } = useMultiCollectionContext(); const [isOpen, setIsOpen] = useState(false); const handleInputChange = (value: string) => { setQuery(value); if (value.length >= 2) { search({ query: value, resultMode: 'perCollection', // Group by collection perCollectionLimit: 5 }); setIsOpen(true); } else { setIsOpen(false); } }; return ( <div className="multi-autocomplete"> <input value={state.query} onChange={(e) => handleInputChange(e.target.value)} placeholder="Search products, categories, brands..." /> {isOpen && state.results?.hitsByCollection && ( <div className="suggestions-panel"> {Object.entries(state.results.hitsByCollection).map(([collection, hits]) => ( <div key={collection} className="collection-group"> <h4>{collection}</h4> {hits.map(hit => ( <div key={hit.document.id} className="suggestion" onClick={() => { // Handle selection based on namespace console.log(`Selected ${hit._namespace}: ${hit.document.name}`); setIsOpen(false); }} > {hit.highlight?.name?.snippet || hit.document.name} </div> ))} </div> ))} </div> )} </div> ); } ``` ### Multi-Collection Search-as-you-type ```tsx function MultiCollectionInstantSearch() { const collections = [ { collection: 'products', queryBy: 'name,description', weight: 2.0, includeFields: 'id,name,price,image', maxResults: 10 }, { collection: 'help_articles', queryBy: 'title,content', weight: 1.0, includeFields: 'id,title,excerpt', maxResults: 5 } ]; return ( <MultiCollectionProvider config={typesenseConfig} defaultCollections={collections} searchOptions={{ debounceMs: 100, minQueryLength: 2 }} > <InstantMultiSearch /> </MultiCollectionProvider> ); } function InstantMultiSearch() { const { state, search, setQuery, clearResults } = useMultiCollectionContext(); useEffect(() => { if (state.query.length >= 2) { search({ query: state.query, enableHighlighting: true, highlightConfig: { startTag: '<mark>', endTag: '</mark>' }, // Use round-robin to ensure variety mergeStrategy: 'roundRobin', globalMaxResults: 20 }); } else { clearResults(); } }, [state.query]); return ( <div className="instant-multi-search"> <input value={state.query} onChange={(e) => setQuery(e.target.value)} placeholder="Start typing to search..." className="search-input" /> <div className="results-container"> {state.loading && <div className="loading">Searching...</div>} {state.results?.hits.map(hit => ( <div key={`${hit._collection}-${hit.document.id}`} className={`result-item ${hit._collection}`} > <div className="result-header"> <span className="collection-label">{hit._collection}</span> <span className="relevance-score"> {(hit._normalizedScore * 100).toFixed(0)}% match </span> </div> {hit._collection === 'products' ? ( <ProductResult hit={hit} /> ) : ( <ArticleResult hit={hit} /> )} </div> ))} {state.results && state.results.totalFound === 0 && ( <div className="no-results"> No results found for "{state.query}" </div> )} </div> </div> ); } function ProductResult({ hit }) { return ( <div className="product-result"> {hit.document.image && ( <img src={hit.document.image} alt={hit.document.name} /> )} <div> <h3 dangerouslySetInnerHTML={{ __html: hit.highlight?.name?.snippet || hit.document.name }} /> <p className="price">${hit.document.price}</p> </div> </div> ); } function ArticleResult({ hit }) { return ( <div className="article-result"> <h3 dangerouslySetInnerHTML={{ __html: hit.highlight?.title?.snippet || hit.document.title }} /> <p dangerouslySetInnerHTML={{ __html: hit.highlight?.excerpt?.snippet || hit.document.excerpt }} /> </div> ); } ``` --- ## Previous Documentation The following sections contain additional advanced features and implementation details: ### Advanced Features - [Advanced Faceting](#advanced-faceting) - Multiple filter types with disjunctive support - [UI State Management](#ui-state-management) - Built-in facet UI state handling - [Advanced Filtering](#advanced-filtering) - Raw Typesense filter support - [Schema Intelligence](#schema-intelligence) - Auto-configuration from schema - [Performance Optimization](#performance-optimization) - Caching and query optimization - [URL State Management](#url-state-management) - Shareable search URLs ### Advanced Faceting Configure different facet types based on your data: ```tsx const facetConfig = [ { field: 'category', label: 'Category', type: 'checkbox', // Multi-select checkboxes disjunctive: true, // OR logic between selections maxValues: 10, // Maximum values to show searchable: true, // Enable search within facet sortBy: 'count' // Sort by count or value }, { field: 'price', label: 'Price', type: 'numeric', // Numeric range filter numericDisplay: 'range', // 'checkbox' | 'range' | 'both' rangeStep: 10 // Step size for range slider }, { field: 'created_at', label: 'Date Added', type: 'date', // Date range picker dateFormat: 'YYYY-MM-DD' }, { field: 'status', label: 'Status', type: 'select' // Single-select dropdown }, { field: 'tags', label: 'Tags', type: 'custom', // Custom filter type renderLabel: (value) => value.toUpperCase() } ]; ``` ### UI State Management The `useFacetState` hook manages UI state for facets: ```tsx // Global facet UI state const facetUI = useFacetState(); // Search within facet values facetUI.setFacetSearch('category', 'elec'); const filteredValues = values.filter(v => v.value.toLowerCase().includes(facetUI.getFacetSearch('category')) ); // Manage facet expansion facetUI.toggleFacetExpanded('category'); const isExpanded = facetUI.isFacetExpanded('category'); // Track scroll position facetUI.setFacetScrollTop('category', 100); const scrollTop = facetUI.getFacetScrollTop('category'); ``` ### Advanced Filtering and Sorting The package now supports native Typesense filter_by strings and multi-field sorting: ```tsx // Add raw Typesense filters actions.setAdditionalFilters('in_stock:true && (category:electronics || category:computers)'); // Multi-field sorting actions.setMultiSortBy([ { field: 'price', order: 'desc' }, { field: 'rating', order: 'desc' }, { field: 'name', order: 'asc' } ]); // Or use in initial state <SearchProvider initialState={{ additionalFilters: 'featured:true && discount:>0', multiSortBy: [ { field: 'popularity', order: 'desc' }, { field: 'price', order: 'asc' } ] }} > ``` ### Performance Optimization ```tsx <SearchProvider config={config} collection="products" performanceMode={true} // Disable expensive features enableDisjunctiveFacetQueries={false} // Disable parallel facet queries accumulateFacets={false} // Disable facet accumulation > ``` ### Utility Functions Build complex filter strings programmatically: ```tsx import { buildDisjunctiveFilter, buildNumericFilter, buildDateFilter, combineFilters, buildMultiSortString, parseSortString } from '@jungle-commerce/typesense-react'; // Build filters const categoryFilter = buildDisjunctiveFilter('category', ['Electronics', 'Books']); const priceFilter = buildNumericFilter('price', 10, 100); const combined = combineFilters([categoryFilter, priceFilter]); // Build sort strings const sortString = buildMultiSortString([ { field: 'price', order: 'desc' }, { field: 'name', order: 'asc' } ]); // Returns: "price:desc,name:asc" ``` ## API Reference For detailed API documentation, see the [API Reference](#api-reference) section in the original documentation above. ## Testing ### Quick Start ```bash # Run unit tests pnpm test # Run integration tests (requires Docker) pnpm test:integration # Run all tests with coverage pnpm test:all:coverage ``` ### Test Setup 1. **Install dependencies**: `pnpm install` 2. **For integration tests**: Ensure Docker is installed and running 3. **Run tests**: Use the commands above For detailed testing documentation, see our [Testing Guide](docs/TESTING.md). ### Testing Your Implementation When using typesense-react in your application: ```tsx import { SearchProvider } from '@jungle-commerce/typesense-react'; import { render } from '@testing-library/react'; const renderWithSearch = (ui: React.ReactElement) => { return render( <SearchProvider config={mockConfig} collection="test"> {ui} </SearchProvider> ); }; // Test your components it('should search when typing', async () => { renderWithSearch(<YourSearchComponent />); // ... test implementation }); ``` ## Claude Integration This package includes comprehensive Claude MCP (Model Context Protocol) integration documentation: - [Claude Integration Guide](./docs/claude-mcp/claude-integration.md) - [Claude API Reference](./docs/claude-mcp/api-reference-claude.md) - [Claude Examples](./docs/claude-mcp/claude-examples.md) ## Testing ```bash # Run unit tests pnpm test # Run integration tests (requires Docker) pnpm test:integration # Run all tests with coverage pnpm test:all:coverage ``` For detailed testing documentation, see our [Testing Guide](./docs/guides/testing-guide.md). ## Contributing We welcome contributions! Please see our [Contributing Guide](./CONTRIBUTING.md). ## Support - [GitHub Issues](https://github.com/jungle-commerce/typesense-react/issues) - [NPM Package](https://www.npmjs.com/package/@jungle-commerce/typesense-react) - [Documentation](./docs) ## License MIT Ā© Jungle Commerce