π Parse plaintext search queries into easy-to-use filter structures.
This library takes a human-typed search query string and parses it into a structured Query object containing typed filters (KeywordFilter, FieldFilter). It supports quoted strings, negation, comparison operators, and field-based filtering.
composer require technically/search-queryRequirements:
- PHP 8.4+
useTechnically\SearchQuery\QueryParser;
$parser = newQueryParser();
$query = $parser->parse('tag:php -legacy "best practices"');
foreach ($query->filtersas$filter) {
// Filter instances...
}| Syntax | Parsed As |
|---|---|
hello | KeywordFilter('hello') |
"hello world" | KeywordFilter('hello world', quoted: true) |
-hello | KeywordFilter('hello', exclude: true) |
tag:php | FieldFilter('tag', ':', 'php') |
-tag:php | FieldFilter('tag', ':', 'php', exclude: true) |
year>2020 | FieldFilter('year', '>', '2020') |
year>=2020 | FieldFilter('year', '>=', '2020') |
year<2020 | FieldFilter('year', '<', '2020') |
year<=2020 | FieldFilter('year', '<=', '2020') |
hello\ world | KeywordFilter('hello world') (escaped whitespace) |
"custom field":value | FieldFilter('custom field', ':', 'value') |
A leading - (minus) before a keyword or field filter negates it. Multiple minuses are gracefully collapsed into a single negation.
-apple -> KeywordFilter('apple', exclude: true)
-tag:legacy -> FieldFilter('tag', ':', 'legacy', exclude: true)
Double quotes group multiple words into a single token. Quotes can be escaped with \.
"hello world" -> KeywordFilter('hello world', quoted: true)
field:"hello world" -> FieldFilter('field', ':', 'hello world', quoted: true)
The backslash \ escape character works both inside and outside quoted strings:
apples\ fruits -> KeywordFilter('apples fruits')
55\" -> KeywordFilter('55"')
"hello \"world\"" -> KeywordFilter('hello "world"', quoted: true)
The parser is built using the Tolerant Reader design pattern β to be forgiving with malformed input. It never throws.
The main entry point for parsing query strings.
useTechnically\SearchQuery\QueryParser;
$parser = newQueryParser();
$query = $parser->parse('your search query');The parser accepts an optional Tokenizer instance in its constructor. By default, it uses QueryTokenizer.
parse(string $query): Queryβ Parses a query string into aQueryobject.
An immutable value object representing the parsed search query.
useTechnically\SearchQuery\Query;
$query = newQuery([
newKeywordFilter('php'),
newFieldFilter('tag', ':', 'tutorial'),
]);public readonly array $filtersβ Array ofFilterinstances.
static empty(): selfβ Create a new empty query.isEmpty(): boolβ Check if the query is empty (has no filters).toString(): stringβ Serializes the query back to the search query syntax string.
All filters implement the Technically\SearchQuery\Filters\Filter marker interface.
Represents a free-text keyword search term.
useTechnically\SearchQuery\Filters\KeywordFilter;
newKeywordFilter('php');
newKeywordFilter('hello world', quoted: true);
newKeywordFilter('legacy', exclude: true);Properties:
public readonly string $keywordβ The keyword value.public readonly bool $quotedβ Whether the keyword was originally quoted.public readonly bool $excludeβ Whether the keyword is negated.
Methods:
unquote(): selfβ Returns a new instance withquotedset tofalse.toString(): stringβ Serializes the filter back to query syntax.
Represents a field-based filter (field:operator:value).
useTechnically\SearchQuery\Filters\FieldFilter;
newFieldFilter('year', '>', '2020');
newFieldFilter('status', ':', 'active', quoted: true);
newFieldFilter('tag', ':', 'legacy', exclude: true);Properties:
public readonly string $fieldβ The field name.public readonly FilterOperator $operatorβ The comparison operator.public readonly string $valueβ The filter value.public readonly bool $quotedβ Whether the value was originally quoted.public readonly bool $excludeβ Whether the filter is negated.
Methods:
matches(...): boolβ Check whether the filter matches the given properties.unquote(): selfβ Returns a new instance withquotedset tofalse.toString(): stringβ Serializes the filter back to query syntax.
useTechnically\SearchQuery\QueryParser;
useTechnically\SearchQuery\Filters\KeywordFilter;
useTechnically\SearchQuery\Filters\FieldFilter;
$parser = newQueryParser();
$query = $parser->parse('php -legacy "best practices" year>=2020');
foreach ($query->filtersas$filter) {
if ($filterinstanceof KeywordFilter) {
echo"Keyword: {$filter->keyword}"
. ($filter->exclude ? ' (excluded)' : '')
. ($filter->quoted ? ' (quoted)' : '')
. "\n";
} elseif ($filterinstanceof FieldFilter) {
echo"Field: {$filter->field}{$filter->operator->value}{$filter->value}"
. ($filter->exclude ? ' (excluded)' : '')
. ($filter->quoted ? ' (quoted)' : '')
. "\n";
}
}
// Output:// Keyword: php// Keyword: legacy (excluded)// Keyword: best practices (quoted)// Field: year >= 2020$filter = newFieldFilter('tag', ':', 'hello world', quoted: true, exclude: true);
echo$filter->toString(); // -tag:"hello world"// Or serialize an entire Query back to string:$query = newQuery([
newKeywordFilter('php'),
newFieldFilter('year', '>', '2020', exclude: true),
]);
echo$query->toString(); // php -year>2020useTechnically\SearchQuery\QueryParser;
useTechnically\SearchQuery\Contracts\Tokenizer;
class MyCustomTokenizer implements Tokenizer
{
publicfunctiontokenize(string$query): iterable
{
// Custom tokenization logic...
}
}
$parser = newQueryParser(newMyCustomTokenizer());composer testsTests are written with Pest PHP.
MIT
Implemented by πΎ Ivan Voskoboinyk.