Sphinx Behavior

By Vilen (xumix)
This behavior helps to use Sphinx search engine in your projects.
First, you need Sphinx http://sphinxsearch.com/ installed and configured. I hope that you've already set up.
Now, get the sphinxapi.php from the sphinx distribution and place it in app/vendors.
Download the code and save it to app/models/behaviors/sphinx.php

Behavior code:

Download code
<?php
/**
 * Behavior for simple usage of Sphinx search engine
 * http://www.sphinxsearch.com
 *
 * @copyright 2008, Vilen Tambovtsev
 * @author  Vilen Tambovtsev
 * @license      http://www.opensource.org/licenses/mit-license.php The MIT License
 */


class SphinxBehavior extends ModelBehavior
{
    
/**
     * Used for runtime configuration of model
     */
    
var $runtime = array();
    var 
$_defaults = array('server' => 'localhost''port' => 3312);

    
/**
     * Spinx client object
     *
     * @var SphinxClient
     */
    
var $sphinx null;

    function 
setup(&$model$config = array())
    {
        
$settings array_merge($this->_defaults, (array)$config);

        
$this->settings[$model->alias] = $settings;

        
App::import('Vendor''sphinxapi');
        
$this->runtime[$model->alias]['sphinx'] = new SphinxClient();
        
$this->runtime[$model->alias]['sphinx']->SetServer($this->settings[$model->alias]['server'],
                                                           
$this->settings[$model->alias]['port']);
    }

    
/**
     * beforeFind Callback
     *
     * @param array $query
     * @return array Modified query
     * @access public
     */
    
function beforeFind(&$model$query)
    {
        if (empty(
$query['sphinx']) || empty($query['search']))
            return 
true;

        if (
$model->findQueryType == 'count')
        {
            
$model->recursive = -1;
            
$query['limit'] = 1;
            
$query['page'] = 1;
        }
        else if (empty(
$query['limit']))
        {
            
$query['limit'] = 9999999;
            
$query['page'] = 1;
        }

        foreach (
$query['sphinx'] as $key => $setting)
        {

            switch (
$key)
            {
                case 
'filter':
                    foreach (
$setting as $arg)
                    {
                        
$arg[2] = empty($arg[2]) ? false $arg[2];
                        
$this->runtime[$model->alias]['sphinx']->SetFilter($arg[0], (array)$arg[1], $arg[2]);
                    }
                   break;
                case 
'filterRange':
                case 
'filterFloatRange':
                    
$method 'Set' $key;
                    foreach (
$setting as $arg)
                    {
                        
$arg[3] = empty($arg[3]) ? false $arg[3];
                        
$this->runtime[$model->alias]['sphinx']->{$method}($arg[0], (array)$arg[1], $arg[2], $arg[3]);
                    }
                   break;
                case 
'matchMode':
                   
$this->runtime[$model->alias]['sphinx']->SetMatchMode($setting);
                   break;
                case 
'sortMode':
                    
$this->runtime[$model->alias]['sphinx']->SetSortMode(key($setting), reset($setting));
                    break;
                default:
                    break;
            }
        }
        
$this->runtime[$model->alias]['sphinx']->SetLimits(($query['page'] - 1) * $query['limit'],
                                                           
$query['limit']);

        
$indexes = !empty($query['sphinx']['index']) ? implode(',' $query['sphinx']['index']) : '*';

        
$result $this->runtime[$model->alias]['sphinx']->Query($query['search'], $indexes);

        if (
$result === false)
        {
            
trigger_error("Search query failed: " $this->runtime[$model->alias]['sphinx']->GetLastError());
            return 
false;
        }
        else if(isset(
$result['matches']))
        {
            if (
$this->runtime[$model->alias]['sphinx']->GetLastWarning())
            {
                
trigger_error("Search query warning: " $this->runtime[$model->alias]['sphinx']->GetLastWarning());
            }
        }

        unset(
$query['conditions']);
        unset(
$query['order']);
        unset(
$query['offset']);
        
$query['page'] = 1;
        if (
$model->findQueryType == 'count')
        {
            
$result['total'] = !empty($result['total']) ? $result['total'] : 0;
            
$query['fields'] = 'ABS(' $result['total'] . ') AS count';

        }
        else
        {
            if (isset(
$result['matches']))
                
$ids array_keys($result['matches']);
            else
                
$ids = array(0);
            
$query['conditions'] = array($model->alias '.'.$model->primaryKey => $ids);
            
$query['order'] = 'FIND_IN_SET('.$model->alias.'.'.$model->primaryKey.', \'' implode(','$ids) . '\')';

        }

        return 
$query;
    }
}
?>


Usage:

Model Class:

Download code <?php 
class Film extends AppModel {
var 
$actsAs = array('Sphinx');
}
?>


Controller Class:

