chez14 / lucene-ast
Zero-runtime Lucene syntax parser to AST.
Requires
- php: ^8.4
Requires (Dev)
- chez14/phpcs: ^1.0.5
- friendsofphp/php-cs-fixer: ^3.95
- phpunit/phpunit: ^12.5 || ^13.4
- squizlabs/php_codesniffer: ^3.13
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-04 10:51:48 UTC
README
Parse Lucene-style query strings into a typed AST. Zero dependencies.
Installation
composer require chez14/lucene-ast
Usage
use CHEZ14\LuceneAst\Parser;
use CHEZ14\LuceneAst\Printer;
$ast = (new Parser())->parse('title:"hello world" AND -(status:draft OR status:archived) date:[2020 TO *}');
parse() returns a tree of immutable nodes (null for an empty query):
Or
├─ And
│ ├─ Field(title) → Phrase("hello world")
│ └─ Not
│ └─ Or
│ ├─ Field(status) → Term("draft")
│ └─ Field(status) → Term("archived")
└─ Field(date) → Range(lower: "2020", upper: null, includeLower: true, includeUpper: false)
Every node is JsonSerializable:
echo json_encode($ast);
// {"type":"or","children":[{"type":"and","children":[{"type":"field","field":"title","value":{"type":"phrase","value":"hello world"}},{"type":"not","child":{"type":"or","children":[...]}}]},{"type":"field","field":"date","value":{"type":"range","lower":"2020","upper":null,"includeLower":true,"includeUpper":false}}]}
Clauses without an operator (a b) are joined with an OR by default. To join them with AND instead:
use CHEZ14\LuceneAst\BooleanOperator;
$parser = new Parser(defaultOperator: BooleanOperator::And);
Printer turns an AST back into a canonical query string. Parsing the output gives the same AST:
echo (new Printer())->print($ast);
// (title:"hello world" AND -(status:draft OR status:archived)) OR date:[2020 TO *}
Supported syntax
| Syntax | Meaning | AST |
|---|---|---|
hello | term | TermNode('hello') |
"hello world" | phrase | PhraseNode('hello world') |
hello\ world, chez\:14 | term with escaped characters | TermNode('hello world'), TermNode('chez:14') |
foo*, te?t | wildcard | WildcardNode('foo*') |
/jo(h)?n/ | regular expression | RegexpNode('jo(h)?n') |
[a TO b], {a TO b}, [a TO b} | range: [ ] inclusive, { } exclusive | RangeNode('a', 'b', true, true) |
[* TO 100] | open-ended range | RangeNode(null, '100', true, true) |
title:hello | field | FieldNode('title', TermNode('hello')) |
title:(a OR b) | field applied to a group | FieldNode('title', OrNode(a, b)) |
a AND b, a && b | conjunction | AndNode(a, b) |
a OR b, a \|\| b | disjunction | OrNode(a, b) |
a b | implicit operator | OrNode(a, b) by default, AndNode(a, b) with defaultOperator: And |
-a, NOT a, !a | negation | NotNode(a) |
(a OR b) AND c | grouping | AndNode(OrNode(a, b), c): parentheses shape the tree but leave no node |
The full grammar, escaping rules and error list are in docs/syntax.md.
Differences from Lucene
- Boolean precedence. Lucene's classic parser has no precedence: it flags each clause as optional, required or prohibited. Here the operators are plain boolean logic,
NOTbinds tighter thanAND, andANDbinds tighter thanOR. Soa b -cisa OR b OR NOT c(ora AND b AND NOT cwithdefaultOperator: And), not Lucene's "(a or b) and not c". See ADR 0001. +is rejected.+a +bwould silently turn into an OR of a and b, so it throws instead. Writea AND b, or escape the character (\+a).^(boost) and~(fuzzy) are rejected too. See ADR 0002.
Security
The parser is hardened against hostile input: lexing and parsing are linear, nesting is capped by maxDepth (default 64), and error messages never contain query text. What you do with the AST is up to you:
- Cap the query length before parsing. Memory grows linearly with the input.
- Whitelist field names. Never put them into SQL or column names.
- Bind values as parameters.
- Never pass
RegexpNode::$patternorWildcardNode::$patternstraight topreg_*orLIKE. They are Lucene syntax, not PCRE or SQL, and user-controlled patterns can cause catastrophic backtracking. - Keep
maxDepthmodest. Each level costs about five PHP frames; raising it far above the default can hit Xdebug'sxdebug.max_nesting_level(512 by default) or PHP's own stack limit.
More detail in docs/syntax.md.
Errors
Syntax errors throw CHEZ14\LuceneAst\Exception\ParseException (an InvalidArgumentException) with a reason and the byte position of the problem:
use CHEZ14\LuceneAst\Exception\ParseException;
try {
(new Parser())->parse('title:(foo OR');
} catch (ParseException $e) {
echo $e->reason; // Unexpected end of query
echo $e->position; // 13
}
Development
composer install
composer test # PHPUnit
composer lint # PHP-CS-Fixer (check) then phpcs
composer lint:fix # PHP-CS-Fixer, apply formatting fixes
composer check # lint + test, same as CI
Formatting is done by PHP-CS-Fixer (@PER-CS);
phpcs enforces the
chez14/phpcs rules, including documented functions and no ternary
expressions.
License
Released under the MIT License.