Custom Search

Rspress provides built-in full-text search functionality, but also allows you to customize the search experience or integrate third-party search services. This chapter covers search configuration, customization, and integration with external search providers.

Rspress includes FlexSearch-based search by default:

// rspress.config.ts
import { defineConfig } from 'rspress/config';

export default defineConfig({
  search: {
    // Enable built-in search (enabled by default)
    versioned: true,
  },
});

Search Features

The built-in search provides:

  • Full-text search - Search across all documentation content
  • Instant results - Real-time search as you type
  • Keyboard navigation - Navigate results with arrow keys
  • Highlighting - Matched terms are highlighted in results
  • Version filtering - Filter by documentation version

Search Keyboard Shortcuts

  • Ctrl/Cmd + K - Open search
  • Arrow keys - Navigate results
  • Enter - Go to selected result
  • Esc - Close search

Basic Configuration

// rspress.config.ts
export default defineConfig({
  search: {
    // Include/exclude specific paths
    include: ['docs/**/*.md'],
    exclude: ['**/private/**', '**/temp/**'],

    // Search placeholder text
    placeholder: 'Search documentation...',

    // Maximum number of results
    limit: 10,

    // Enable versioned search
    versioned: true,

    // Custom hotkey
    hotkey: {
      key: 'k',
      ctrlKey: true,
    },
  },
});

Advanced Configuration

export default defineConfig({
  search: {
    // Customize search options
    searchOptions: {
      // Minimum characters to trigger search
      minChars: 2,

      // Maximum results per page
      maxResults: 50,

      // Boost for title matches
      titleBoost: 2,

      // Boost for heading matches
      headingBoost: 1.5,

      // Enable fuzzy matching
      fuzzy: true,

      // Distance for fuzzy matching
      distance: 100,
    },

    // Customize indexing
    indexOptions: {
      // Fields to index
      fields: ['title', 'content', 'headers'],

      // Fields to store (returned in results)
      store: ['title', 'content', 'url'],

      // Tokenizer settings
      tokenize: 'forward',
    },
  },
});

Custom Search UI

Override Search Component

Create a custom search component:

// theme/components/CustomSearch.tsx
import { useState, useEffect } from 'react';
import { useSearchIndex } from 'rspress/runtime';
import './CustomSearch.css';

export function CustomSearch() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [isOpen, setIsOpen] = useState(false);
  const searchIndex = useSearchIndex();

  const handleSearch = async (searchQuery: string) => {
    if (searchQuery.length < 2) {
      setResults([]);
      return;
    }

    const searchResults = await searchIndex.search(searchQuery, {
      limit: 10,
      enrich: true,
    });

    setResults(searchResults);
  };

  useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
        e.preventDefault();
        setIsOpen(true);
      }
    };

    window.addEventListener('keydown', handleKeyDown);
    return () => window.removeEventListener('keydown', handleKeyDown);
  }, []);

  return (
    <>
      <button
        className="search-button"
        onClick={() => setIsOpen(true)}
      >
        <span className="search-icon">🔍</span>
        <span className="search-text">Search...</span>
        <kbd className="search-hotkey">⌘K</kbd>
      </button>

      {isOpen && (
        <div className="search-modal">
          <div className="search-backdrop" onClick={() => setIsOpen(false)} />
          <div className="search-content">
            <input
              type="text"
              className="search-input"
              placeholder="Search documentation..."
              value={query}
              onChange={(e) => {
                setQuery(e.target.value);
                handleSearch(e.target.value);
              }}
              autoFocus
            />

            <div className="search-results">
              {results.map((result) => (
                <a
                  key={result.id}
                  href={result.url}
                  className="search-result-item"
                  onClick={() => setIsOpen(false)}
                >
                  <div className="result-title">{result.title}</div>
                  <div className="result-excerpt">{result.excerpt}</div>
                </a>
              ))}

              {query.length >= 2 && results.length === 0 && (
                <div className="no-results">No results found</div>
              )}
            </div>
          </div>
        </div>
      )}
    </>
  );
}

Custom Search Styles

/* theme/components/CustomSearch.css */
.search-button {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 1rem;
  background: var(--rp-c-bg-soft);
  border: 1px solid var(--rp-c-border);
  border-radius: 6px;
  cursor: pointer;
  transition: all 0.2s;
}

.search-button:hover {
  background: var(--rp-c-bg-mute);
  border-color: var(--rp-c-brand);
}

