Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Fulltext Search

CRUDCrate provides case-insensitive substring search across multiple fields.

Mark fields to include in search:

#[derive(EntityToModels)]
pub struct Model {
    #[crudcrate(fulltext)]
    pub title: String,

    #[crudcrate(fulltext)]
    pub description: String,

    #[crudcrate(fulltext)]
    pub tags: String,

    // Not searchable
    pub internal_code: String,
}

Search Syntax

Use the q parameter:

# Simple search
GET /items?q=rust programming

# Search with other parameters
GET /items?q=async&filter={"status":"published"}&sort=["created_at","DESC"]

How It Works

The search term is matched as a substring of the concatenated searchable fields. There is no fuzzy or similarity matching: a typo (“progamming”) does not match “programming”. The term is bound as a query parameter, and LIKE wildcards in it (%, _) are escaped with ! so they match literally.

PostgreSQL

Uses ILIKE for case-insensitive matching:

-- Generated query (simplified)
SELECT * FROM items
WHERE (COALESCE(title::text, '') || ' ' || COALESCE(description::text, ''))
    ILIKE '%rust programming%' ESCAPE '!'

MySQL & SQLite

Uses UPPER(...) LIKE with the pattern uppercased on the Rust side:

-- Generated query (simplified)
SELECT * FROM items
WHERE UPPER(CAST(title AS TEXT) || ' ' || CAST(description AS TEXT))
    LIKE '%RUST PROGRAMMING%' ESCAPE '!'

The uppercasing of the search pattern is ASCII-reliable; non-ASCII case folding can differ between Rust and the database collation on these backends.

The query is treated as a single phrase, matching records where the concatenated fields contain the search string.

Search Behavior

The entire query is treated as a single search term:

GET /items?q=rust programming

# Matches items containing the phrase "rust programming"
# Does NOT split into separate "rust" AND "programming" terms

Case Insensitivity

All searches are case-insensitive:

GET /items?q=RUST
GET /items?q=rust
GET /items?q=Rust

# All return the same results

Partial Matching (Substring)

All databases support substring matching via LIKE:

GET /items?q=rust

# Matches: "rust", "rusty", "trustworthy", "Rust Programming"

Combining with Filters

Search works with other query parameters:

# Search within active items
GET /items?q=tutorial&filter={"status":"active"}

# Search + sort + paginate
GET /items?q=rust&sort=["created_at","DESC"]&range=[0,9]

Performance Tips

Index Strategy

DatabaseRecommended Index
PostgreSQLOptional: a pg_trgm GIN index (gin_trgm_ops) accelerates ILIKE '%term%' scans
MySQLStandard B-tree on searched columns
SQLiteStandard indexes on searched columns

Query Optimization

  1. Limit results: Always paginate search results
  2. Use filters: Narrow results before fulltext search
  3. Cache common searches: For popular queries
# Slow: fulltext search on all items
GET /items?q=rust

# Fast: filter first, then search
GET /items?q=rust&filter={"category":"programming"}&range=[0,19]

Highlighting Results

For search result highlighting, implement post-processing:

fn highlight_matches(text: &str, query: &str) -> String {
    let terms: Vec<&str> = query.split_whitespace().collect();
    let mut result = text.to_string();

    for term in terms {
        let pattern = regex::Regex::new(&format!("(?i)({})", regex::escape(term))).unwrap();
        result = pattern.replace_all(&result, "<mark>$1</mark>").to_string();
    }

    result
}

Empty or whitespace-only queries return all items:

GET /items?q=
GET /items?q=

# Both return unfiltered results (with pagination)

Special Characters

Search queries are sanitized:

# Special characters are escaped
GET /items?q=c++
GET /items?q=node.js
GET /items?q=user@email

# All work safely

Next Steps