SkillAgentSearch skills...

Doctrine Fulltext Search

Smart Doctrine search engine with ranking system.

Install / Use

npx skills add baraja-core/doctrine-fulltext-search

Installs into whichever agent you are using.

README

<div align='center'> <picture> <source media='(prefers-color-scheme: dark)' srcset='https://cdn.brj.app/images/brj-logo/logo-regular.png'> <img src='https://cdn.brj.app/images/brj-logo/logo-dark.png' alt='BRJ logo'> </picture> <br> <a href="https://brj.app">BRJ organisation</a> </div> <hr>

Doctrine Fulltext Search

Integrity check

A powerful, easy-to-use fulltext search engine for Doctrine entities with automatic relevance scoring, query normalization, and machine learning-powered suggestions.

  • Define entity and column mappings with simple configuration
  • Automatic relevance scoring and result sorting
  • Built-in "Did you mean?" suggestions using analytics
  • Query normalization with stopword filtering
  • Support for entity relationships and custom getters
  • Nette Framework integration via DIC extension

🎯 Core Principles

  • Zero Configuration Start: Define your entity map and start searching immediately
  • Intelligent Scoring: Results are automatically scored and sorted by relevance (0-512 points)
  • Query Normalization: Automatic stopword removal, duplicate filtering, and query sanitization
  • Relationship Support: Search across related entities using dot notation
  • Analytics-Powered: Machine learning suggestions based on search history
  • Extensible Architecture: Override query normalizer and score calculator via interfaces
  • Performance Optimized: PARTIAL selection for efficient database queries with configurable timeout

🏗️ Architecture Overview

The package follows a modular architecture with clear separation of concerns:

┌─────────────────────────────────────────────────────────────────────────┐
│                              Search                                      │
│                         (Main Entry Point)                               │
└─────────────────────────────────────────────────────────────────────────┘
                                    │
                    ┌───────────────┼───────────────┐
                    ▼               ▼               ▼
         ┌──────────────┐  ┌───────────────┐  ┌──────────────┐
         │   Container  │  │SelectorBuilder│  │EntityMapNorm.│
         │  (Services)  │  │ (Fluent API)  │  │ (Validation) │
         └──────────────┘  └───────────────┘  └──────────────┘
                 │
    ┌────────────┼────────────┬──────────────┐
    ▼            ▼            ▼              ▼
┌────────┐ ┌──────────┐ ┌───────────┐ ┌───────────┐
│  Core  │ │Analytics │ │  Query    │ │  Score    │
│(Search)│ │(Did you  │ │Normalizer │ │Calculator │
│        │ │  mean?)  │ │           │ │           │
└────────┘ └──────────┘ └───────────┘ └───────────┘
    │
    ▼
┌──────────────┐
│ QueryBuilder │
│   (DQL)      │
└──────────────┘
    │
    ▼
┌─────────────────────────────────────────────────────────────────────────┐
│                          SearchResult                                    │
│              (Contains SearchItem[] with scoring)                        │
└─────────────────────────────────────────────────────────────────────────┘

🔧 Main Components

| Component | Purpose | |-----------|---------| | Search | Main entry point, orchestrates the search process | | SelectorBuilder | Fluent API for building search queries with type validation | | Container | Service container holding all dependencies (PSR-11 compatible) | | Core | Internal search logic, processes candidate results | | QueryBuilder | Builds DQL queries with JOIN support for relations | | Analytics | Stores search statistics, powers "Did you mean?" feature | | QueryNormalizer | Normalizes queries, removes stopwords | | ScoreCalculator | Calculates relevance scores with year boost | | SearchResult | Collection of results implementing Iterator | | SearchItem | Single search result with entity, title, snippet, and score |


📦 Installation

It's best to use Composer for installation, and you can also find the package on Packagist and GitHub.

To install, simply use the command:

$ composer require baraja-core/doctrine-fulltext-search

Requirements

  • PHP 8.0 or higher
  • ext-mbstring
  • Doctrine ORM 2.9+

Nette Framework Integration

Register the DIC extension in your NEON configuration:

extensions:
    doctrineFulltextSearch: Baraja\Search\DoctrineFulltextSearchExtension

The extension automatically registers:

  • Search service
  • QueryNormalizer service
  • ScoreCalculator service
  • SearchAccessor accessor
  • QueryBuilder service

Manual Instantiation

You can create an instance of Search manually:

use Baraja\Search\Search;
use Doctrine\ORM\EntityManagerInterface;

$search = new Search($entityManager);

With custom normalizer and score calculator:

