Adding search to Yii blog example (using Zend Lucene)

  1. Introduction
  2. Requirements and Preparation
  3. Step 1 - adding the search form
  4. Creating SearchController
  5. Creating Search Index
  6. Doing the search
  7. Creating the view
  8. Conclusion
  9. Additional Links
  10. Bonus: Pagination with CPagination
  11. Another pagination solution

Introduction

For this tutorial I will add a search to the blog demo. The search would be based on Zend Lucene.

Requirements and Preparation

  • Yii framework - I will work with the current version yii-1.1.8.r3324 extract the archive in your server...
  • Now go to your yii-1.1.8.r3324\demos\blog\protected directory and create there a vendors directory In vendors directory you should put Zend Framework library, For this you will need to download it After downloading copy the Zend directory that inside library (with Zend directory inside) to vendors Should look like this now (I copied only Search directory...):

  • add under runtime directory, a new one call it search, it will be used for the index files that Zend Lucene will create. Make sure it is writable!

Step 1 - adding the search form

For this we will create a widget or if to be more specific a CPortlet! The most simple way is go to your components directory, create SearchBlock.php file And copy inside code from tag cloud. change class name to SearchBlock. Now lets insert it ! Go to views/layouts/column2.php add this, abouve the TagCloud widget

<?php
      $this->widget('SearchBlock', array(
      )); 
?>

after saving you will see 2 tag coulds in your main blog page... lets now edit it and change it to some search form

Now lets change the SearchBlog widget code...

<?php

Yii::import('zii.widgets.CPortlet');

class SearchBlock extends CPortlet
{
	public $title='Search';

	protected function renderContent()
	{
	       echo CHtml::beginForm(array('search/search'), 'get', array('style'=> 'inline')) .
		CHtml::textField('q', '', array('placeholder'=> 'search...','style'=>'width:140px;')) .
		CHtml::submitButton('Go!',array('style'=>'width:30px;')) .
		CHtml::endForm('');
	}
}

It will look like this:

Creating SearchController

Now lets add a SearchController

<?php
class SearchController extends Controller
{
	/**
	 * @var string index dir as alias path from <b>application.</b>  , default to <b>runtime.search</b>
	 */
	private $_indexFiles = 'runtime.search';
	/**
	 * (non-PHPdoc)
	 * @see CController::init()
	 */
	public function init(){
		Yii::import('application.vendors.*');
		require_once('Zend/Search/Lucene.php');
		parent::init();	
	}
	
    public function actionCreate()
    {
    }
    
    public function actionSearch()
    {
    }
}

As you can see in the init method we import Zend from vendors and we require the Search Lucene

It has two actions Search and Create For this tutorial I don't need more... but you can add update for example to update the search index ( of course you should implement it) etc.

It is a time to mention the documentation for Zend Lucene Here To do more advanced stuff, you will need to learn it

Also the api for the Lucene class

Creating Search Index

This is an example of blog array after AR findAll

'id' => '2'
'title' => 'A Test Post'
'content' => 'a lot of text'
'tags' => 'test'
'status' => '2'
'create_time' => '1230952187'
'update_time' => '1230952187'
'author_id' => '1'

This is how we create the index:

/**
	 * Search index creation
	 */
    public function actionCreate()
    {
    	$index = new Zend_Search_Lucene(Yii::getPathOfAlias('application.' . $this->_indexFiles), true);
    	
    	$posts = Post::model()->findAll();
    	foreach($posts as $post){
	    	$doc = new Zend_Search_Lucene_Document();
	    	
	    	$doc->addField(Zend_Search_Lucene_Field::Text('title',
                                          CHtml::encode($post->title), 'utf-8')
            );
            
            $doc->addField(Zend_Search_Lucene_Field::Text('link',
                                         	CHtml::encode($post->url)
												, 'utf-8')
			);   
			
			$doc->addField(Zend_Search_Lucene_Field::Text('content',
                                          CHtml::encode($post->content)
                                          , 'utf-8')
            );
           
			
			$index->addDocument($doc);
    	}
	    $index->commit();
	   	echo 'Lucene index created';
    }

First I found all the posts with Post::model()->findAll(); and than I added post to search index one by one, his content title and link fields

Now you should navigate to http://localhost/yii-1.1.8.r3324/demos/blog/index.php/search/create And you will see "Lucene index created"

Doing the search

