CakePHP Searchable Plugin
A flexible and full featured CakePHP plugin for quickly adding site wide, multi-model search functionality to your application.
At work we recently developed an application for a client that required site-wide search functionality, and provided a single results set from multiple models/sources. Normally I’d use the CakePHP Yahoo BOSS site search I wrote and blogged about previously, but this particular app requires users to login to access any content, so Yahoo wouldn’t be able to index any of the content.
I had a hunt around to see if there was anything already out there that would fit the requirements and I found this http://code.google.com/p/searchable-behaviour-for-cakephp/ which is a behavior that stores the data from multiple models in a single search_index table and performs mysql full text search on that, but it didn’t quite have all the features I needed such as scope for search results, i.e. you set the status of a record to in-active and the corresponding record in the search_index table goes in-active. I did, however, like this approach.
So, inspired by the above solution, I’ve written a plugin that you can add to your app and integrate site wide search functionality in a matter of minutes.
The code is available on my github account. Note, it relies on MySQL Full Text Search, but you could replace this with your own search algorithm or alternative RDBMS equivalent.
The plugin includes:
- A Searchable Behavior to attach to models in your app that automatically maintains a record in the search_index table for each record in the model you attach it to.
- A shell script to build/re-build the search_index table for all/some models that have the Searchable Behavior attached
- Model, View and Controller for the search_index table that handles performing the search, and displaying the results.
To get it up and running:
- Get the code from github
- Run the SQL in searchable/config/sql/search_index.sql
- Attach the Searchable Behavior to the models in your app that you want to search, e.g.
var $actsAs => array('Searchable.Searchable'); - Run the build_search_index shell, e.g.
$> cake build_search_index - Add the searchable/config/routes.php file to you app/config/routes.php
// app/config/routes.php
include(APP.'plugins'.DS.'searchable'.DS.'config'.DS.'routes.php'); - Add the search form element to a page in your site, or in the default.ctp layout file e.g.
echo $this->element('form', array('plugin' => 'searchable'));
Now type something in the search box and go.
Some additional features/notes:
- You’ll notice on the search results page you can restrict your search to a single model.
- Search results are paginated.
- Search terms are added to the URL so you can deep link to search results
- The search supports MySQL Full Text Search in boolean mode, so you can do things like searching for phrases using quotes and excluding words using the minus sign
- The search_index table has a scope field which is a boolean (tinyint 1) and is set to 1 by default, but if you specify some normal cakephp conditions in the scope setting when you attach the Searchable Behavior, this will be set depending on whether these conditions are met for that particular record. E.g.var $actsAs = array(‘Searchable.Searchable’ => array(’scope’ => array(‘Post.active’ => 1)));
- Data from your model is stored in the ‘data’ field of the search_index table and is json_encode’d. This is to circumvent one of the issues of the Searchable behavior I found earlier that someone noted in the issues list – if you call saveField, only that field’s data got saved in the search_index table. With this behavior, when editing a record, if not all fields are present in the data you are saving, the existing content of the data field is merged with the new data you are saving, so you don’t lose any data that you had previously.
- By default, all string type fields are included in the json_encode’d data field, but you can override this if necessary using the ‘fields’ setting when you attach the behavior. E.g.var $actsAs = array(‘Searchable.Searchable’ => array(‘fields’ => array(‘title’, ‘abstract’, ‘body’, ‘published‘)));
- Sometimes it’s useful to be able to search for associated data as well, e.g. the name of the Category that a Post belongsTo, to achieve this you can do the following:var $actsAs = array(‘Searchable.Searchable’ => array(‘fields’ => array(‘title’, ‘abstract’, ‘body’, ‘category_id‘ => ‘Category.name’)));I.e. the foreign key field in the searchable model => the model.field you want to fetch the value from.
- The search_index table also includes fields for ‘name’ and ’summary’, you can configure which fields in your model are used to populate these fields in the search_index table in the settings array too. What goes in here are what’s displayed in the search results.
- If your data uses a published date field (or equivalent) to determine whether content should be displayed or not, as an alternative or in addition to scope, the search_index table also has a published field, and again you can configure which field in your model should map through to it. The search results are scope to only display records whose published field is null (which it will be by default if you have no published data), or the published date is in the past – but you can configure this as required by your app. For example on another app I’ve used this on I changed these conditions to published in the past, but not more than 6 months ago, or if logged in (i.e. an administrator), display future content as well so they can preview stuff.
- By default, the search result will link through to the controller for the model of that search result, it’s view action, and pass the id of the record as a parameter. You can configure this to some extent at the moment, e.g. if your model is actually in another plugin, you can add this to the settings, but that’s about it at the moment, so no slugs or anything like that. If you need to configure the url formats, suggest you just amend the views/search_indexes/index.ctp view file to your requirements.
Enjoy ;-)


