gam6itko/spiral-validator-manticore-search

Manticore Search full-text query syntax checker for spiral/validator

Maintainers

Package info

github.com/gam6itko/spiral-validator-manticore-search

pkg:composer/gam6itko/spiral-validator-manticore-search

Transparency log

Statistics

Installs: 78

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

0.2.0 2026-07-27 18:37 UTC

This package is auto-updated.

Last update: 2026-07-27 18:39:35 UTC


README

Manticore Search full-text query syntax checker for spiral/validator.

Validates the structure of a Manticore full-text query (extended query syntax) before it is stored or executed. Useful when a query is typed in by hand — by an administrator, a content editor, an API client — and executed later: a broken expression is rejected with a validation error on input instead of throwing a query error somewhere deep in the request that runs it.

The checker is a small hand-written lexer plus a recursive-descent parser. It needs no connection to a Manticore instance and performs no queries.

Requirements

Installation

composer require gam6itko/spiral-validator-manticore-search

Registration

Add the checker to the checkers section of app/config/validator.php:

<?php

declare(strict_types=1);

use Gam6itko\Spiral\Validator\ManticoreSearch\ManticoreSearchChecker;

return [
    'checkers' => [
        // ... the default spiral/validator checkers
        'manticore' => ManticoreSearchChecker::class,
    ],
];

Or register it from a bootloader:

use Gam6itko\Spiral\Validator\ManticoreSearch\ManticoreSearchChecker;
use Spiral\Boot\Bootloader\Bootloader;
use Spiral\Validator\Bootloader\ValidatorBootloader;

final class AppBootloader extends Bootloader
{
    public function init(ValidatorBootloader $validator): void
    {
        $validator->addChecker('manticore', ManticoreSearchChecker::class);
    }
}

Usage

The checker exposes a single rule — manticore::query. Its optional argument is the list of full-text fields the @ operator is allowed to reference:

use Spiral\Filters\Attribute\Input\Query;
use Spiral\Filters\Model\Filter;
use Spiral\Filters\Model\FilterDefinitionInterface;
use Spiral\Filters\Model\HasFilterDefinition;
use Spiral\Validator\FilterDefinition;

final class NewsSearchFilter extends Filter implements HasFilterDefinition
{
    #[Query]
    public string $search;

    public function filterDefinition(): FilterDefinitionInterface
    {
        return new FilterDefinition([
            'search' => [
                'string::trim',
                ['string::length', 256],
                // only `title` and `body` may be used with the `@` operator
                ['manticore::query', ['title', 'body']],
            ],
        ]);
    }
}

Without the field list only the syntax of @ is checked, not the field names:

['manticore::query'] // `@whatever word` passes, `@whatever` (no operand) does not

The rule is deliberately narrow: an empty string and non-string values are passed through, so combine it with notEmpty / string::* when those matter. The error message key is query, the default message is [[Invalid search query syntax.]].

The checker can also be used on its own, outside of the validator:

$checker = new ManticoreSearchChecker();
$checker->query('"bitcoin rate"~5 -scam', ['title', 'body']); // true
$checker->query('bitcoin | -scam', ['title', 'body']);        // false

What is validated

Structure only, never meaning:

  • quotes " and parentheses ( ) are balanced;
  • binary operators (|, <<, NEAR/N, MAYBE, SENTENCE, PARAGRAPH) have an operand on both sides, and neither operand consists of negations only — Manticore rejects a | -b, -a << b, a | (-b) as non-computable, but accepts a -b | c and a | (-b c);
  • unary NOT (- / !) has an operand, and the query is not made of negations only;
  • ~N and /N appear only right after a closing phrase quote, /N also as part of NEAR/N — anywhere else ~ and / are a syntax error for Manticore (price/rate, http://foo.bar, word~2, "a b"~5~5);
  • the numeric argument of ~N / /N ends the term: only whitespace or the start of another operator (|, !, (, ), ", @) may follow it, so "a b"~2 c and "a b"~2|c are fine while "a b"~2a, "a b"~2-c, "a b"~2.c and "a b"~2* are rejected;
  • ! is the NOT operator in any position, not just at the start of a term: word! and word!! are rejected (NOT without an operand), a!b reads as a without b;
  • - is the NOT operator unless the character before it is a letter or a digit, where it is a literal instead: some-one, e-mail and 2024-05- are ordinary words, a--b and a,-b read as a without b, while a--, 100$- and C++- are a NOT left without an operand — rejected;
  • the numeric argument of ~N / /N / NEAR/N / @field[N] is a positive integer;
  • @field / @(f1,f2) / @!field / @field[N] refers to a known field, when the field list is given;
  • a term made of positional markers only (^, $, =, *) does not count as a positive term: ^ is rejected, word ^ is accepted.

The reference implementation is Manticore's own parser: whatever the checker accepts, the daemon must be able to parse without an error. The rules were calibrated by running a corpus of queries through SELECT ... WHERE MATCH(?) against manticore 28.4.4; that corpus is the test suite.

In a few corner cases the checker is stricter than Manticore, where the daemon tolerates input that cannot match anything: an empty group (), a lone @, @field[abc], a dangling \ at the end of the string, a negation whose operand is a term made of separators only (a -.). Each of those is a plain typo, and a validation error is more useful than an empty result set. See provideStricterThanManticore() in the test suite for the full list.

What the checker does not do: it does not check that terms exist in the index, does not evaluate the query, and does not touch non-full-text parts of a SELECT (attribute filters, ORDER BY, OPTION) — it only looks at what goes inside MATCH().

Testing

composer install
composer tests
composer csfix

Links

License

MIT. See LICENSE.