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