π 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+
use Technically\SearchQuery\QueryParser;
$parser = new QueryParser();
$query = $parser->parse('tag:php -legacy "best practices"');
foreach ($query->filters as $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.
use Technically\SearchQuery\QueryParser;
$parser = new QueryParser();
$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.
use Technically\SearchQuery\Query;
$query = new Query([
new KeywordFilter('php'),
new FieldFilter('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.
use Technically\SearchQuery\Filters\KeywordFilter;
new KeywordFilter('php');
new KeywordFilter('hello world', quoted: true);
new KeywordFilter('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).
use Technically\SearchQuery\Filters\FieldFilter;
new FieldFilter('year', '>', '2020');
new FieldFilter('status', ':', 'active', quoted: true);
new FieldFilter('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.
use Technically\SearchQuery\QueryParser;
use Technically\SearchQuery\Filters\KeywordFilter;
use Technically\SearchQuery\Filters\FieldFilter;
$parser = new QueryParser();
$query = $parser->parse('php -legacy "best practices" year>=2020');
foreach ($query->filters as $filter) {
if ($filter instanceof KeywordFilter) {
echo "Keyword: {$filter->keyword}"
. ($filter->exclude ? ' (excluded)' : '')
. ($filter->quoted ? ' (quoted)' : '')
. "\n";
} elseif ($filter instanceof 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 = new FieldFilter('tag', ':', 'hello world', quoted: true, exclude: true);
echo $filter->toString(); // -tag:"hello world"
// Or serialize an entire Query back to string:
$query = new Query([
new KeywordFilter('php'),
new FieldFilter('year', '>', '2020', exclude: true),
]);
echo $query->toString(); // php -year>2020use Technically\SearchQuery\QueryParser;
use Technically\SearchQuery\Contracts\Tokenizer;
class MyCustomTokenizer implements Tokenizer
{
public function tokenize(string $query): iterable
{
// Custom tokenization logic...
}
}
$parser = new QueryParser(new MyCustomTokenizer());composer testsTests are written with Pest PHP.
MIT
Implemented by πΎ Ivan Voskoboinyk.