(5 votes, average: 4.60 out of 5)
31 Responses so far
November 21st, 2009
12:43 pm
Wow, great plugin. I will be implementing this into one of my current projects. Thanks.
November 22nd, 2009
1:32 am
Nice plugin – I’m already using it (slightly modified).
November 26th, 2009
7:54 pm
Very Cool Plugin! Many thanks.
December 1st, 2009
2:44 pm
Hi, I followed your tutorial but I get these errors as soon as I load a page:
Warning (2): include(/Sites/cake/app/plugins/searchable/config/routes.php) [function.include]: failed to open stream: No such file or directory [APP/config/routes.php, line 44]
Warning (2): include() [function.include]: Failed opening ‘Sites/cake/app/plugins/searchable/config/routes.php’ for inclusion (include_path=’/Sites/cake:/Users/ahandley/Sites/cake/app/:.:/Applications/MAMP/bin/php5/lib/php’) [APP/config/routes.php, line 44]
thanks
December 1st, 2009
9:26 pm
Either the path to the routes.php file inside app/plugins/searchable/config/routes.php is incorrect or you have not put the file there.
December 11th, 2009
4:41 pm
Great plugin.
Is it possible to search for associated data if it is a HABTM?
December 11th, 2009
5:52 pm
This looks like a killer plugin, however I’m getting errors when trying to run the shell…
“Warning: SQL Error: 1064: You h
ave an error in your SQL syntax; check the manual that corresponds to your MySQL
server version for the right syntax to use near ‘deleteSearchIndex’ at line 1 in D:\Server\cake\cake\libs\model\datasources\dbo_source.php on line 524
Query: deleteSearchIndex Erro
r: Could not delete search index”
Any ideas?
December 11th, 2009
9:38 pm
@Jasmin, not at the moment, but that would be a good enhancement, feel free to fork the code and have a go, and let me know how you get on.
@unidev, the shell deletes all the records in the search index in the db for the given model before rebuilding them. It does this by calling deleteSearchIndex() on the model. That method is on the SearchableBehavior, but cake will automatically pass it off to the Behavior if the method doesn’t exist in the model but does exist in one of the behaviors that are attached to the model. However, if cake isn’t aware that that method is available directly on the model, or in any behavior that is attached to it, it assumes it is a stored procedure in the db and tries to execute on the db directly – which is what is happening here. What you need to do is figure out why cake does not know that the method is available on the behavior that is attached to your model – sorry, I’m not sure.
December 22nd, 2009
1:46 am
Hi Neil,
Thanks for making this – seems similar to the site search by Kalt but with the shell rebuild features which should come in handy.
Q: Have you had any thoughts on the best way to lock the search down with permissions? i.e. restrict results depending on who is searching?
Cheers in advance and have a good chrimbo.
December 22nd, 2009
9:35 am
@unclezoot, just add logic in the controller action to set conditions based on the logged in user.
December 25th, 2009
8:30 pm
First of all merry xmas and thanks for this great plugin!
I have the same problem:
Error: Could not delete search index
I’m using ACL but even when inactive the error is displayed. Must be something else?
I’ve got this in the model:
‘Searchable.Searchable’ => array(‘fields’ => array(‘title’, ‘description’, ‘company_id’ => ‘Company.name’)),
Would be great if you know of anything else I could check.
Merry Xmas!
December 25th, 2009
8:35 pm
Btw, if I remove deleteSearchIndex from the shell script it works.
December 26th, 2009
4:58 pm
@Nader, if you add deleteSearchIndex back in and run it again, do you get the error? I wonder if I added that line after I ran it for the first time?
December 27th, 2009
4:36 pm
@Neil, when I put the function in again, it works now. Maybe it has got something todo with an initial empty table or weird database issue?
Using # Server version: 5.0.41
# Protocol version: 10
# Server: Localhost via UNIX socket
with MAMP on MacOS X Leopard
January 3rd, 2010
1:14 pm
Hi Neil again,
any hint on where and how I would implement AJAX Search with jQuery and your Plugin?
January 4th, 2010
10:00 pm
@Nader, sorry, not my area of expertise, but check out the cook book for ajax form submissions and using the RequestHandler component for handling ajax requests.
January 6th, 2010
6:37 pm
Hey, thanks for the great plugin.
I’m trying to fill in the summary field from a field in my model, but I can’t quite figure out which options to pass to do that.
January 6th, 2010
6:47 pm
Nevermind, I got it.
Of course I totally missed it in the comments until just after I posted here. Go Murphy.
January 8th, 2010
6:37 pm
protected function _setUrl seems incomplete. It doesn’t handle non standard URL components and mappings to dynamic fields (ie. slug).
This is what I patched:
http://bin.cakephp.org/view/1213709512
January 13th, 2010
10:53 pm
[...] Neil Crookes’ Searchable plugin also helped. [...]
January 15th, 2010
7:46 pm
Hi Neil,
Thanks for this great plugin!
I’m having a problem but I don’t know why could it be. The thing is when I perform any search the query returns every single record of the search_index table, even those which don’t include in the field “data” anything about the word I searched.
Could you help me?
Thanks!
January 15th, 2010
9:30 pm
@Adam, good work. You’re right, that was unfinished. I’ll take a look at the patch and add it. Thanks.
@Jorge, hmm interesting. Can’t think of any reason why that would be, check the SQL debug queries and copy the one containing MATCH…AGAINST, then run it in your SQL editor and see if you get the same results. If you do, sounds like it could be your MySQL not working correctly, maybe?
January 18th, 2010
8:36 pm
Hi Neil,
Finally I reinstalled the plugin and now is working perfectly!
Thank you so much for your work!
January 19th, 2010
12:02 am
Hi again Niel,
I cannot figure out how to do to customize the name and summary field when you said “The search_index table also includes fields for ‘name’ and ’summary’, you can configure which fields in your model are used to populate these fields in the search_index table in the settings array too”.
How and where do I need to write something like ‘name’ => ‘myFieldName’?
I tried putting that in the settings array in diferents ways but never worked.
Thank you very much!
January 19th, 2010
12:22 am
Hi Jorge,
Should be as simple as:
var $actsAs = array(‘Searchable.Searchable’ => array(’name’ => ‘my_field_name’));
Does this not work for you?
January 19th, 2010
1:21 am
Hi Neil,
Nop, doesn’t work for me.
I tried something like this:
var $actsAs = array(‘Searchable.Searchable’ => array(‘fields’ => array(‘email’, ‘email1′, ‘direccion’), ‘name’ => ‘nombre’));
Then I re run the build_search_index and nothing changes in the name field.
Thanks!
January 22nd, 2010
4:57 am
Hi Neil,
I’m having problems to run the shell to index. Copied the codes over to my app and once I ran the shell, prompted with
Fatal error: Class ‘SearchableAppModel’ not found in …… search_index.php on line 2
Any idea what is really happening? Thanks Neil
January 22nd, 2010
5:23 am
Sorry my bad. I’ve totally misintepreted…plugins… Well now it works
January 26th, 2010
4:41 pm
@ Jorge / Neil
Take a look at lines 100 – 103 in searchable.php – there’s a condition preventing the ‘name’ being set.
Thanks for the great plugin.
January 26th, 2010
9:05 pm
Hi Paul,
Thank you so much for your help!
January 27th, 2010
1:25 am
@Jorge sorry for not getting back to you.
@Paul, thanks for pointing out the bug, I’ll check it out and patch the repo on github when I get a sec. I’ll post back here when it’s done
Leave a comment