technically/search-query

Parse plaintext search queries into easy-to-use structures

Maintainers

Package info

github.com/technically-php/search-query

pkg:composer/technically/search-query

Statistics

Installs: 2

Dependents: 0

Suggesters: 0

Stars: 1

Open Issues: 0

0.2.0 2026-06-16 12:44 UTC

This package is auto-updated.

Last update: 2026-06-16 12:48:05 UTC


README

🔍 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.

Test

Installation

composer require technically/search-query

Requirements:

  • PHP 8.4+

Quick Start

use Technically\SearchQuery\QueryParser;

$parser = new QueryParser();
$query  = $parser->parse('tag:php -legacy "best practices"');

foreach ($query->filters as $filter) {
    // Filter instances...
}

Supported Query Syntax

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')

Negation

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)

Quoting

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)

Escaping

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 Tolerant Reader

The parser is built using the Tolerant Reader design pattern — to be forgiving with malformed input. It never throws.

API Reference

QueryParser

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.

Methods

  • parse(string $query): Query — Parses a query string into a Query object.

Query

An immutable value object representing the parsed search query.

use Technically\SearchQuery\Query;

$query = new Query([
    new KeywordFilter('php'),
    new FieldFilter('tag', ':', 'tutorial'),
]);

Properties

  • public readonly array $filters — Array of Filter instances.

Methods

  • 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.

Filters

All filters implement the Technically\SearchQuery\Filters\Filter marker interface.

KeywordFilter

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 with quoted set to false.
  • toString(): string — Serializes the filter back to query syntax.

FieldFilter

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 with quoted set to false.
  • toString(): string — Serializes the filter back to query syntax.

Examples

Parse a complex query

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

Serialize filters back to strings

$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>2020

Custom tokenization

use 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());

Running Tests

composer tests

Tests are written with Pest PHP.

License

MIT

Credits

Implemented by 👾 Ivan Voskoboinyk.