Scratch provides fast, full-text search across all your notes using the Tantivy search engine, a Rust-based alternative to Lucene.
Quick Search
Access search from anywhere in the app:
Keyboard Shortcut
Command Palette
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.
Open Cmd+P / Ctrl+P and start typing your search query - results appear in real-time.
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.
In-Note Search
Search within the currently open note:
Open Find
Press Cmd+F (Mac) or Ctrl+F (Windows/Linux) while editing a note
Enter Query
Type your search query in the toolbar that appears
Navigate Matches
Use the arrow buttons or Enter / Shift+Enter to jump between matches
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
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
Fallback Search
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.