Skip to content

Filters ​

A filter maps an input property to a piece of query logic. You attach filters to a model with the repeatable #[Filter] attribute, and each one points to a filter instance that knows how to narrow a query and how much it contributes to the ranking score.

Declaring filters on a model ​

#[Filter] takes the input property name and a filter instance. It is repeatable, so a model can declare as many filters as it needs.

php
<?php
declare(strict_types=1);

namespace App\Model;

use Raxos\Database\Orm\Attribute\{Column, PrimaryKey, Table};
use Raxos\Database\Orm\Model;
use Raxos\Search\Attribute\Filter;
use Raxos\Search\Filter\{DateTime, Exact, NaturalText};

#[Table('articles')]
#[Filter('author_id', filter: new Exact(modelKey: 'author_id'))]
#[Filter('published', filter: new DateTime(modelKey: 'published_at'))]
#[Filter('q', filter: new NaturalText(keys: ['title', 'body']))]
final class Article extends Model
{
    #[PrimaryKey]
    #[Column]
    public int $id;

    #[Column]
    public string $title;

    #[Column]
    public string $body;
}

The property name is what a key:value query part or an HTTP parameter targets. The free text of a query is always assigned to the q property, so a #[Filter('q', ...)] acts as the model's main text search.

The filter contracts ​

Every filter implements FilterInterface, which exposes three read-only properties (modelClass, modelKey, weight) and one method:

php
public function apply(
    StructureInterface $structure,
    Filter $attribute,
    QueryInterface $query,
    QueryNodeInterface $searchQuery
): ScoreExpression;

apply() adds conditions to the query and returns a ScoreExpression describing this filter's contribution to the ranking. A filter resolves the target column from modelClass (defaulting to the model being searched) and modelKey (defaulting to the filter's property name).

A filter that also implements StructuredFilterInterface can be driven from plain request parameters and documented for OpenAPI:

php
public function fromInput(string $property, MapInterface $params): ?QueryNodeInterface;
public function describe(string $property): array;

fromInput() reads the value from a map of request parameters and returns a query node (or null when the filter has no value), which is then fed to the same apply(). describe() returns a framework-neutral shape (a name, a type, optionally an enum or format) used for documentation.

Built in filters ​

The package ships the following filter classes in Raxos\Search\Filter. See the filter class reference for constructor signatures.

FilterBehaviourStructured
BooleanMatches a column against a true/1 style boolean word.yes
DateTimeRange over a date or datetime column, read from _after and _before keys.yes
DefinedMatches on a column being null or not null.yes
EnumMatches a column against one case of a backed enum, validating the value.yes
EveryDeclared as an AND combinator, but apply() currently throws. Use Some.no
ExactMatches a column against an exact word value.yes
ExistsCorrelated EXISTS / NOT EXISTS against a related table.yes
NaturalTextMySQL MATCH ... AGAINST full text match over one or more columns.no
NumberMatches a single number or a numeric range, with a closeness score.no
SomeCombines several filters with OR semantics inside one parenthesis.yes
TextSubstring LIKE match on a column.no

Every is not implemented

Every is declared as an AND combinator, but its apply() throws a RuntimeException('Not implemented.'). To combine several filters, use Some, which is the supported OR combinator.

Combining filters with Some ​

Some runs several sub filters inside one parenthesis and joins their conditions with OR. Its score is the greatest of the sub scores.

php
use Raxos\Search\Attribute\Filter;
use Raxos\Search\Filter\{Some, Text};

#[Filter('q', filter: new Some([
    new Text(modelKey: 'title'),
    new Text(modelKey: 'body'),
]))]

A query part q:draft then matches rows whose title or body contains draft.

Custom filters ​

A custom filter narrows the node it receives, adds its own conditions and returns a score expression. Implementing StructuredFilterInterface in addition lets the same class serve request parameters.

php
<?php
declare(strict_types=1);

namespace App\Search;

use Raxos\Contract\Collection\MapInterface;
use Raxos\Contract\Database\Orm\StructureInterface;
use Raxos\Contract\Database\Query\QueryInterface;
use Raxos\Contract\Search\{FilterInterface, QueryNodeInterface, StructuredFilterInterface};
use Raxos\Database\Query\Literal\Literal;
use Raxos\Search\Attribute\Filter;
use Raxos\Search\Error\InvalidFilterValueException;
use Raxos\Search\Query\Token as T;
use Raxos\Search\ScoreExpression;

final readonly class ActiveFilter implements FilterInterface, StructuredFilterInterface
{
    public function __construct(
        public ?string $modelClass = null,
        public ?string $modelKey = null,
        public int $weight = 1
    ) {}

    public function apply(StructureInterface $structure, Filter $attribute, QueryInterface $query, QueryNodeInterface $searchQuery): ScoreExpression
    {
        if (!($searchQuery instanceof T\Word)) {
            throw new InvalidFilterValueException(self::class);
        }

        $modelClass = $this->modelClass ?? $structure->class;
        $modelKey = $this->modelKey ?? $attribute->property;

        $query->where($modelClass::col($modelKey), $searchQuery->text === 'true');

        return new ScoreExpression(Literal::of(0), weight: $this->weight);
    }

    public function fromInput(string $property, MapInterface $params): ?QueryNodeInterface
    {
        if (!$params->has($property)) {
            return null;
        }

        return new T\Word((string)$params->get($property));
    }

    public function describe(string $property): array
    {
        return [['name' => $property, 'type' => 'boolean']];
    }
}

When the node type a filter receives does not match what it expects, it throws InvalidFilterValueException, which extends the base exception from raxos/error.

Where to next ​

  • Scoring: how the score expression a filter returns is combined into the final ranking.
  • Policies: guarding a model before its filters run.