Installation:
composer require egeloen/lucene-search-bundle
Add the bundle to config/bundles.php:
return [
// ...
Ivory\SearchBundle\IvorySearchBundle::class => ['all' => true],
];
Configure Indexes:
Define a YAML config file (e.g., config/lucene_search.yml) to map Doctrine entities to Lucene indexes:
ivory_search:
indexers:
app_product:
type: ivory_search.indexer.doctrine.orm
model: App\Entity\Product
fields:
- { name: name, type: text }
- { name: description, type: text }
- { name: price, type: numeric }
First Use Case:
Inject the IvorySearchIndexManager service and index an entity:
use Ivory\SearchBundle\IndexManager;
public function __construct(private IndexManager $indexManager) {}
public function indexProduct(Product $product) {
$this->indexManager->index('app_product', $product);
}
Searching:
Use the IvorySearchQueryBuilder to query the index:
$queryBuilder = $this->indexManager->getQueryBuilder('app_product');
$results = $queryBuilder
->createQuery()
->where('name', 'apple')
->find();
Indexing Workflows:
IndexManager::indexAll() to reindex all entities in a model.prePersist, preUpdate) to auto-index entities:
$entityManager->getEventManager()->addEventListener(
Doctrine\ORM\Events::prePersist,
new IndexListener($indexManager, 'app_product')
);
Search Workflows:
QueryBuilder::setLimit() and setOffset() for paginated results.$queryBuilder->addFacet('category', 'category');
Hybrid Search: Combine Lucene results with Doctrine queries for precision:
$luceneResults = $queryBuilder->find();
$doctrineResults = $entityManager->createQueryBuilder()
->where('id IN (:ids)')
->setParameter('ids', array_column($luceneResults, 'id'))
->getQuery()
->getResult();
IvorySearchType for search form fields:
$builder->add('q', IvorySearchType::class, [
'index' => 'app_product',
'field' => 'name',
]);
return $this->json($queryBuilder->find());
$cache = $this->container->get('cache.app');
$cacheKey = 'lucene_search_results_' . md5($query);
$results = $cache->get($cacheKey, function() use ($queryBuilder) {
return $queryBuilder->find();
});
Index Schema Changes:
fields in lucene_search.yml requires reindexing the entire dataset. Use migrations or scripts to handle this gracefully.$indexManager = $this->container->get('ivory_search.index_manager');
$indexManager->indexAll('app_product');
Performance:
type: ignore for fields that don’t need searching.IndexManager::indexAll() with chunking:
$indexManager->indexAll('app_product', 100); // 100 entities per batch
Doctrine Sync Issues:
Lucene Version Mismatches:
composer.json and server environment match the expected Lucene version:
composer require zendframework/zendsearch-lucene:~1.11
Index Validation:
$index = $indexManager->getIndex('app_product');
dump($index->getDocumentCount()); // Should return > 0
foreach ($index->getDocuments() as $doc) {
dump($doc->getData());
}
Query Debugging:
export IVORY_SEARCH_DEBUG=1
QueryBuilder::getQuery() to inspect the raw Lucene query:
dump($queryBuilder->getQuery()->getQuery());
Common Errors:
lucene_search.yml matches the one used in code.fields match the entity property names exactly (case-sensitive).Custom Indexers:
Extend Ivory\SearchBundle\Indexer\IndexerInterface for non-Doctrine data sources (e.g., CSV files):
class CustomIndexer implements IndexerInterface {
public function index($id, $data) {
// Custom indexing logic
}
}
Register it in services.yaml:
services:
App\Indexer\CustomIndexer:
tags:
- { name: ivory_search.indexer, alias: 'custom_index' }
Custom Query Builders:
Extend Ivory\SearchBundle\QueryBuilder\QueryBuilder to add domain-specific methods:
class ProductQueryBuilder extends QueryBuilder {
public function priceRange($min, $max) {
$this->query->addTerm('price', "[$min TO $max]", 'range');
return $this;
}
}
Override the default query builder in services.yaml:
services:
App\QueryBuilder\ProductQueryBuilder:
arguments:
- '@ivory_search.index_manager'
- 'app_product'
tags:
- { name: ivory_search.query_builder, alias: 'app_product' }
Post-Processing Results:
Use Ivory\SearchBundle\Result\ResultInterface to transform results before returning them:
$results = $queryBuilder->find();
$transformed = array_map(function($result) {
return $this->productTransformer->transform($result->getData());
}, $results);
How can I help you explore Laravel packages today?