.search-hotkey {
  padding: 0.125rem 0.375rem;
  background: var(--rp-c-bg);
  border: 1px solid var(--rp-c-divider);
  border-radius: 4px;
  font-size: 0.75rem;
  font-family: var(--rp-font-family-mono);
}

.search-modal {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 9999;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding-top: 10vh;
}

.search-backdrop {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.5);
  backdrop-filter: blur(4px);
}

.search-content {
  position: relative;
  width: 90%;
  max-width: 600px;
  background: var(--rp-c-bg);
  border-radius: 12px;
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
  overflow: hidden;
}

.search-input {
  width: 100%;
  padding: 1rem 1.5rem;
  border: none;
  border-bottom: 1px solid var(--rp-c-divider);
  font-size: 1rem;
  background: transparent;
  color: var(--rp-c-text-1);
  outline: none;
}

.search-results {
  max-height: 400px;
  overflow-y: auto;
}

.search-result-item {
  display: block;
  padding: 1rem 1.5rem;
  border-bottom: 1px solid var(--rp-c-divider);
  text-decoration: none;
  color: inherit;
  transition: background 0.2s;
}

.search-result-item:hover {
  background: var(--rp-c-bg-soft);
}

.result-title {
  font-weight: 600;
  color: var(--rp-c-brand);
  margin-bottom: 0.25rem;
}

.result-excerpt {
  font-size: 0.875rem;
  color: var(--rp-c-text-2);
  line-height: 1.5;
}

.no-results {
  padding: 2rem 1.5rem;
  text-align: center;
  color: var(--rp-c-text-3);
}

Algolia DocSearch Integration

Why Algolia DocSearch?

Algolia DocSearch provides:

  • Extremely fast search
  • Better ranking algorithm
  • Typo tolerance
  • Analytics and insights
  • Free for open-source projects

Setup Algolia DocSearch

  1. Apply for DocSearch

Visit docsearch.algolia.com and apply for free access.

  1. Get Your Credentials

After approval, you'll receive:

  • Application ID
  • API Key
  • Index Name
  1. Configure in Rspress
// rspress.config.ts
export default defineConfig({
  search: {
    algolia: {
      appId: 'YOUR_APP_ID',
      apiKey: 'YOUR_SEARCH_API_KEY',
      indexName: 'YOUR_INDEX_NAME',

      // Optional configuration
      searchParameters: {
        facetFilters: ['language:en', 'version:1.0'],
      },

      // Custom placeholder
      placeholder: 'Search docs...',

      // Custom translations
      translations: {
        button: {
          buttonText: 'Search',
          buttonAriaLabel: 'Search documentation',
        },
        modal: {
          searchBox: {
            resetButtonTitle: 'Clear',
            resetButtonAriaLabel: 'Clear',
            cancelButtonText: 'Cancel',
            cancelButtonAriaLabel: 'Cancel',
          },
          footer: {
            selectText: 'to select',
            navigateText: 'to navigate',
            closeText: 'to close',
          },
        },
      },
    },
  },
});

Customize Algolia UI

/* Custom Algolia search styles */
.DocSearch-Button {
  background: var(--rp-c-bg-soft);
  border: 1px solid var(--rp-c-border);
  border-radius: 8px;
}

.DocSearch-Button:hover {
  background: var(--rp-c-bg-mute);
  border-color: var(--rp-c-brand);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

.DocSearch-Modal {
  --docsearch-primary-color: var(--rp-c-brand);
  --docsearch-text-color: var(--rp-c-text-1);
  --docsearch-modal-background: var(--rp-c-bg);
  --docsearch-searchbox-background: var(--rp-c-bg-soft);
}

Custom Search Provider

Create a custom search provider:

// lib/customSearch.ts
export interface SearchProvider {
  search(query: string): Promise<SearchResult[]>;
  index(content: any[]): Promise<void>;
}

export interface SearchResult {
  id: string;
  title: string;
  excerpt: string;
  url: string;
  score: number;
}

export class CustomSearchProvider implements SearchProvider {
  private apiEndpoint: string;

  constructor(config: { apiEndpoint: string }) {
    this.apiEndpoint = config.apiEndpoint;
  }

  async search(query: string): Promise<SearchResult[]> {
    const response = await fetch(`${this.apiEndpoint}/search`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ query }),
    });

    const data = await response.json();
    return data.results;
  }

  async index(content: any[]): Promise<void> {
    await fetch(`${this.apiEndpoint}/index`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ content }),
    });
  }
}

