A portable, framework-agnostic search query parser with Gmail-like syntax support. Zero dependencies, TypeScript-first, and optimized for performance.
- 🚀 Zero dependencies - Lightweight and fast
- 📝 Gmail-like syntax - Familiar search operators
- 🔧 Framework-agnostic - Works anywhere (Node.js, browser, edge)
- 📦 TypeScript-first - Full type safety
- ⚡ Optimized - Returns simple array for easy iteration
- 🎯 Extensible - Custom operators support
npm install @muhgholy/search-query-parseryarn add @muhgholy/search-query-parserpnpm add @muhgholy/search-query-parserimport { parse } from "@muhgholy/search-query-parser"; const terms = parse('"Promo Code" -spam from:newsletter after:-7d'); // Returns: TParsedTerm[] // [ // { type: 'phrase', value: 'Promo Code', negated: false }, // { type: 'text', value: 'spam', negated: true }, // { type: 'from', value: 'newsletter', negated: false }, // { type: 'after', value: '-7d', negated: false, date: Date } // ] // Simple iteration for (const term of terms) { switch (term.type) { case "text": case "phrase": // Handle text search break; case "from": // Handle from filter break; case "after": if (term.date) { // Use resolved date } break; case "or": // Handle OR logic // term.terms contains the operands break; case "group": // Handle group // term.terms contains the inner terms break; } }| Syntax | Description | Example |
|---|---|---|
word | Plain text search | hello |
"phrase" | Exact phrase match | "hello world" |
-word | Exclude term | -spam |
-"phrase" | Exclude phrase | -"unsubscribe here" |
( ... ) | Grouping | (term1 term2) |
OR | Logical OR | term1 OR term2 |
Comma-separated values are automatically treated as OR conditions.
| Syntax | Description | Example |
|---|---|---|
key:val1,val2 | Value 1 OR Value 2 | to:john,jane |
key:"a","b" | Quoted list | to:"John Doe","Jane" |
-key:val1,val2 | NOT val1 AND NOT val2 | -from:spam,marketing |
| Operator | Aliases | Description | Example |
|---|---|---|---|
from: | f:, sender: | From address/name | from:john@example.com |
to: | t:, recipient: | To address/name | to:jane |
subject: | subj:, s: | Subject line | subject:meeting |
body: | content:, b: | Body content | body:invoice |
has: | - | Has property | has:attachment |
is: | - | Status filter | is:unread |
in: | folder:, box: | Folder/mailbox | in:inbox |
label: | tag:, l: | Label/tag | label:important |
header-k: | hk: | Header key | header-k:X-Custom |
header-v: | hv: | Header value | header-v:"custom value" |
date: | d: | Date/Range | date:2024-01-01 |
before: | b4:, older: | Before date | before:2024-12-31 |
after: | af:, newer: | After date | after:2024-01-01 |
size: | larger:, smaller: | Size filter | size:>1mb |
| Syntax | Description | Example |
|---|---|---|
date:YYYY-MM-DD | Specific date | date:2024-01-01 |
date:Start-End | Date range | date:2024-01-01-2024-12-31 |
after:YYYY-MM-DD | After date (absolute) | after:2024-01-01 |
before:YYYY-MM-DD | Before date (absolute) | before:2024-12-31 |
after:-Nd | After N days ago | after:-7d |
after:-Nh | After N hours ago | after:-24h |
after:-Nw | After N weeks ago | after:-2w |
after:-Nm | After N months ago | after:-1m |
after:-Ny | After N years ago | after:-1y |
after:"natural" | Natural language | after:"last week" |
Supported natural dates: today, yesterday, tomorrow, last week, last month, last year, this week, this month, this year
| Syntax | Description | Example |
|---|---|---|
size:>N | Larger than N bytes | size:>1mb |
size:<N | Smaller than N bytes | size:<100kb |
size:N | Equal to N bytes | size:500 |
Supported units: b, kb, mb, gb
Parse a search query string into an array of terms.
const terms = parse('"hello world" from:john -spam', { operatorsAllowed: ["from", "to"], // Only allow specific operators // OR operatorsDisallowed: ["size"], // Block specific operators // Custom operators operators: [{ name: "priority", aliases: ["p"], type: "string", allowNegation: true }], });Low-level tokenizer for custom parsing needs.
const tokens = tokenize('from:john "hello world"');Parse date strings (absolute, relative, natural).
parseDate("-7d"); // { date: Date (7 days ago) } parseDate("2024-01-01"); // { date: Date } parseDate("last week"); // { date: Date }Escape special regex characters.
escapeRegex("hello.*world"); // 'hello\\.\\*world'Validate search query syntax.
validate('"unclosed quote'); // { valid: false, errors: ['Unmatched quote: "'] }Check if search string has any terms.
hasTerms(""); // false hasTerms("hello"); // trueGet human-readable summary of search query.
summarize('"Promo" from:newsletter after:-7d'); // ['Exact: "Promo"', 'From: newsletter', 'After: 12/6/2024']type TDefaultTermType = | "text" // Plain text | "phrase" // Exact phrase | "from" // From filter | "to" // To filter | "subject" // Subject filter | "body" // Body filter | "header-k" // Header key | "header-v" // Header value | "has" // Has property | "is" // Status filter | "in" // Folder filter | "before" // Before date | "after" // After date | "label" // Label filter | "size" // Size filter | "or" // Logical OR | "group"; // Parenthesized group type TTermType = TDefaultTermType; type TParsedTerm<T extends string = TTermType> = { type: T; value: string; negated: boolean; date?: Date; // Resolved date (for date types) dateRange?: { // Resolved date range start: Date; end: Date; }; size?: { // Parsed size (for size type) op: "gt" | "lt" | "eq"; bytes: number; }; terms?: TParsedTerm<T>[]; // For 'or' and 'group' types }; type TParseResult<T extends string = TTermType> = TParsedTerm<T>[]; type TOperatorDef<T extends string = TTermType> = { name: string; // Operator name (becomes term type) aliases: string[]; // Alternative names type: "string" | "date" | "size"; // Value parsing type allowNegation: boolean; // Whether negation is allowed }; type TParserOptions<T extends string = TTermType> = { operators?: TOperatorDef<T>[]; caseSensitive?: boolean; operatorsAllowed?: string[]; operatorsDisallowed?: string[]; };import { parse, escapeRegex } from "@muhgholy/search-query-parser"; function buildMongoQuery(searchQuery: string) { const terms = parse(searchQuery); const conditions = []; for (const term of terms) { const regex = { $regex: escapeRegex(term.value), $options: "i" }; switch (term.type) { case "text": case "phrase": conditions.push({ $or: [{ title: term.negated ? { $not: regex } : regex }, { content: term.negated ? { $not: regex } : regex }], }); break; case "from": conditions.push({ "from.email": regex }); break; case "after": if (term.date) { conditions.push({ createdAt: { $gte: term.date } }); } break; } } return conditions.length ? { $and: conditions } : {}; }import { parse, escapeRegex } from "@muhgholy/search-query-parser"; function buildSQLWhere(searchQuery: string) { const terms = parse(searchQuery); const clauses = []; const params = []; for (const term of terms) { switch (term.type) { case "text": clauses.push(term.negated ? "(title NOT LIKE ? AND content NOT LIKE ?)" : "(title LIKE ? OR content LIKE ?)"); params.push(`%${term.value}%`, `%${term.value}%`); break; case "after": if (term.date) { clauses.push("created_at >= ?"); params.push(term.date.toISOString()); } break; } } return { where: clauses.join(" AND "), params }; }MIT © Muhammad Gholy