$search = new Search(
    em: $entityManager,
    queryNormalizer: new CustomQueryNormalizer(),
    scoreCalculator: new CustomScoreCalculator(),
);

🚀 Basic Usage

Simple Array-Based Query

The simplest way to perform a search is by defining an entity map:

$results = $search->search($query, [
    Article::class => [':title', 'description', 'content'],
    User::class => ':username',
    Product::class => [':name', 'sku', '!internalCode'],
]);

echo $results; // Uses built-in HTML renderer

Fluent SelectorBuilder API

For better type safety and IDE autocompletion, use the SelectorBuilder:

$results = $search->selectorBuilder($query)
    ->addEntity(Article::class)
        ->addColumnTitle('title')
        ->addColumn('description')
        ->addColumn('content')
    ->addEntity(User::class)
        ->addColumnTitle('username')
        ->addEntity(Product::class)
        ->addColumnTitle('name')
        ->addColumn('sku')
        ->addColumnSearchOnly('internalCode')
    ->search();

Adding WHERE Conditions

Filter results with custom conditions:

$results = $search->selectorBuilder($query)
    ->addEntity(Article::class)
        ->addColumnTitle('title')
        ->addColumn('content')
    ->addWhere('active = TRUE')
    ->addWhere('publishedAt <= NOW()')
    ->search();

🛠️ Column Modifiers

Column names support special prefixes that control how they're used in search:

| Modifier | Syntax | Description | |----------|--------|-------------| | Title | :column | Used as result caption, displayed even without match | | Search Only | !column | Searched but excluded from snippet output | | Select Only | _column | Loaded but not searched or included in snippet | | Normal | column | Searched and included in snippet |

Examples

$entityMap = [
    Article::class => [
        ':title',           // Title column - always shown
        'description',      // Normal - searched and in snippet
        '!slug',            // Search only - searched but not in snippet
        '_authorId',        // Select only - loaded but not searched
    ],
];

Using SelectorBuilder:

$search->selectorBuilder($query)
    ->addEntity(Article::class)
        ->addColumnTitle('title')           // :title
        ->addColumn('description')          // description
        ->addColumnSearchOnly('slug')       // !slug
        ->addColumnSelectOnly('authorId')   // _authorId
    ->search();

🔗 Entity Relationships

Search across related entities using dot notation:

$entityMap = [
    Article::class => [
        ':title',
        'author.name',           // ManyToOne: Article -> Author
        'categories.name',       // ManyToMany: Article -> Categories
        'content.versions.text', // Deep relation chain
    ],
];

Custom Getters

When the getter method differs from the column name:

$entityMap = [
    Article::class => [
        'versions(content)', // Joins 'versions' but calls getContent()
    ],
];

🔍 Advanced Query Features

Exact Match

Wrap phrases in quotes for exact matching:

$query = '"to be or not to be"';
// Finds exact phrase

Negative Match

Exclude words with minus prefix:

$query = 'linux -ubuntu';
// Finds "linux" but excludes results containing "ubuntu"

Number Intervals

Search for number ranges:

$query = 'conference 2020..2024';
// Finds results containing years 2020, 2021, 2022, 2023, or 2024

📊 Working with Results

SearchResult Entity

The search() method returns a SearchResult entity implementing Iterator:

$results = $search->search($query, $entityMap);

// Total count
$count = $results->getCountResults();

// Search time in milliseconds
$time = $results->getSearchTime();

// "Did you mean?" suggestion
$suggestion = $results->getDidYouMean();

// Iterate results
foreach ($results as $item) {
    echo $item->getTitle();
}

Getting Results

// Get first 10 results
$items = $results->getItems();

// With pagination
$items = $results->getItems(limit: 20, offset: 40);

// Filter by entity type
$articles = $results->getItemsOfType(Article::class, limit: 10);

// Get only IDs
$ids = $results->getIds(limit: 100);

SearchItem Methods

Each result is a SearchItem with these methods:

| Method | Return Type | Description | |--------|-------------|-------------| | getId() | string\|int | Entity identifier | | getEntity() | object | Original Doctrine entity (PARTIAL loaded) | | getTitle() | ?string | Normalized title | | getTitleHighlighted() | ?string | Title with <i class="highlight"> tags | | getSnippet() | string | Best matching text snippet | | getSnippetHighlighted() | string | Snippet with highlighted words | | getScore() | int | Relevance score (0-512) | | `ent

Related Skills

View on GitHub
GitHub Stars20
CategoryData
Updated5mo ago
Forks3

Languages

PHP

Security Score

92/100

Audited on Feb 23, 2026

No findings