Doctrine Fulltext Search
Smart Doctrine search engine with ranking system.
Install / Use
npx skills add baraja-core/doctrine-fulltext-searchInstalls into whichever agent you are using.
README
Doctrine Fulltext Search
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:
SearchserviceQueryNormalizerserviceScoreCalculatorserviceSearchAccessoraccessorQueryBuilderservice
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
codebase-memory-mcp
38.0kHigh-performance code intelligence MCP server. Indexes codebases into a persistent knowledge graph — average repo in milliseconds. 158 languages, sub-ms queries, 99% fewer tokens. Single static binary, zero dependencies.
codebase-memory-mcp
38.0kHigh-performance code intelligence MCP server. Indexes codebases into a persistent knowledge graph — average repo in milliseconds. 158 languages, sub-ms queries, 99% fewer tokens. Single static binary, zero dependencies.
codebase-memory-mcp
38.1kHigh-performance code intelligence MCP server. Indexes codebases into a persistent knowledge graph — average repo in milliseconds. 158 languages, sub-ms queries, 99% fewer tokens. Single static binary, zero dependencies.
tabularis
4.0kOpen-source desktop SQL workspace for PostgreSQL, MySQL/MariaDB, SQLite and 15+ more databases like DuckDB, ClickHouse, Redis and Firestore. Built-in MCP server for Claude, Cursor and Devin, SQL notebooks and visual EXPLAIN.
