@muhgholy/search-query-parser
v3.0.0
Published
A portable, framework-agnostic search query parser with Gmail-like syntax support
Maintainers
Readme
Search Query Parser
A portable, framework-agnostic search query parser with Gmail-like syntax support. Zero dependencies, TypeScript-first, and optimized for performance.
Features
- 🚀 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
Installation
npm install @muhgholy/search-query-parseryarn add @muhgholy/search-query-parserpnpm add @muhgholy/search-query-parserUsage
import { 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;
}
}Supported Syntax
Text Search
| 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 |
Lists & Arrays
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 |
Operators
| Operator | Aliases | Description | Example |
| ----------- | ------------------ | ----------------- | ------------------------- |
| from: | f:, sender: | From address/name | from:[email protected] |
| 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 |
Date Filters
| 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
Size Filters
| 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
API Reference
parse(input: string, options?: TParserOptions): TParseResult
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 }],
});tokenize(input: string): TToken[]
Low-level tokenizer for custom parsing needs.
const tokens = tokenize('from:john "hello world"');parseDate(value: string): { date: Date } | null
Parse date strings (absolute, relative, natural).
parseDate("-7d"); // { date: Date (7 days ago) }
parseDate("2024-01-01"); // { date: Date }
parseDate("last week"); // { date: Date }escapeRegex(str: string): string
Escape special regex characters.
escapeRegex("hello.*world"); // 'hello\\.\\*world'validate(input: string): { valid: boolean; errors: string[] }
Validate search query syntax.
validate('"unclosed quote'); // { valid: false, errors: ['Unmatched quote: "'] }hasTerms(input: string): boolean
Check if search string has any terms.
hasTerms(""); // false
hasTerms("hello"); // truesummarize(input: string): string[]
Get human-readable summary of search query.
summarize('"Promo" from:newsletter after:-7d');
// ['Exact: "Promo"', 'From: newsletter', 'After: 12/6/2024']Types
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[];
};Examples
MongoDB Integration
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 } : {};
}SQL Integration
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 };
}License
MIT © Muhammad Gholy
