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: 9

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

0.1.0 2026-07-24 20:02 UTC

This package is auto-updated.

Last update: 2026-07-24 20:15:18 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);
  • ! 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. A hyphen behaves differently — some-one is an ordinary word;
  • 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. 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.