public function actionSearch()
    {
    	$this->layout='column2';
   		 if (($term = Yii::app()->getRequest()->getParam('q', null)) !== null) {
	        $index = new Zend_Search_Lucene(Yii::getPathOfAlias('application.' . $this->_indexFiles));
	        $results = $index->find($term);
	        $query = Zend_Search_Lucene_Search_QueryParser::parse($term);       

	        $this->render('search', compact('results', 'term', 'query'));
	    }
    }

Creating the view

Create views/search/search.php add there

<?php
$this->pageTitle=Yii::app()->name . ' - Search results';
$this->breadcrumbs=array(
	'Search Results',
);
?>

<h3>Search Results for: "<?php echo CHtml::encode($term); ?>"</h3>
<?php if (!empty($results)): ?>
			    <?php foreach($results as $result): 
?>					
					<p>Title: <?php echo $query->highlightMatches(CHtml::encode($result->title)); ?></p>
			        <p>Link: <?php echo CHtml::link($query->highlightMatches(CHtml::encode($result->link)), CHtml::encode($result->link)); ?></p>
			        <p>Content: <?php echo $query->highlightMatches(CHtml::encode($result->content)); ?></p>
			        <hr/>
			    <?php endforeach; ?>
			    
			<?php else: ?>
			    <p class="error">No results matched your search terms.</p>
			<?php endif; ?>

As you can see because I passed to the view the $query, I can use Zend's highlightMatches ...
(if your search results not in english, you might have some encoding issues with highlightMatches, so you might consider using your own highlighter for greater flexibility and encoding issues free)
Also I created a real link from the linked I passed and created via $doc->addField(Zend_Search_Lucene_Field::Text('link' ...

And thats it try to search the word "it" And you will get:

Conclusion

Now we are done, we have our search working... As you noticed I don't explained a lot about Zend Lucene itself... So you should look at the links I gave, and try to learn about it more!

Additional Links

  1. larry ulman's artice about Zend Lucene
  2. Zend Lucene documentation
  3. Good article 1
  4. Good article 2
  5. Good articles 3

Bonus: Pagination with CPagination

As some one here added in the comment, pagination can be a little tricky... You need to do some calculations by yourself...

Actually it is always a dilemma if to give you find the trick yourself, or give it to you directly...

There is a very big danger if you always get the answers for your problem from others... because in real time job, no one will do the job for you...

So this is how it would look with CPagination, after you figure out how to do the pagination of the result yourself:

(The page limit is 3 per page)

So dont continue reading, until you spend some time thinking about it and trying your self!

Add to search

$pages = new CPagination(count($results));
$currentPage = Yii::app()->getRequest()->getQuery('page', 1);
$pages->pageSize = 3;

In the view change the foreach view with for

<?php for($i = $currentPage * $pages->pageSize - $pages->pageSize, $end = $currentPage * $pages->pageSize; $i<$end;$i++):
?>					
					<p>Title: <?php echo $query->highlightMatches(CHtml::encode($results[$i]->title)); ?></p>
			        <p>Link: <?php echo CHtml::link($query->highlightMatches(CHtml::encode($results[$i]->link)), CHtml::encode($results[$i]->link)); ?></p>
			        <p>Content: <?php echo $query->highlightMatches(CHtml::encode($results[$i]->content)); ?></p>
			        <hr/>
			    <?php endfor; ?>

and add after this

<?php $this->widget('CLinkPager', array(
    'pages' => $pages,
)) ?>

It is not perfect, but it is the start. The problems you should think about are: what to do if some one tries to access page number 40 ? now it will throw error 500

What will happen if you have 2 pages, but only 4 records? You will get undefind ofsset errors...

Consider this as homework ;-)

p.s. don't forget to add some blog posts or you will have nothing to paginate ;-)

Another pagination solution

Actually if you want it pass between pages via ajax, and don't handle the offset issues etc. ( it is easy but... we still most likly want ajax)

So you can consider doing the pagination via CArrayDataProvider You have there example http://www.yiiframework.com/doc/api/1.1/CArrayDataProvider Just pass the result array than you can create CListView with some basic view for search result element, and you are done... CArrayDataProvider will do all the dirty work for you!

29 1
47 followers
Viewed: 88 476 times
Version: 1.1
Category: Tutorials
Written by: dckurushin
Last updated by: klammeraffe
Created on: Oct 7, 2011
Last updated: 11 years ago
Update Article

Revisions

View all history

Related Articles