Download code <?php 
class FilmsController extends AppController
{
function 
index()
{
    
$sphinx = array('matchMode' => SPH_MATCH_ALL'sortMode' => array(SPH_SORT_EXTENDED => '@relevance DESC'));
    
$results $this->Film->find('all', array('search' => 'search string here''sphinx' => $sphinx));
}


function 
paging()
{
        
$pagination = array('Film' => array('contain' =>
                                       array(
'FilmType',
                                             
'Genre',
                                             
'FilmPicture' => array('conditions' => array('type' => 'smallposter')),
                                             
'Country',
                                             
'Person' => array('conditions' => array('FilmsPerson.profession_id' => array(134))),
                                             
'MediaRating'),
                                        
'order' => array('Film.modified' => 'desc'),
                                        
'conditions' => array('Film.active' => 1),
                                        
'limit' => 30));
        
$pagination['Film']['fields'] = array('Film.id''Film.imdb_rating''Film.title',
                                              
'Film.year''MediaRating.rating');


        
$pagination['Film']['sphinx']['filter'][] = array('country_id'$this->params['named']['country']);
        if (!empty(
$this->params['named']['search']))
        {
            
$search trim($this->params['named']['search']);

            
$sort ', modified DESC';
            if (!empty(
$this->params['named']['sort']))
            {
                
$sort explode('.'$this->params['named']['sort']);
                
$sort ', ' $sort[1] . ' DESC';
            }

            
$pagination['Film']['sphinx']['matchMode'] = SPH_MATCH_ALL;
            
$pagination['Film']['sphinx']['sortMode'] = array(SPH_SORT_EXTENDED => '@relevance DESC' $sort);

            
$pagination['Film']['search'] = $search;
        }
        
$this->paginate $pagination;
        
$films $this->paginate();

}

}
?>

 

Comments 822

CakePHP Team Comments Author Comments
 

Comment

1 Thanks

Thanks for this. Was just about to dive into sphinx, and this will certainly save me a lot of time.
Posted Nov 19, 2008 by Thomas Pedersen
 

Comment

2 update

added more examples and updated code
Posted Dec 3, 2008 by Vilen
 

Comment

3 Thanks

Thanks for sharing this. It's exactly what I'm looking for. I'm excited to try it out.
Posted Jul 9, 2009 by Jon Chin
 

Question

4 Possible to use find() without specifying a limit?

I think I have this behavior and Sphinx installed correctly. However, when I first executed the code similar to what you had for the index action, I got two assertion errors (lines 390 and 392 of sphinxapi.php). I remedied this by adding a 'limit' parameter to my find(). Do you know if there's a way to use the find() method to retrieve ALL the results?

Here's my code if it helps:

Controller Class:

<?php 
$sphinx 
= array('matchMode' => 'SPH_MATCH_ALL''sortMode' => array('SPH_SORT_EXTENDED' => '@relevance DESC'));
$results $this->Resume->find('all', array('search' => 'of''limit'=>10'sphinx' => $sphinx));?>
Posted Jul 10, 2009 by Jon Chin
 

Comment

5 smth like this:)

I think I have this behavior and Sphinx installed correctly. However, when I first executed the code similar to what you had for the index action, I got two assertion errors (lines 390 and 392 of sphinxapi.php). I remedied this by adding a 'limit' parameter to my find(). Do you know if there's a way to use the find() method to retrieve ALL the results?

limit => 100000000 ;)
Posted Jul 10, 2009 by Vilen
 

Comment

6 try this piece of code:

Replace if ($model->findQueryType == 'count') .......  with

        if ($model->findQueryType == 'count')
        {
            $model->recursive = -1;
            $query['limit'] = 1;
            $query['page'] = 1;
        }
        else if (empty($query['limit']))
        {
            $query['limit'] = 9999999;
        $query['page'] = 1;
        }
Posted Jul 10, 2009 by Vilen
 

Question

7 Another name for field "id"?

Hi there,
first of all:
Thank you very much for this manual!!!!
Since I'm using UUIDs (name:id) for my tables, I inserted an additional field "aiid" (auto-increment id) for sphinx search.
Searching via console works perfectly.
Searching via Cake does not work.
Do I have to define that the Sphinx within Cake should use another table field than "id"?
Best!
stebu
Posted Jul 13, 2009 by stebu
 

Comment

8 got it

i just had to add
var $primaryKey = 'aiid'; to my models.

Hi there,
first of all:
Thank you very much for this manual!!!!
Since I'm using UUIDs (name:id) for my tables, I inserted an additional field "aiid" (auto-increment id) for sphinx search.
Searching via console works perfectly.
Searching via Cake does not work.
Do I have to define that the Sphinx within Cake should use another table field than "id"?
Best!
stebu
Posted Jul 13, 2009 by stebu
 

Question

9 Problems with Sphinx Filter

