ciricihq/match-against-bundle
Installation
composer require ciricihq/match-against-bundle
Add the bundle to config/bundles.php:
return [
// ...
Cirici\MatchAgainstBundle\CiriciMatchAgainstBundle::class => ['all' => true],
];
Configure Database
Ensure your MySQL tables have FULLTEXT indexes on the fields you want to search. Example schema:
ALTER TABLE your_table ADD FULLTEXT(content);
First Query
Use the SearchTextIndex entity to query against indexed fields:
use Cirici\MatchAgainstBundle\Entity\SearchTextIndex;
$qb = $entityManager->getConnection()->createQueryBuilder();
$qb->select('sti.foreignId')
->from(SearchTextIndex::class, 'sti')
->where('sti.model = :entityClass')
->andWhere('sti.field = :fieldName')
->andWhere("MATCH_AGAINST(sti.content, :text 'IN BOOLEAN MODE') > :score")
->setParameter('entityClass', 'App\Entity\YourEntity')
->setParameter('fieldName', 'title')
->setParameter('text', 'search term')
->setParameter('score', 0.1);
Pre-indexing
Use a Command or EventListener to populate SearchTextIndex for new/updated entities:
// Example: Doctrine Lifecycle Event Listener
$entityManager->getEventManager()->addEventListener(
[OnFlush::class, PostFlush::class],
new SearchIndexUpdater($entityManager)
);
Query Patterns
+term1 -term2).
->andWhere("MATCH_AGAINST(sti.content, :text 'IN BOOLEAN MODE') > 0.5")
->andWhere("MATCH_AGAINST(sti.content, :text 'IN NATURAL LANGUAGE MODE') > 0.3")
->andWhere('sti.model IN (:models)')
->setParameter('models', ['App\Entity\Post', 'App\Entity\Page']);
Pagination
Use LIMIT and OFFSET with ORDER BY for relevance:
->orderBy('MATCH_AGAINST(sti.content, :text) DESC')
->setMaxResults(10)
->setFirstResult(0);
Dynamic Field Searching Build a flexible search across multiple fields:
$fields = ['title', 'content', 'tags'];
$orConditions = [];
foreach ($fields as $field) {
$orConditions[] = "MATCH_AGAINST(sti.content, :text 'IN BOOLEAN MODE') > 0.1 AND sti.field = :field";
}
$qb->andWhere(implode(' OR ', $orConditions))
->setParameter('field', $fields);
FULLTEXT Index Requirements
FULLTEXT indexes require a minimum word length (default: 4 characters). Configure in my.cnf:
ft_min_word_len=3
CHAR, VARCHAR, or TEXT types.Case Sensitivity
FULLTEXT searches are case-insensitive by default, but accents may not match. Use BINARY for exact matches:
MATCH_AGAINST(BINARY sti.content, :text) > 0
Performance
UNION).Boolean Mode Quirks
+ (required) or - (excluded) in boolean mode:
->setParameter('text', '+laravel -framework')
*) are inefficient; avoid leading wildcards (e.g., *term).Scoring
> 0.1 vs. > 0.5).Raw SQL
Use getSQL() to inspect queries:
$sql = $qb->getSQL();
$params = $qb->getParameters();
EXPLAIN Analyze query performance:
EXPLAIN SELECT * FROM search_text_index WHERE MATCH_AGAINST(content, 'term') > 0;
Log Queries
Enable Doctrine logging in config/packages/doctrine.yaml:
doctrine:
dbal:
logging: true
profiling: true
Custom Indexing
Extend SearchTextIndex or create a proxy entity to handle complex mappings:
class CustomSearchIndex extends SearchTextIndex {
public function getSearchContent(): string {
return $this->title . ' ' . $this->description;
}
}
Query Builder Helper Create a service to wrap repetitive logic:
class MatchAgainstQueryBuilder {
public function search(string $text, string $entityClass, array $fields, float $score): QueryBuilder {
// ... implementation
}
}
Event-Driven Indexing Use Symfony events to trigger indexing on entity updates:
$dispatcher->addListener(
KernelEvents::VIEW,
[SearchIndexUpdater::class, 'updateIndex']
);
Synonyms/Stemming Configure MySQL for better search behavior:
-- Enable stopword removal (e.g., 'the', 'and')
SET SESSION ft_stopword_file = '/path/to/stopwords.txt';
How can I help you explore Laravel packages today?