Search Functionality
Relevant source files
The following files were used as context for generating this wiki page:The search functionality for the Railway documentation provides users with a fast, real-time search experience. It is powered by MeiliSearch on the backend for indexing and searching, and implemented on the frontend using React components and a custom hook for debounced searching. The entire search experience is encapsulated within a modal, which can be triggered by a user click or the $mod+K keyboard shortcut.
The system is designed to be responsive, fetching and displaying results as the user types. It includes features like result highlighting, keyboard navigation, and grouping of results by documentation section to enhance usability. Data is fed into MeiliSearch via a scraper configured to parse the structure of the documentation site.
Architecture Overview
The search system consists of two main parts: a data ingestion pipeline and a client-side search interface.
- Data Ingestion: The
docs-scrapertool from MeiliSearch is configured viameilisearch-docs-scraper.config.jsonto crawl the documentation website, extract content based on specific CSS selectors, and index it in a MeiliSearch instance. - Client-Side Interface: A React-based implementation that includes a search modal, input components, and a results display. The core logic is managed by the
useDebouncedSearchhook, which communicates with the MeiliSearch API.
The following diagram illustrates the high-level data flow from user interaction to displaying results.
Data Ingestion and Indexing
The content for the search index is gathered by the MeiliSearch docs-scraper. The configuration for this process is defined in meilisearch-docs-scraper.config.json.
Scraper Configuration
The scraper is configured to start at the root of the documentation site and uses CSS selectors to identify and structure the content.
| Key | Description | Value |
|---|---|---|
index_uid | The unique identifier for the MeiliSearch index. | docs_railway_app_production |
start_urls | The initial URLs the scraper will visit. | https://docs.railway.com/ |
selectors | A map of CSS selectors to extract hierarchical content (lvl0 to lvl4) and the main text body. | See table below |
custom_settings | Custom MeiliSearch index settings, such as synonyms. | postgres <=> postgresql, api <=> graphql <=> gql |
Sources: meilisearch-docs-scraper.config.json:2-26
Content Selectors
The following selectors are used to build the hierarchical structure of the search documents.
| Level | Selector | Description |
|---|---|---|
lvl0 | .sidebar .current-section | The main section title from the sidebar (e.g., "Guides", "Reference"). |
lvl1 | .docs-content h1 | The main title of the page. |
lvl2 | .docs-content h2 | A secondary heading on the page. |
lvl3 | .docs-content h3 | A tertiary heading on the page. |
lvl4 | .docs-content h4 | A quaternary heading on the page. |
text | .docs-content p, li, a, code, td, pre | The text content associated with the headings. |
Sources: meilisearch-docs-scraper.config.json:11-18
Core Search Logic
The client-side search logic is primarily handled by the useDebouncedSearch custom hook. This hook encapsulates state management, API communication, and response transformation.
Sources: src/hooks/useDebouncedSearch.ts
useDebouncedSearch Hook
This hook provides a debounced interface to the MeiliSearch index.
Parameters:
| Parameter | Type | Description |
|---|---|---|
host | string | The URL of the MeiliSearch host. |
apiKey | string | The public API key for reading from the index. |
indexName | string | The name of the MeiliSearch index to query. |
params | SearchParams | MeiliSearch parameters (e.g., limit, attributesToHighlight). |
debounceMs | number | The debounce delay in milliseconds. Default is 500. |
transformResponse | Transformer | A function to transform the API response into the desired Result structure. |
Sources: src/hooks/useDebouncedSearch.ts:51-62
State and Returned Values:
The hook manages several internal states and returns an object for the UI components to use.
| Returned Value | Type | Description |
|---|---|---|
query | string | The current value of the search input, for rendering. |
isSearching | boolean | A loading state indicator, true while a search is in progress. |
clearResponse | () => void | A function to clear the query and results. |
setQuery | (string) => void | A function to update the raw input value. |
results | Result | null | The transformed and grouped search results, or null if no query. |
Sources: src/hooks/useDebouncedSearch.ts:111-120
Search and Debounce Flow
The following sequence diagram shows how user input is debounced before an API request is made.
Response Transformation
The default transformer function, defaultTransformResponse, processes the raw SearchResponse from MeiliSearch. It groups the hits by their top-level hierarchy (hierarchy_lvl0) and formats them into a Search.Result object, which is a record where keys are chapter titles and values are arrays of result items.
Sources: src/hooks/useDebouncedSearch.ts:15-49
UI Components
The search interface is composed of several React components that work together.
Search Modal
The Modal component, located in src/components/Search/Modal.tsx, serves as the main container for the search UI. It is displayed when the isSearchOpen state in a global searchStore is true.
- It initializes
useDebouncedSearchwith the necessary MeiliSearch credentials and parameters. - The search parameters are configured to limit results to 10 and to wrap highlighted matches in
<span>tags. - It conditionally renders a
LoadingIndicator,NoResultscomponent, or theResultscomponent based on theisSearchingandresultsstate from the hook.
// src/components/Search/Modal.tsx:12-17
const searchParams = {
limit: 10,
attributesToHighlight: ["*"],
highlightPreTag: "<span>",
highlightPostTag: "</span>",
};
Sources: src/components/Search/Modal.tsx:11-23, src/layouts/Page.tsx:13-22, 40-46
Results Display
The Results component (src/components/Search/Results.tsx) is responsible for rendering the search results.
- Grouping: It iterates over the
resultsobject and renders each group of hits under its corresponding chapter title. - Highlighting: It uses the
interweavelibrary'sMarkupcomponent to render thetextfield, which contains the HTML<span>tags for highlighting matched terms. - Keyboard Navigation: It uses the
tinykeyslibrary to listen forArrowDown,ArrowUp, andEnterkeys to allow users to navigate and select results without using a mouse. - Slug Cleaning: It processes the
slugfrom the search hit to remove the base URI and any extraneous fragments like#__nextbefore navigation.
Sources: src/components/Search/Results.tsx
Data Structures
The search functionality relies on specific data structures defined in src/types.ts within the Search namespace.
Search.Document
This interface represents the structure of a single document as indexed in MeiliSearch by the docs-scraper.
| Field | Type | Description |
|---|---|---|
hierarchy_lvl0 | string | The top-level section title (e.g., "Guides"). |
hierarchy_lvl1 | string | The page title. |
hierarchy_lvl2 | string | An h2 heading. |
hierarchy_lvl3 | string | An h3 heading. |
hierarchy_lvl4 | string | An h4 heading. |
url | string | The full URL to the content. |
content | string | The text content. |
Sources: src/types.ts:25-33
Search.Result
This type represents the data structure after it has been transformed by the useDebouncedSearch hook, ready for rendering.
// src/types.ts:20-24
export namespace Search {
interface ResultItem {
hierarchies: string[];
slug: string;
text: string;
}
export type Result = Record<string, ResultItem[]>;
// ...
}
The Result type is an object where keys are the hierarchy_lvl0 values (chapter titles), and the values are arrays of ResultItem objects. Each ResultItem contains the cleaned hierarchy, the URL slug, and the highlighted text snippet.
Sources: src/types.ts:20-24, src/hooks/useDebouncedSearch.ts:15-49