Thank you for the tutorial, it was great to me, but I have got any problems with the filters.
The sphinx was implemented in my project but I can't to use the filters like the example in url http://planetcakephp.org/aggregator/items/1319-cakephp-sphinx-applying-filters-to-your-search
I debugging the behaviors and the sphinx api but I didn't find nothing. The array structure that I use is
'filter' => array( array('price >','300'))
I prove too:
'filter' => array( array('price','300')) - without '>'
If anybody can help me, I great them very much.

Thank you

-------------UPDATED-----------------
I can use Sphinx in my project. The problem was a configuration in sphinx because I didn't add the parameter in the file sphinx.conf.
It's neccesary to add "sql_attr_uint with = " the name of the integer variable .

Posted Sep 3, 2009 by Jorge De Bernardo
 

Comment

10 I have solved my problem

I have a similar problem and had to do a research and saw this post. Sphinx is actually a lifesaver for me. I impressed my boss at pet meds using your code. Thank you so much.
Posted Oct 12, 2009 by Leah Guz
 

Comment

11 Wonderful Thank you

This is a fantastic addition to the Bakery, thank you.
How does this interact with the Model Caching?
Trying this out on Training-Classes.com
Posted Nov 12, 2009 by Training Guy
 

Comment

12 Pagination

Is seriously mucky on that - is there no way to shorten it? Also - what about re-indexing? is this done by Sphinx or does Cake Handle it?

Implementing this on Web Design Birmingham
Posted Dec 1, 2009 by Web Design Birmingham
 

Comment

13 "Field Weight" ability

Insert

case 'fieldWeights':
   $this->runtime[$model->alias]['sphinx']->SetFieldWeights($setting);
   break; 
between lines 93 and 94 of sphinx.php behavior.

Usage within the controller (assuming your table owns fields 'title' and 'tags'):

$pagination['Article']['sphinx']['matchMode'] = SPH_MATCH_EXTENDED;
$pagination['Article']['sphinx']['sortMode'] = array (SPH_SORT_EXTENDED=>'@relevance DESC');
$pagination['Article']['sphinx']['fieldWeights'] = array ('title'=>5, 'tags'=>50);

Posted Dec 15, 2009 by stebu
 

Comment

14 re

Is seriously mucky on that - is there no way to shorten it? Also - what about re-indexing? is this done by Sphinx or does Cake Handle it?

Implementing this on Web Design Birmingham
your pagination code might be as short as first example with find
Posted Dec 15, 2009 by Vilen
 

Comment

15 view file sample

i am having trouble to get the search to work in cake. sphinx on the server works fine but i dont understand how to use the above code with a search form. sorry about this newbie question. it would be great if someone could post a view on how to use the sphinx behavior.

thanks
Posted Dec 15, 2009 by janrupp
 

Comment

16 setFilterRange Question

I set up the sphinx behaviors with sphinx API some months ago and have had no problems -- this is great, and I really appreciate the effort! The application I'm developing requires some flexible date searching, so I have date fields in my table and index, in unix timestamp format. My controller sets variables $min and $max and does this call (the date table is named 'dateopened'):

Controller Class:

<?php 
$sphinx 
= array('matchMode' => SPH_MATCH_EXTENDED'sortMode' => array( $sortmode => $sorttype), 'filterRange' => array('1' => 'dateopened''2' => $min'3' => $maxFALSE));

    
$this->set('results'$this->Search->find('all', array('search' => $phrase.$openSearch'sphinx' => $sphinx)));
?>

Using the original code in the behavior, this didn't work at all -- $arg[1] would be 'd', $arg[2] 'a', and so on, breaking the first parameter letter by letter. I'm having a hard time seeing how any of the foreach statements in there could work, since they seem to be doing a full array operation on individual elements. So I rewrote it like this:


case 'filterRange':
// case 'filterFloatRange':
    $method = 'Set' . $key;
    $i=1;
    foreach ($setting as $arg)
    {
    $para[$i] = $arg;
    echo $arg."!<br />"; //this works, shows me what I would expect
    $i++;
    }
    if (!empty($para[3])) $this->runtime[$model->alias]['sphinx']->SetfilterRange($para[1], $para[2], $para[3], $para[4]);
    break;

The result is that my date range is ignored in the results. I'm using Sphinx 9.8.1 on this server, so it could be that SetFilterRange just doesn't work with that release. Or I might need something in my sphinx.conf file that I don't have -- I'm a bit unclear on that. But I'm certain (having debugged it) that the routine in the behavior is running, and I'm fairly certain that it's outputting a proper API call to SetFilterRange. Any idea why that call wouldn't be honored?

Thanks!
Posted Dec 29, 2009 by Peter Campbell
 

Comment

17 Solved my own problem

Never mind! I didn't have the attribute specified properly in sphinx.conf. It's working great now!
Posted Dec 29, 2009 by Peter Campbell