Skip to main content
Scratch provides fast, full-text search across all your notes using the Tantivy search engine, a Rust-based alternative to Lucene. Access search from anywhere in the app:
Press Cmd+Shift+F (Mac) or Ctrl+Shift+F (Windows/Linux) to open the sidebar search input.
This shortcut automatically shows the sidebar if it’s hidden.

Search Features

Full-Text Indexing

Scratch indexes both note titles and note content using Tantivy:
  • Inverted index - Efficient lookups across thousands of notes
  • Prefix queries - Automatically falls back to prefix matching (e.g., query*)
  • Case-insensitive - Matches regardless of capitalization
  • Top 20 results - Returns the most relevant matches
  • Score ranking - Results sorted by relevance score
The search index is automatically updated when you create, edit, or delete notes.
Search within the currently open note:
1

Open Find

Press Cmd+F (Mac) or Ctrl+F (Windows/Linux) while editing a note
2

Enter Query

Type your search query in the toolbar that appears
3

Navigate Matches

Use the arrow buttons or Enter / Shift+Enter to jump between matches
4

Visual Highlighting

All matches are highlighted in yellow with the current match brighter
const findMatches = useCallback(
  (query: string, editorInstance: TiptapEditor | null) => {
    if (!editorInstance || !query.trim()) return [];

    const doc = editorInstance.state.doc;
    const lowerQuery = query.toLowerCase();
    const matches: Array<{ from: number; to: number }> = [];

    // Search through each text node
    doc.descendants((node, nodePos) => {
      if (node.isText && node.text) {
        const text = node.text;
        const lowerText = text.toLowerCase();

        let searchPos = 0;
        while (searchPos < lowerText.length && matches.length < 500) {
          const index = lowerText.indexOf(lowerQuery, searchPos);
          if (index === -1) break;

          const matchFrom = nodePos + index;
          const matchTo = matchFrom + query.length;

          if (matchTo <= doc.content.size) {
            matches.push({ from: matchFrom, to: matchTo });
          }

          searchPos = index + 1;
        }
      }
    });

    return matches;
  },
  [],
);

Implementation

Tantivy Search Engine

Scratch uses Tantivy, a fast full-text search library written in Rust:
pub struct SearchIndex {
    index: Index,
    reader: IndexReader,
    writer: Mutex<IndexWriter>,
    schema: Schema,
    id_field: Field,
    title_field: Field,
    content_field: Field,
    modified_field: Field,
}

impl SearchIndex {
    fn new(index_path: &PathBuf) -> Result<Self> {
        // Build schema
        let mut schema_builder = Schema::builder();
        let id_field = schema_builder.add_text_field("id", STRING | STORED);
        let title_field = schema_builder.add_text_field("title", TEXT | STORED);
        let content_field = schema_builder.add_text_field("content", TEXT | STORED);
        let modified_field = schema_builder.add_i64_field("modified", INDEXED | STORED);
        let schema = schema_builder.build();

        // Create or open index
        std::fs::create_dir_all(index_path)?;
        let index = Index::create_in_dir(index_path, schema.clone())
            .or_else(|_| Index::open_in_dir(index_path))?;

        let reader = index
            .reader_builder()
            .reload_policy(ReloadPolicy::OnCommitWithDelay)
            .try_into()?;

        let writer = index.writer(50_000_000)?; // 50MB buffer

        Ok(Self {
            index,
            reader,
            writer: Mutex::new(writer),
            schema,
            id_field,
            title_field,
            content_field,
            modified_field,
        })
    }
}

Search Query Execution

The search implementation supports full-text queries with prefix fallback:
fn search(&self, query_str: &str, limit: usize) -> Result<Vec<SearchResult>> {
    let searcher = self.reader.searcher();
    let query_parser =
        QueryParser::for_index(&self.index, vec![self.title_field, self.content_field]);

    // Parse query, fall back to prefix query if parsing fails
    let query = query_parser
        .parse_query(query_str)
        .or_else(|_| query_parser.parse_query(&format!("{}*", query_str)))?;

    let top_docs = searcher.search(&query, &TopDocs::with_limit(limit))?;

    let mut results = Vec::with_capacity(top_docs.len());
    for (score, doc_address) in top_docs {
        let doc: TantivyDocument = searcher.doc(doc_address)?;

        let id = doc.get_first(self.id_field)
            .and_then(|v| v.as_str())
            .unwrap_or("").to_string();

        let title = doc.get_first(self.title_field)
            .and_then(|v| v.as_str())
            .unwrap_or("").to_string();

        let content = doc.get_first(self.content_field)
            .and_then(|v| v.as_str())
            .unwrap_or("");

        let modified = doc.get_first(self.modified_field)
            .and_then(|v| v.as_i64())
            .unwrap_or(0);

        let preview = generate_preview(content);

        results.push(SearchResult {
            id,
            title,
            preview,
            modified,
            score,
        });
    }

    Ok(results)
}

Index Management

Automatic Indexing

The search index updates automatically:
  • On note save - Note is re-indexed with updated content
  • On note creation - New note is added to the index
  • On note deletion - Note is removed from the index
  • On file changes - External edits trigger re-indexing

Manual Rebuild

Force a complete index rebuild via Tauri command:
import { invoke } from "@tauri-apps/api/core";

await invoke("rebuild_search_index");
Rebuilding the index can take several seconds for large note collections (1000+ notes). The UI remains responsive during rebuilding.

Search Storage

The Tantivy index is stored on disk:
~/.scratch/
└── index/
    ├── meta.json
    ├── .managed.json
    └── [segment files]
  • Location: {APP_DATA}/.scratch/index/
  • Size: Typically 10-20% of total note content size
  • Persistence: Index persists across app restarts

Performance

Fast Queries

Sub-millisecond search for most queries on typical note collections

50MB Buffer

Efficient indexing with large memory buffer for batch operations

Lazy Reloading

Index reader reloads on commit with delay policy for efficiency

500 Match Limit

In-note search caps at 500 matches to prevent performance issues
If Tantivy search fails, Scratch falls back to cache-based search:
  • Searches cached note metadata (title and preview)
  • Case-insensitive substring matching
  • Slower than Tantivy but ensures search always works
Use specific search terms for better results. Tantivy ranks exact matches and phrase matches higher than partial matches.

Build docs developers (and LLMs) love