FEATURE: added advanced search form to allow searching by module and version

This commit is contained in:
Will Rossiter 2011-08-04 10:04:53 +12:00
parent 0ba6d8d338
commit 85e5b1b72d
5 changed files with 247 additions and 52 deletions

View File

@ -59,6 +59,21 @@ class DocumentationSearch {
*/ */
private $outputController; private $outputController;
/**
* Optionally filter by module and version
*
* @var array
*/
private $modules, $versions;
public function setModules($modules) {
$this->modules = $modules;
}
public function setVersions($versions) {
$this->versions = $versions;
}
/** /**
* Set the current search query * Set the current search query
* *
@ -173,9 +188,37 @@ class DocumentationSearch {
try { try {
$index = Zend_Search_Lucene::open(self::get_index_location()); $index = Zend_Search_Lucene::open(self::get_index_location());
Zend_Search_Lucene::setResultSetLimit(200); Zend_Search_Lucene::setResultSetLimit(100);
$query = new Zend_Search_Lucene_Search_Query_Boolean();
$term = Zend_Search_Lucene_Search_QueryParser::parse($this->getQuery());
$query->addSubquery($term, true);
if($this->modules) {
$moduleQuery = new Zend_Search_Lucene_Search_Query_MultiTerm();
foreach($this->modules as $module) {
$moduleQuery->addTerm(new Zend_Search_Lucene_Index_Term($module, 'Module'));
}
$query->addSubquery($moduleQuery, true);
}
if($this->versions) {
$versionQuery = new Zend_Search_Lucene_Search_Query_MultiTerm();
foreach($this->versions as $version) {
$versionQuery->addTerm(new Zend_Search_Lucene_Index_Term($version, 'Version'));
}
$query->addSubquery($versionQuery, true);
}
$er = error_reporting();
error_reporting('E_ALL ^ E_NOTICE');
$this->results = $index->find($query);
error_reporting($er);
$this->results = $index->find($this->getQuery());
$this->totalResults = $index->numDocs(); $this->totalResults = $index->numDocs();
} }
catch(Zend_Search_Lucene_Exception $e) { catch(Zend_Search_Lucene_Exception $e) {
@ -192,6 +235,8 @@ class DocumentationSearch {
$data = array( $data = array(
'Results' => null, 'Results' => null,
'Query' => null, 'Query' => null,
'Versions' => DBField::create('Text', implode(',', $this->versions)),
'Modules' => DBField::create('Text', implode(',', $this->modules)),
'Title' => _t('DocumentationSearch.SEARCHRESULTS', 'Search Results'), 'Title' => _t('DocumentationSearch.SEARCHRESULTS', 'Search Results'),
'TotalResults' => null, 'TotalResults' => null,
'TotalPages' => null, 'TotalPages' => null,
@ -216,6 +261,7 @@ class DocumentationSearch {
$results = new DataObjectSet(); $results = new DataObjectSet();
if($this->results) {
foreach($this->results as $k => $hit) { foreach($this->results as $k => $hit) {
if($k < ($currentPage-1)*$pageLength || $k >= ($currentPage*$pageLength)) continue; if($k < ($currentPage-1)*$pageLength || $k >= ($currentPage*$pageLength)) continue;
@ -223,13 +269,13 @@ class DocumentationSearch {
$content = $hit->content; $content = $hit->content;
// do a simple markdown parse of the file
$obj = new ArrayData(array( $obj = new ArrayData(array(
'Title' => DBField::create('Varchar', $doc->getFieldValue('Title')), 'Title' => DBField::create('Varchar', $doc->getFieldValue('Title')),
'BreadcrumbTitle' => DBField::create('HTMLText', $doc->getFieldValue('BreadcrumbTitle')), 'BreadcrumbTitle' => DBField::create('HTMLText', $doc->getFieldValue('BreadcrumbTitle')),
'Link' => DBField::create('Varchar',$doc->getFieldValue('Link')), 'Link' => DBField::create('Varchar',$doc->getFieldValue('Link')),
'Language' => DBField::create('Varchar',$doc->getFieldValue('Language')), 'Language' => DBField::create('Varchar',$doc->getFieldValue('Language')),
'Version' => DBField::create('Varchar',$doc->getFieldValue('Version')), 'Version' => DBField::create('Varchar',$doc->getFieldValue('Version')),
'Module' => DBField::create('Varchar', $doc->getFieldValue('Module')),
'Content' => DBField::create('HTMLText', $content), 'Content' => DBField::create('HTMLText', $content),
'Score' => $hit->score, 'Score' => $hit->score,
'Number' => $k + 1, 'Number' => $k + 1,
@ -238,6 +284,7 @@ class DocumentationSearch {
$results->push($obj); $results->push($obj);
} }
}
$data['Results'] = $results; $data['Results'] = $results;
$data['Query'] = DBField::create('Text', $query); $data['Query'] = DBField::create('Text', $query);
@ -358,7 +405,7 @@ class DocumentationSearch {
* the search results template or the Atom feed * the search results template or the Atom feed
*/ */
public function renderResults() { public function renderResults() {
if(!$this->results) $this->performSearch(); if(!$this->results && $this->query) $this->performSearch();
if(!$this->outputController) return user_error('Call renderResults() on a DocumentationViewer instance.', E_USER_ERROR); if(!$this->outputController) return user_error('Call renderResults() on a DocumentationViewer instance.', E_USER_ERROR);
$request = $this->outputController->getRequest(); $request = $this->outputController->getRequest();

View File

@ -144,7 +144,7 @@ class DocumentationService {
if($version || $lang) { if($version || $lang) {
foreach($entities as $entity) { foreach($entities as $entity) {
if(self::is_registered_entity($entity->getFolder(), $version, $lang)) { if(self::is_registered_entity($entity->getFolder(), $version, $lang)) {
$output[] = $entity; $output[$entity->getFolder()] = $entity;
} }
} }
} }

View File

@ -156,7 +156,7 @@ class DocumentationViewer extends Controller {
} }
// check to see if the module is a valid module. If it isn't, then we // check to see if the request is a valid entity. If it isn't, then we
// need to throw a 404. // need to throw a 404.
if(!DocumentationService::is_registered_entity($firstParam)) { if(!DocumentationService::is_registered_entity($firstParam)) {
return $this->throw404(); return $this->throw404();
@ -246,7 +246,7 @@ class DocumentationViewer extends Controller {
/** /**
* Returns the current version. If no version is set then it is the current * Returns the current version. If no version is set then it is the current
* set version so need to pull that from the module. * set version so need to pull that from the {@link Entity}.
* *
* @return String * @return String
*/ */
@ -272,10 +272,9 @@ class DocumentationViewer extends Controller {
} }
/** /**
* Return all the available languages for the module. * Return all the available languages for the {@link Entity}.
* *
* @param String - The name of the module * @return array
* @return DataObjectSet
*/ */
function getLanguages() { function getLanguages() {
$entity = $this->getEntity(); $entity = $this->getEntity();
@ -291,7 +290,7 @@ class DocumentationViewer extends Controller {
* Get all the versions loaded for the current {@link DocumentationEntity}. * Get all the versions loaded for the current {@link DocumentationEntity}.
* the filesystem then they are loaded under the 'Current' namespace. * the filesystem then they are loaded under the 'Current' namespace.
* *
* @param String $entity name of module to limit it to eg sapphire * @param String $entity name of {@link Entity} to limit it to eg sapphire
* @return DataObjectSet * @return DataObjectSet
*/ */
function getVersions($entity = false) { function getVersions($entity = false) {
@ -308,8 +307,6 @@ class DocumentationViewer extends Controller {
$currentVersion = $this->getVersion(); $currentVersion = $this->getVersion();
foreach($versions as $key => $version) { foreach($versions as $key => $version) {
// work out the link to this version of the documentation.
// @todo Keep the user on their given page rather than redirecting to module.
$linkingMode = ($currentVersion == $version) ? 'current' : 'link'; $linkingMode = ($currentVersion == $version) ? 'current' : 'link';
if(!$version) $version = 'Current'; if(!$version) $version = 'Current';
@ -480,7 +477,7 @@ class DocumentationViewer extends Controller {
} }
/** /**
* Get the module pages under a given page. Recursive call for {@link getEntityPages()} * Get all the pages under a given page. Recursive call for {@link getEntityPages()}
* *
* @todo Need to rethink how to support pages which are pulling content from their children * @todo Need to rethink how to support pages which are pulling content from their children
* i.e if a folder doesn't have 2 then it will load the first file in the folder * i.e if a folder doesn't have 2 then it will load the first file in the folder
@ -607,7 +604,7 @@ class DocumentationViewer extends Controller {
foreach($pages as $i => $title) { foreach($pages as $i => $title) {
if($title) { if($title) {
// Don't add module name, already present in Link() // Don't add entity name, already present in Link()
if($i > 0) $path[] = $title; if($i > 0) $path[] = $title;
$output->push(new ArrayData(array( $output->push(new ArrayData(array(
@ -651,9 +648,6 @@ class DocumentationViewer extends Controller {
* @return String * @return String
*/ */
public function Link($path = false, $entity = false, $version = false, $lang = false) { public function Link($path = false, $entity = false, $version = false, $lang = false) {
$base = Director::absoluteBaseURL();
// only include the version. Version is optional after all
$version = ($version === null) ? $this->getVersion() : $version; $version = ($version === null) ? $this->getVersion() : $version;
$lang = (!$lang) ? $this->getLang() : $lang; $lang = (!$lang) ? $this->getLang() : $lang;
@ -668,7 +662,14 @@ class DocumentationViewer extends Controller {
$action = implode('/', $path); $action = implode('/', $path);
} }
$link = Controller::join_links($base, self::get_link_base(), $entity, $lang, $version, $action); $link = Controller::join_links(
Director::absoluteBaseURL(),
self::get_link_base(),
$entity,
($entity) ? $lang : "", // only include lang for entity - sapphire/en vs en/
($entity) ? $version :"",
$action
);
return $link; return $link;
} }
@ -733,18 +734,19 @@ class DocumentationViewer extends Controller {
} }
/** /**
* Documentation Basic Search Form * Documentation Search Form. Allows filtering of the results by many entities
* and multiple versions.
* *
* Integrates with sphinx
* @return Form * @return Form
*/ */
function DocumentationSearchForm() { function DocumentationSearchForm() {
if(!DocumentationSearch::enabled()) return false; if(!DocumentationSearch::enabled()) return false;
$q = ($q = $this->getSearchQuery()) ? $q->NoHTML() : "";
$query = (isset($_REQUEST['Search'])) ? Convert::raw2xml($_REQUEST['Search']) : "";
$fields = new FieldSet( $fields = new FieldSet(
new TextField('Search', _t('DocumentationViewer.SEARCH', 'Search'), $query) new TextField('Search', _t('DocumentationViewer.SEARCH', 'Search'), $q),
new HiddenField('Entities', '', implode(',', array_keys($this->getSearchedEntities()))),
new HiddenField('Versions', '', implode(',', $this->getSearchedVersions()))
); );
$actions = new FieldSet( $actions = new FieldSet(
@ -759,22 +761,137 @@ class DocumentationViewer extends Controller {
return $form; return $form;
} }
/**
* Return an array of folders and titles
*
* @return array
*/
function getSearchedEntities() {
$entities = array();
if(isset($_REQUEST['Entities'])) {
if(is_array($_REQUEST['Entities'])) {
$entities = Convert::raw2att($_REQUEST['Entities']);
}
else {
$entities = explode(',', Convert::raw2att($_REQUEST['Entities']));
$entities = array_combine($entities, $entities);
}
}
else if($entity = $this->getEntity()) {
$entities[$entity->getFolder()] = Convert::raw2att($entity->getTitle());
}
return $entities;
}
/**
* Return an array of versions that we're allowed to return
*
* @return array
*/
function getSearchedVersions() {
$versions = array();
if(isset($_REQUEST['Versions'])) {
if(is_array($_REQUEST['Versions'])) {
$versions = Convert::raw2att($_REQUEST['Versions']);
$versions = array_combine($versions, $versions);
}
else {
$version = Convert::raw2att($_REQUEST['Versions']);
$versions[$version] = $version;
}
}
else if($version = $this->getVersion()) {
$version = Convert::raw2att($version);
$versions[$version] = $version;
}
return $versions;
}
/**
* Return the current search query
*
* @return HTMLText|null
*/
function getSearchQuery() {
if(isset($_REQUEST['Search'])) {
return DBField::create('HTMLText', $_REQUEST['Search']);
}
}
/** /**
* Past straight to results, display and encode the query * Past straight to results, display and encode the query
*/ */
function results($data, $form = false) { function results($data, $form = false) {
$query = (isset($_REQUEST['Search'])) ? $_REQUEST['Search'] : false; $query = (isset($_REQUEST['Search'])) ? $_REQUEST['Search'] : false;
if(!$query) return $this->httpError('404');
$search = new DocumentationSearch(); $search = new DocumentationSearch();
$search->setQuery($query); $search->setQuery($query);
$search->setVersions($this->getSearchedVersions());
$search->setModules($this->getSearchedEntities());
$search->setOutputController($this); $search->setOutputController($this);
return $search->renderResults(); return $search->renderResults();
} }
/**
* Returns an search form which allows people to express more complex rules
* and options than the plain search form.
*
* @todo client side filtering of checkable option based on the module selected.
*
* @return Form
*/
function AdvancedSearchForm() {
$entities = DocumentationService::get_registered_entities();
$versions = array();
foreach($entities as $entity) {
$versions[$entity->getFolder()] = $entity->getVersions();
}
// get a list of all the unique versions
$uniqueVersions = array_unique(self::array_flatten(array_values($versions)));
asort($uniqueVersions);
$uniqueVersions = array_combine($uniqueVersions,$uniqueVersions);
$q = ($q = $this->getSearchQuery()) ? $q->NoHTML() : "";
// klude to take an array of objects down to a simple map
$entities = new DataObjectSet($entities);
$entities = $entities->map('Folder', 'Title');
// if we haven't gone any search limit then we're searching everything
$searchedEntities = $this->getSearchedEntities();
if(count($searchedEntities) < 1) $searchedEntities = $entities;
$searchedVersions = $this->getSearchedVersions();
if(count($searchedVersions) < 1) $searchedVersions = $uniqueVersions;
$fields = new FieldSet(
new TextField('Search', _t('DocumentationViewer.KEYWORDS', 'Keywords'), $q),
new CheckboxSetField('Entities', _t('DocumentationViewer.MODULES', 'Modules'), $entities, $searchedEntities),
new CheckboxSetField('Versions', _t('DocumentationViewer.VERSIONS', 'Versions'),
$uniqueVersions, $searchedVersions
)
);
$actions = new FieldSet(
new FormAction('results', _t('DocumentationViewer.SEARCH', 'Search'))
);
$required = new RequiredFields(array('Search'));
$form = new Form($this, 'AdvancedSearchForm', $fields, $actions, $required);
$form->disableSecurityToken();
$form->setFormMethod('get');
$form->setFormAction(self::$link_base . 'DocumentationSearchForm');
return $form;
}
/** /**
* Check to see if the currently accessed version is out of date or * Check to see if the currently accessed version is out of date or
* perhaps a future version rather than the stable edition * perhaps a future version rather than the stable edition
@ -810,4 +927,26 @@ class DocumentationViewer extends Controller {
return false; return false;
} }
/**
* Flattens an array
*
* @param array
* @return array
*/
public static function array_flatten($array) {
if(!is_array($array)) return false;
$output = array();
foreach($array as $k => $v) {
if(is_array($v)) {
$output = array_merge($output, self::array_flatten($v));
}
else {
$output[$k] = $v;
}
}
return $output;
}
} }

View File

@ -73,15 +73,18 @@ class RebuildLuceneDocsIndex extends BuildTask {
$content = $page->getMarkdown(); $content = $page->getMarkdown();
if($content) $content = Markdown($content); if($content) $content = Markdown($content);
$entity = ($entity = $page->getEntity()) ? $entity->getTitle() : "";
$doc->addField(Zend_Search_Lucene_Field::Text('content', $content)); $doc->addField(Zend_Search_Lucene_Field::Text('content', $content));
$doc->addField($titleField = Zend_Search_Lucene_Field::Text('Title', $page->getTitle())); $doc->addField($titleField = Zend_Search_Lucene_Field::Text('Title', $page->getTitle()));
$doc->addField($breadcrumbField = Zend_Search_Lucene_Field::Text('BreadcrumbTitle', $page->getBreadcrumbTitle())); $doc->addField($breadcrumbField = Zend_Search_Lucene_Field::Text('BreadcrumbTitle', $page->getBreadcrumbTitle()));
$doc->addField(Zend_Search_Lucene_Field::Keyword('Version', $page->getVersion())); $doc->addField(Zend_Search_Lucene_Field::Keyword('Version', $page->getVersion()));
$doc->addField(Zend_Search_Lucene_Field::Keyword('Language', $page->getLang())); $doc->addField(Zend_Search_Lucene_Field::Keyword('Language', $page->getLang()));
$doc->addField(Zend_Search_Lucene_Field::Keyword('Entity', $entity));
$doc->addField(Zend_Search_Lucene_Field::Keyword('Link', $page->Link())); $doc->addField(Zend_Search_Lucene_Field::Keyword('Link', $page->Link()));
// custom boosts // custom boosts
$titleField->boost = 1.5; $titleField->boost = 3;
$breadcrumbField->boost = 1.5; $breadcrumbField->boost = 1.5;
foreach(DocumentationSearch::$boost_by_path as $pathExpr => $boost) { foreach(DocumentationSearch::$boost_by_path as $pathExpr => $boost) {
if(preg_match($pathExpr, $page->getRelativePath())) $doc->boost = $boost; if(preg_match($pathExpr, $page->getRelativePath())) $doc->boost = $boost;

View File

@ -1,6 +1,9 @@
<div id="documentation-page"> <div id="documentation-page">
<div id="content-column"> <div id="content-column">
<p>Your search for <strong>&quot;$Query.XML&quot;</strong> found $TotalResults result<% if TotalResults != 1 %>s<% end_if %>.</p> <p>Your search for <strong>&quot;$Query.XML&quot;</strong> found $TotalResults result<% if TotalResults != 1 %>s<% end_if %>.</p>
<% if Modules || Versions %>
<p>Limited search to <% if Modules %>$Modules <% if Versions %>of<% end_if %><% end_if %> <% if Versions %>versions $Versions<% end_if %>
<% end_if %>
<% if Results %> <% if Results %>
<p>Showing page $ThisPage of $TotalPages</p> <p>Showing page $ThisPage of $TotalPages</p>
@ -41,6 +44,9 @@
</div> </div>
<div id="sidebar-column"> <div id="sidebar-column">
<div class="sidebar-box">
<h4><% _t('ADVANCEDSEARCH', 'Advanced Search') %></h4>
$AdvancedSearchForm
</div>
</div> </div>
</div> </div>