Use Custom Provider

// rspress.config.ts
import { CustomSearchProvider } from './lib/customSearch';

export default defineConfig({
  search: {
    provider: new CustomSearchProvider({
      apiEndpoint: 'https://api.example.com',
    }),
  },
});

Search Analytics

Track Search Queries

// theme/components/SearchAnalytics.tsx
import { useEffect } from 'react';

export function useSearchAnalytics() {
  useEffect(() => {
    const handleSearch = (e: CustomEvent) => {
      const { query, results } = e.detail;

      // Send to analytics
      if (typeof window !== 'undefined' && window.gtag) {
        window.gtag('event', 'search', {
          search_term: query,
          results_count: results.length,
        });
      }
    };

    window.addEventListener('rspress:search', handleSearch as EventListener);
    return () => {
      window.removeEventListener('rspress:search', handleSearch as EventListener);
    };
  }, []);
}
// lib/searchAnalytics.ts
export class SearchAnalytics {
  private searches: Map<string, number> = new Map();

  trackSearch(query: string, resultsCount: number) {
    const count = this.searches.get(query) || 0;
    this.searches.set(query, count + 1);

    // Log searches with no results
    if (resultsCount === 0) {
      console.log(`No results for: ${query}`);
    }
  }

  getTopSearches(limit: number = 10): [string, number][] {
    return Array.from(this.searches.entries())
      .sort((a, b) => b[1] - a[1])
      .slice(0, limit);
  }
}

Search Optimization

Improve Search Indexing

// rspress.config.ts
export default defineConfig({
  search: {
    indexOptions: {
      // Index custom fields
      fields: {
        title: { weight: 3 },      // Higher weight for titles
        headings: { weight: 2 },   // Medium weight for headings
        content: { weight: 1 },     // Lower weight for content
        tags: { weight: 2 },        // Weight for tags
      },

      // Exclude noise words
      stopWords: ['the', 'a', 'an', 'and', 'or', 'but'],

      // Stemming for better matching
      stemming: true,

      // Case sensitivity
      caseSensitive: false,
    },
  },
});

Add Search Metadata

Improve search results with frontmatter:

---
title: Custom Search
description: Learn how to customize search in Rspress
tags: [search, customization, algolia]
keywords: search, custom search, algolia, docsearch
---

# Custom Search

Your content here...

Troubleshooting

Search Not Working

Check configuration:

// Ensure search is enabled
export default defineConfig({
  search: true,  // or detailed config object
});

Poor Search Results

Improve indexing:

export default defineConfig({
  search: {
    // Include more content
    include: ['**/*.md', '**/*.mdx'],

    // Adjust weights
    indexOptions: {
      fields: {
        title: { weight: 5 },
        content: { weight: 1 },
      },
    },
  },
});

Algolia Not Loading

Check credentials and network:

// Verify credentials
console.log('App ID:', process.env.ALGOLIA_APP_ID);
console.log('API Key:', process.env.ALGOLIA_API_KEY);

// Check if Algolia is loaded
if (typeof window !== 'undefined' && window.docsearch) {
  console.log('Algolia loaded successfully');
}

Best Practices

1. Optimize Index Size

Keep search index manageable:

export default defineConfig({
  search: {
    // Exclude unnecessary content
    exclude: [
      '**/generated/**',
      '**/temp/**',
      '**/*.test.md',
    ],
  },
});

2. Use Descriptive Titles

Help users find content:

---
title: How to Configure Custom Search
---

Better than: "Configuration"

3. Add Search Keywords

Include common search terms:

---
keywords: [search, find, lookup, query]
---

4. Test Search Regularly

Monitor search quality:

# Test common search queries
- "getting started"
- "configuration"
- "api reference"

Next Steps


::: tip 💡 Search Tips

  • Use Algolia DocSearch for public documentation (it's free!)
  • Implement search analytics to understand user needs
  • Regularly test and optimize search results
  • Add synonyms for common terms :::

::: info 📊 Search Performance

  • Built-in search: Best for small to medium sites (<500 pages)
  • Algolia: Best for large sites (500+ pages) or public docs
  • Custom provider: Best for specific requirements or existing infrastructure :::

::: warning ⚠️ Privacy Considerations

  • Search queries may be logged for analytics
  • Consider privacy policies when using third-party search
  • Allow users to opt-out of search tracking if needed :::