Permalink
Browse files

Initial Commit

  • Loading branch information...
0 parents commit 5b13ba222b488b269983c26deac6d668293625b1 @microbubble committed Nov 12, 2009
Showing with 642 additions and 0 deletions.
  1. +3 −0 config/search.php
  2. +65 −0 controllers/search.php
  3. +184 −0 libraries/Search.php
  4. +28 −0 libraries/Search_Field.php
  5. +38 −0 libraries/Searchable.php
  6. +19 −0 libraries/Searchable_ORM.php
  7. +9 −0 models/cd.php
  8. +9 −0 models/mp3.php
  9. +51 −0 models/track.php
  10. +236 −0 views/search.php
@@ -0,0 +1,3 @@
+<?php defined('SYSPATH') or die('No direct script access.');
+
+$config['index_path'] = 'searchindex';
@@ -0,0 +1,65 @@
+<?php defined('SYSPATH') OR die('No direct access allowed.');
+
+class Search_Controller extends Controller {
+
+ const ALLOW_PRODUCTION = FALSE;
+
+ public function index($msg = NULL) {
+
+ $results = NULL;
+ $query = NULL;
+
+ $view = new View('search');
+
+ $view->bind("results", $results)
+ ->bind("query", $query)
+ ->bind("msg", $msg);
+
+ if (!empty($_GET["q"])) {
+
+ try {
+ $query = $_GET["q"];
+
+ $search = new Search;
+ $results = $search->find($query);
+ }
+ catch(Exception $e) {
+ Kohana::log("error", $e);
+ }
+ }
+
+ $view->render(TRUE);
+ }
+
+ public function add() {
+
+ $items = array();
+
+ $song = new Mp3_Model(1, "Ian Brown", "My Star");
+ $items[] = $song;
+
+ $song = new Mp3_Model(2, "Rolling Stones", "Brown Sugar");
+ $items[] = $song;
+
+ $song = new Cd_Model(3, "Stone Roses", "Sugar Spun Sister");
+ $items[] = $song;
+
+ $song = new Cd_Model(4, "David Bowie", "Starman");
+ $items[] = $song;
+
+ $song = new Mp3_Model(4, "Bob Dylan", "Like a Rolling Stone");
+ $items[] = $song;
+
+
+ try {
+ $search = new Search;
+ $search->build_search_index($items);
+
+ $this->index("Index successfully populated");
+ }
+ catch(Exception $e) {
+ $this->index($e);
+ }
+ }
+
+} // End Search Controller
@@ -0,0 +1,184 @@
+<?php defined('SYSPATH') or die('No direct script access.');
+
+class Search_Core {
+
+ const CREATE_NEW = TRUE;
+
+ private $index_path, $index;
+
+ public function __construct() {
+ $this->index_path = Kohana::config('search.index_path');
+
+ if( !file_exists($this->get_index_path())) {
+ throw new Kohana_User_Exception('Invalid index path', 'Could not find index path '.$this->get_index_path());
+ }
+ elseif(! is_dir($this->get_index_path())) {
+ throw new Kohana_User_Exception('Invalid index path', 'index path id not a directory');
+ }
+ elseif(! is_writable($this->get_index_path())) {
+ throw new Kohana_User_Exception('Invalid index path', 'Could not find index path ');
+ }
+
+ $this->load_search_libs();
+
+ // set default analyzer to UTF8 with numbers, and case insensitive. Number are useful when searching on e.g. product codes
+ //Zend_Search_Lucene_Analysis_Analyzer::setDefault(new Zend_Search_Lucene_Analysis_Analyzer_Common_Utf8Num_CaseInsensitive());
+
+ // use stemming analyser - http://codefury.net/2008/06/a-stemming-analyzer-for-zends-php-lucene/
+ Zend_Search_Lucene_Analysis_Analyzer::setDefault(new StandardAnalyzer_Analyzer_Standard_English());
+ }
+
+ /**
+ * Query the index
+ * @param String $query Lucene query
+ * @return Zend_Search_Lucene_Search_QueryHit hits
+ */
+ public function find($query) {
+ $this->open_index();
+ return $this->index->find($query);
+ }
+
+ /**
+ * Add an entry
+ */
+ public function add($item, $create_new = FALSE) {
+
+ if(!$create_new) {
+ $this->open_index();
+ }
+
+ // ensure item implements Searchable interface
+ if(! is_a($item, "Searchable")) {
+ throw new Kohana_User_Exception('Invalid Object', 'Object must implement Searchable Interface');
+ }
+
+ $doc = new Zend_Search_Lucene_Document();
+
+ // get indexable fields;
+ $fields = $item->get_indexable_fields();
+
+ // index the object type - this allows search results to be grouped/searched by type
+ $doc->addField(Zend_Search_Lucene_Field::Keyword('type', $item->get_type()));
+
+ // index the object's id - to avoid any confusion, we call it 'identifier' as Lucene uses 'id' attribute internally.
+ $doc->addField(Zend_Search_Lucene_Field::UnIndexed('identifier', $item->get_identifier())); // store, but don't index or tokenize
+
+ // index the object type plus identifier - this gives us a unique identifier for later retrieval - e.g. to delete
+ $doc->addField(Zend_Search_Lucene_Field::Keyword('uid', $item->get_unique_identifier()));
+
+ // index all fields that have been identified by Interface
+ foreach($fields as $field) {
+ // get attribute value from model
+ $value = $item->__get($field->name);
+
+ // html decode value if required
+ $value = $field->html_decode ? htmlspecialchars_decode($value) : $value;
+
+ // add field value based on type
+ switch($field->type) {
+ case Searchable::KEYWORD :
+ $doc->addField(Zend_Search_Lucene_Field::Keyword($field->name, $value));
+ break;
+
+ case Searchable::UNINDEXED :
+ $doc->addField(Zend_Search_Lucene_Field::UnIndexed($field->name, $value));
+ break;
+
+ case Searchable::BINARY :
+ $doc->addField(Zend_Search_Lucene_Field::Binary($field->name, $value));
+ break;
+
+ case Searchable::TEXT :
+ $doc->addField(Zend_Search_Lucene_Field::Text($field->name, $value));
+ break;
+
+ case Searchable::UNSTORED :
+ $doc->addField(Zend_Search_Lucene_Field::UnStored($field->name, $value));
+ break;
+ }
+ }
+ $this->index->addDocument($doc);
+ }
+
+ /**
+ * Update an entry
+ * We must first remove the entry from the index, then re-add it. To remove, we must find it by unique identifier
+ */
+ public function update($item) {
+
+ $this->remove($item)->add($item);
+ }
+
+ /**
+ * Remove an entry from the index
+ */
+ public function remove($item) {
+
+ $this->open_index();
+
+ // now we have the identifier, find it
+ $hits = $this->find('uid:'.$item->get_unique_identifier());
+
+ if(sizeof($hits) == 0) {
+ Kohana::log("error", "No index entry found for id ".$item->get_unique_identifier());
+ }
+ else if(sizeof($hits) > 1) {
+ Kohana::log("error", "Non-unique Identifier - More than one record was returned");
+ }
+
+ if(sizeof($hits) > 0) {
+ $this->index->delete($hits[0]->id);
+ }
+
+ // return this so we can have chainable methods - for an update
+ return $this;
+ }
+
+ /**
+ * Build new site index
+ */
+ public function build_search_index($items) {
+ // rebuild new index - create, not open
+ $this->create_index();
+
+ foreach($items as $item) {
+ $this->add($item, self::CREATE_NEW);
+ }
+
+ $this->index->optimize();
+ }
+
+ private function load_search_libs() {
+
+ if ($path = Kohana::find_file('vendor', 'Zend/Loader'))
+ {
+ ini_set('include_path', ini_get('include_path').PATH_SEPARATOR.dirname(dirname($path)));
+ }
+
+ require_once 'Zend/Loader/Autoloader.php';
+
+ require_once 'StandardAnalyzer/Analyzer/Standard/English.php';
+
+ Zend_Loader_Autoloader::getInstance();
+ }
+
+ private function get_index_path() {
+ return APPPATH.$this->index_path;
+ }
+
+ private function open_index() {
+
+ if(empty($this->index)) {
+ $this->index = $index = Zend_Search_Lucene::open($this->get_index_path()); // Open existing index;
+ }
+ }
+
+ private function create_index() {
+
+ if(empty($this->index)) {
+ $this->index = Zend_Search_Lucene::create($this->get_index_path(), true);
+ }
+ }
+
+
+}
@@ -0,0 +1,28 @@
+<?php defined('SYSPATH') or die('no direct scrip access');
+
+/**
+ * Represents a Model's search field, describing the type of Index
+ */
+class Search_Field {
+ private $name;
+ private $type;
+ private $html_decode;
+
+ /**
+ * @param String $name attribute name e.g. db table column name
+ * @param Zend_Search_Lucene_Field constant $type
+ * @param boolean $html_decode whether or not field data should be decoded prior to indexing- useful for CMS/WYSIWYG data. Default False
+ */
+ function Search_Field($name, $type, $html_decode = FALSE) {
+ $this->name = $name;
+ $this->type = $type;
+ $this->html_decode = $html_decode;
+ }
+
+ /**
+ * Accessor for private properties
+ */
+ public function __get($var){
+ return $this->$var;
+ }
+}
@@ -0,0 +1,38 @@
+<?php defined('SYSPATH') or die('no direct scrip access');
+
+/**
+ * Searchable Interface for use by Search Class.
+ */
+interface Searchable {
+
+ const KEYWORD = 0;
+ const UNINDEXED = 1;
+ const BINARY = 2;
+ const TEXT = 3;
+ const UNSTORED = 4;
+
+ const DECODE_HTML = TRUE;
+ const DONT_DECODE_HTML = FALSE;
+
+ /**
+ * @return array of Search_Field objects
+ */
+ function get_indexable_fields();
+
+ /**
+ * @return mixed identifier for this item
+ * For ORM Models this would be the PK
+ */
+ function get_identifier();
+
+ /**
+ * @return String type of item
+ * For ORM Models this would likely be the object name
+ */
+ function get_type();
+
+ /**
+ * @return mixed unique id of this item
+ */
+ function get_unique_identifier();
+}
@@ -0,0 +1,19 @@
+<?php defined('SYSPATH') or die('no direct scrip access');
+
+abstract class Searchable_ORM_Core extends ORM implements Searchable {
+
+ public function get_identifier()
+ {
+ return $this->__get($this->primary_key);
+ }
+
+ public function get_type()
+ {
+ return $this->object_name;
+ }
+
+ public function get_unique_identifier()
+ {
+ return $this->object_name."_". $this->get_identifier();
+ }
+}
@@ -0,0 +1,9 @@
+<?php defined('SYSPATH') or die('No direct script access.');
+
+class Cd_Model extends Track_Model
+{
+ public function __construct($id, $artist, $title)
+ {
+ parent::__construct($id, $artist, $title);
+ }
+}
@@ -0,0 +1,9 @@
+<?php defined('SYSPATH') or die('No direct script access.');
+
+class Mp3_Model extends Track_Model
+{
+ public function __construct($id, $artist, $title)
+ {
+ parent::__construct($id, $artist, $title);
+ }
+}
Oops, something went wrong.

0 comments on commit 5b13ba2

Please sign in to comment.