PHPackages                             baraja-core/doctrine-fulltext-search - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Database &amp; ORM](/categories/database)
4. /
5. baraja-core/doctrine-fulltext-search

ActiveLibrary[Database &amp; ORM](/categories/database)

baraja-core/doctrine-fulltext-search
====================================

Entity search engine, extremely easy to use.

v3.2.7(1y ago)2070.6k3[4 issues](https://github.com/baraja-core/doctrine-fulltext-search/issues)3PHPPHP ^8.0CI failing

Since Dec 18Pushed 4mo ago2 watchersCompare

[ Source](https://github.com/baraja-core/doctrine-fulltext-search)[ Packagist](https://packagist.org/packages/baraja-core/doctrine-fulltext-search)[ Docs](https://github.com/baraja-core/doctrine-fulltext-search)[ RSS](/packages/baraja-core-doctrine-fulltext-search/feed)WikiDiscussions master Synced 1mo ago

READMEChangelog (10)Dependencies (15)Versions (49)Used By (3)

   ![BRJ logo](https://camo.githubusercontent.com/813c67e02a7ab7e4dc900316a4521c3ddf5846fe2cabba7140f3f4b78afda198/68747470733a2f2f63646e2e62726a2e6170702f696d616765732f62726a2d6c6f676f2f6c6f676f2d6461726b2e706e67)
 [BRJ organisation](https://brj.app)

---

Doctrine Fulltext Search
========================

[](#doctrine-fulltext-search)

[![Integrity check](https://github.com/baraja-core/doctrine-fulltext-search/workflows/Integrity%20check/badge.svg)](https://github.com/baraja-core/doctrine-fulltext-search/workflows/Integrity%20check/badge.svg)

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

[](#-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
------------------------

[](#️-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

[](#-main-components)

ComponentPurpose**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
--------------

[](#-installation)

It's best to use [Composer](https://getcomposer.org) for installation, and you can also find the package on [Packagist](https://packagist.org/packages/baraja-core/doctrine-fulltext-search) and [GitHub](https://github.com/baraja-core/doctrine-fulltext-search).

To install, simply use the command:

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

### Requirements

[](#requirements)

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

### Nette Framework Integration

[](#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

[](#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
-------------

[](#-basic-usage)

### Simple Array-Based Query

[](#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

[](#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

[](#adding-where-conditions)

Filter results with custom conditions:

```
$results = $search->selectorBuilder($query)
    ->addEntity(Article::class)
        ->addColumnTitle('title')
        ->addColumn('content')
    ->addWhere('active = TRUE')
    ->addWhere('publishedAt  [
        ':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
----------------------

[](#-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

[](#custom-getters)

When the getter method differs from the column name:

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

---

🔍 Advanced Query Features
-------------------------

[](#-advanced-query-features)

### Exact Match

[](#exact-match)

Wrap phrases in quotes for exact matching:

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

### Negative Match

[](#negative-match)

Exclude words with minus prefix:

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

### Number Intervals

[](#number-intervals)

Search for number ranges:

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

---

📊 Working with Results
----------------------

[](#-working-with-results)

### SearchResult Entity

[](#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

[](#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

[](#searchitem-methods)

Each result is a `SearchItem` with these methods:

MethodReturn TypeDescription`getId()``string|int`Entity identifier`getEntity()``object`Original Doctrine entity (PARTIAL loaded)`getTitle()``?string`Normalized title`getTitleHighlighted()``?string`Title with `` tags`getSnippet()``string`Best matching text snippet`getSnippetHighlighted()``string`Snippet with highlighted words`getScore()``int`Relevance score (0-512)`entityToArray()``array`Entity as normalized array### Quick HTML Rendering

[](#quick-html-rendering)

For rapid prototyping, `SearchResult` implements `__toString()`:

```
echo $results;
```

This outputs styled HTML with:

- Result count and search time
- "Did you mean?" suggestion (if available)
- Results with highlighted titles and snippets

Add `?debugMode=1` to URL to see scores in output.

---

✅ "Did You Mean?" Feature
-------------------------

[](#-did-you-mean-feature)

When search returns few or no results, the engine can suggest alternative queries:

```
$results = $search->search('programing', $entityMap);

if ($results->getCountResults() === 0) {
    $suggestion = $results->getDidYouMean();
    if ($suggestion !== null) {
        echo "Did you mean: $suggestion?"; // "programming"
    }
}
```

### How It Works

[](#how-it-works)

1. Every search query and result count is stored in the `search__search_query` table
2. Queries are scored based on frequency and result count
3. When needed, the system finds similar queries using Levenshtein distance
4. The best match is suggested based on combined scoring

Disable analytics for specific searches:

```
$results = $search->search($query, $entityMap, useAnalytics: false);

// Or with SelectorBuilder
$results = $search->selectorBuilder($query)
    ->addEntity(Article::class)
        ->addColumnTitle('title')
    ->search(useAnalytics: false);
```

---

📈 Scoring System
----------------

[](#-scoring-system)

Results are scored on a scale of 0-512 points based on multiple factors:

### Score Calculation

[](#score-calculation)

FactorPointsDescriptionExact match+32Haystack equals query exactlyContains query+4Query found as substringSubstring count+1-3Bonus per occurrence (max 3)Word match+1-4Per word occurrence (max 4)Empty content-16Penalty for empty fieldsSearch-only column-4Reduced weight for `!` columnsTitle columnx6-10Multiplier for `:` columnsYear boostx1-6Bonus for current/recent years### Year Boost

[](#year-boost)

The score calculator automatically boosts results containing recent years:

- Current year and adjacent years receive higher scores
- Particularly relevant for news, events, and time-sensitive content

### Custom Score Calculator

[](#custom-score-calculator)

Implement `IScoreCalculator` for custom scoring:

```
use Baraja\Search\ScoreCalculator\IScoreCalculator;

class CustomScoreCalculator implements IScoreCalculator
{
    public function process(string $haystack, string $query, string $mode = null): int
    {
        // Your custom scoring logic
        return $score;
    }
}
```

Register in Nette DI:

```
services:
    - CustomScoreCalculator
```

The container will automatically use your implementation.

---

🔄 Query Normalization
---------------------

[](#-query-normalization)

Queries are automatically normalized before processing:

### Default Normalizer Features

[](#default-normalizer-features)

1. **Whitespace normalization**: Multiple spaces reduced to single
2. **Length limit**: Truncated to 255 characters
3. **Stopword removal**: Common words filtered (in, it, a, the, of, or, etc.)
4. **Duplicate removal**: Repeated words kept only once
5. **Special character handling**: `%`, `_`, `{`, `}` converted or removed
6. **Hash removal**: `#123` becomes `123`

### Custom Query Normalizer

[](#custom-query-normalizer)

Implement `IQueryNormalizer` for project-specific normalization:

```
use Baraja\Search\QueryNormalizer\IQueryNormalizer;

class CustomQueryNormalizer implements IQueryNormalizer
{
    public function normalize(string $query): string
    {
        // Your normalization logic
        return $normalizedQuery;
    }
}
```

---

⚙️ Configuration Options
------------------------

[](#️-configuration-options)

### Search Timeout

[](#search-timeout)

Configure maximum search time (default: 2500ms):

```
$container = new Container(
    entityManager: $em,
    searchTimeout: 5000, // 5 seconds
);

$search = new Search($em, container: $container);
```

### Exact Search Mode

[](#exact-search-mode)

Disable "Did you mean?" suggestions:

```
$results = $search->search(
    query: $query,
    entityMap: $entityMap,
    searchExactly: true,
);
```

### User Conditions

[](#user-conditions)

Add WHERE conditions to all entity queries:

```
$results = $search->search(
    query: $query,
    entityMap: $entityMap,
    userConditions: [
        'e.active = TRUE',
        'e.deletedAt IS NULL',
    ],
);
```

---

📝 Database Entity
-----------------

[](#-database-entity)

The package creates one database table for analytics:

### SearchQuery Entity

[](#searchquery-entity)

Table: `search__search_query`

ColumnTypeDescriptionidUUIDPrimary keyquerystringNormalized search query (unique)frequencyintNumber of times searchedresultsintLast result countscoreintCalculated relevance (0-100)insertedDatedatetimeFirst search timeupdatedDatedatetimeLast search timeThe table is automatically created when using Doctrine migrations with the package's entity mappings.

---

🎨 Styling Highlighted Results
-----------------------------

[](#-styling-highlighted-results)

The default highlighter wraps matched words in:

```
matched word
```

Add CSS for styling:

```
.highlight {
    background: rgba(68, 134, 255, 0.35);
}

.search__info {
    padding: .5em 0;
    margin-bottom: .5em;
    border-bottom: 1px solid #eee;
}

.search__did_you_mean {
    color: #ff421e;
}
```

### Custom Highlight Pattern

[](#custom-highlight-pattern)

Use `Helpers::highlightFoundWords()` with custom pattern:

```
use Baraja\Search\Helpers;

$highlighted = Helpers::highlightFoundWords(
    haystack: $text,
    words: $query,
    replacePattern: '\0',
);
```

---

🌍 Internationalization
----------------------

[](#-internationalization)

The search engine handles accented characters intelligently:

- **ASCII conversion**: Queries are converted for matching (`café` matches `cafe`)
- **Accent-aware highlighting**: Original text preserved with proper highlighting
- **Character mapping**: Supports Czech, Slovak, Polish, and other Central European languages

Supported character mappings:

- `a` matches `á`, `ä`
- `c` matches `č`
- `e` matches `è`, `ê`, `é`, `ě`
- `n` matches `ň`
- `r` matches `ř`, `ŕ`
- `s` matches `š`, `ś`
- `z` matches `ž`, `ź`
- And more...

---

🔧 Troubleshooting
-----------------

[](#-troubleshooting)

### Column Not Found

[](#column-not-found)

```
InvalidArgumentException: Column "title" is not valid property of "App\Entity\Article".
Did you mean "headline"?

```

The package validates column names against entity metadata. Check your entity properties or use the suggested alternative.

### Empty Results

[](#empty-results)

1. Verify entity has data in the database
2. Check if columns contain searchable text
3. Try disabling query normalization for debugging
4. Verify WHERE conditions aren't too restrictive

### Performance Issues

[](#performance-issues)

1. Add database indexes on searched columns
2. Reduce the number of entities/columns in search
3. Lower the search timeout
4. Use `!` modifier for large text columns
5. Consider `_` modifier for columns only needed in results

---

👤 Author
--------

[](#-author)

**Jan Barášek**

- Website:
- GitHub: [@janbarasek](https://github.com/janbarasek)

---

📄 License
---------

[](#-license)

`baraja-core/doctrine-fulltext-search` is licensed under the MIT license. See the [LICENSE](https://github.com/baraja-core/doctrine-fulltext-search/blob/master/LICENSE) file for more details.

###  Health Score

49

—

FairBetter than 95% of packages

Maintenance53

Moderate activity, may be stable

Popularity36

Limited adoption so far

Community18

Small or concentrated contributor base

Maturity75

Established project with proven stability

 Bus Factor1

Top contributor holds 97.6% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~38 days

Recently: every ~213 days

Total

44

Last Release

703d ago

Major Versions

v1.1.0 → v2.0.02020-03-24

v2.2.2 → v3.0.02021-02-09

PHP version history (3 changes)v1.0.0PHP &gt;=7.1.0

v2.1.0PHP &gt;=7.4.0

v3.0.0PHP ^8.0

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/3382204?v=4)[baraja](/maintainers/baraja)[@baraja](https://github.com/baraja)

---

Top Contributors

[![janbarasek](https://avatars.githubusercontent.com/u/4738758?v=4)](https://github.com/janbarasek "janbarasek (162 commits)")[![dependabot[bot]](https://avatars.githubusercontent.com/in/29110?v=4)](https://github.com/dependabot[bot] "dependabot[bot] (2 commits)")[![dependabot-preview[bot]](https://avatars.githubusercontent.com/in/2141?v=4)](https://github.com/dependabot-preview[bot] "dependabot-preview[bot] (2 commits)")

---

Tags

algorithmcandidatescaptiondatabase-searchdic-containerdid-you-meandoctrinedoctrine-fulltext-searchdoctrine-searchentityfulltextfulltext-searchmysql-searchoverriddenperexphp-searchphp-search-enginesearchsearch-enginesmart

###  Code Quality

Static AnalysisPHPStan

Type Coverage Yes

### Embed Badge

![Health badge](/badges/baraja-core-doctrine-fulltext-search/health.svg)

```
[![Health](https://phpackages.com/badges/baraja-core-doctrine-fulltext-search/health.svg)](https://phpackages.com/packages/baraja-core-doctrine-fulltext-search)
```

###  Alternatives

[laravel/framework

The Laravel Framework.

34.6k509.9M17.0k](/packages/laravel-framework)[sylius/sylius

E-Commerce platform for PHP, based on Symfony framework.

8.4k5.6M651](/packages/sylius-sylius)[hautelook/alice-bundle

Symfony bundle to manage fixtures with Alice and Faker.

19519.4M34](/packages/hautelook-alice-bundle)[kimai/kimai

Kimai - Time Tracking

4.6k7.4k1](/packages/kimai-kimai)[forkcms/forkcms

Fork is an open source CMS that will rock your world.

1.2k44.5k](/packages/forkcms-forkcms)[neos/flow

Flow Application Framework

862.0M451](/packages/neos-flow)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
