handcraftedinthealps/zendsearch
Laravel-friendly integration of ZendSearch/Lucene for fast full‑text search in your app. Index Eloquent models, run queries with relevance scoring, and manage indexing via simple services/commands—ideal for lightweight search without external engines.
Installation
composer require handcraftedinthealps/zendsearch
Add to composer.json if not auto-discovered:
"autoload": {
"psr-4": {
"App\\": "app/",
"ZendSearch\\": "vendor/handcraftedinthealps/zendsearch/"
}
}
Run composer dump-autoload.
First Use Case: Basic Indexing
use ZendSearch\Lucene\Document\Field\Field;
use ZendSearch\Lucene\Document\Document as LuceneDocument;
use ZendSearch\Lucene\Document\DocumentStore\Filesystem as DocumentStore;
// Create a document store (default: ./data/zendsearch)
$store = new DocumentStore();
$index = $store->open('my_index');
// Add a document
$doc = new LuceneDocument();
$doc->addField(Field::Text('title', 'Laravel Assessment'));
$doc->addField(Field::UnStored('content', 'This is a test document...'));
$index->addDocument($doc);
$index->commit();
First Query
$query = new \ZendSearch\Lucene\Query\Query\Query();
$query->addTerm('Laravel', 'title');
$hits = $index->find($query);
foreach ($hits as $hit) {
echo $hit->title . "\n";
}
vendor/handcraftedinthealps/zendsearch/src/ for core classes.config/zendsearch.php (if provided) or create your own config file.Indexing Workflow
DocumentStore to manage multiple indexes.
$store = new DocumentStore();
$index = $store->open('products');
foreach ($products as $product) {
$doc = new LuceneDocument();
$doc->addField(Field::Text('name', $product['name']));
$doc->addField(Field::UnStored('description', $product['desc']));
$index->addDocument($doc);
}
$index->commit(); // Critical for persistence
addDocument() for new/updated docs and removeDocument() for deletions.Querying Workflow
$query = new \ZendSearch\Lucene\Query\Query\Query();
$query->addTerm('php', 'tags');
$query->addTerm('laravel', 'tags');
$query->setMinTermFrequency(1); // Require both terms
$hits = $index->find($query, 0, 10); // Offset, limit
Laravel Service Provider Create a provider to bind the index store as a singleton:
// app/Providers/ZendSearchServiceProvider.php
public function register()
{
$this->app->singleton('zendsearch.store', function () {
return new DocumentStore();
});
}
Then inject it into controllers/services:
public function __construct(DocumentStore $store) {
$this->store = $store;
}
// app/Console/Commands/IndexProducts.php
public function handle()
{
$index = $this->store->open('products');
$index->deleteAll(); // Clear old data
// ... add documents ...
$index->commit();
}
$cacheKey = 'search_results_' . md5($query->__toString());
$results = Cache::remember($cacheKey, now()->addHours(1), function () use ($index, $query) {
return $index->find($query);
});
Case Sensitivity
Field::Text() with Field::STORE_NO for case-insensitive searches:
$doc->addField(Field::Text('title', 'Laravel', Field::STORE_NO, Field::INDEX_ANALYZED));
Memory Leaks
$index->commit() after bulk operations to flush documents to disk.$hits collections in memory for long periods.Concurrent Writes
if (file_put_contents($index->getPath() . '.lock', '1', LOCK_EX)) {
// Safe to write
file_put_contents($index->getPath() . '.lock', '', LOCK_EX);
}
Field Limits
Field::UnStored() or chunk the content.if (!$store->indexExists('my_index')) {
throw new \RuntimeException('Index does not exist!');
}
$count = $index->count();
./data/zendsearch/my_index)../data/zendsearch/ by default. Override with:
$store = new DocumentStore('./custom/path');
use ZendSearch\Lucene\Analysis\Analyzer\Analyzer;
use ZendSearch\Lucene\Analysis\Analyzer\Standard\StandardAnalyzer;
$analyzer = new StandardAnalyzer();
$field = Field::Text('content', $text, Field::STORE_NO, Field::INDEX_ANALYZED, $analyzer);
Custom Field Types
Extend ZendSearch\Lucene\Document\Field\AbstractField for domain-specific fields (e.g., Field::Date()).
Query Parsers
Use ZendSearch\Lucene\QueryParser\QueryParser for complex queries:
$parser = new QueryParser('title');
$query = $parser->parse('laravel OR php');
Laravel Eloquent Integration Create a trait to auto-index models:
trait Searchable {
public static function bootSearchable()
{
static::saved(function ($model) {
$index = app('zendsearch.store')->open('models');
$doc = new LuceneDocument();
$doc->addField(Field::Text('class', get_class($model)));
$doc->addField(Field::Text('id', $model->id));
// ... other fields ...
$index->addDocument($doc);
$index->commit();
});
}
}
High Availability For production, consider:
How can I help you explore Laravel packages today?