Merge pull request #4 from silverstripe-big-o/boost

NEW Added search-time boost support to SolrIndex
This commit is contained in:
Hamish Friedlander 2012-11-01 14:22:59 -07:00
commit 53af22d3bb
4 changed files with 115 additions and 16 deletions

View File

@ -11,6 +11,7 @@ An attempt to add stable support for Fulltext Search engines like Sphinx and Sol
## Requirements
* SilverStripe 3.0
* (optional) [silverstripe-phockito](https://github.com/hafriedlander/silverstripe-phockito) (for testing)
## Documentation

View File

@ -31,11 +31,23 @@ class SearchQuery extends ViewableData {
if (self::$present === null) self::$present = new stdClass();
}
function search($text, $fields = null, $boost = 1) {
/**
* @param [type] $text [description]
* @param [type] $fields [description]
* @param array $boost Map of field names to float values. The higher the value,
* the more important the field gets for relevancy.
*/
function search($text, $fields = null, $boost = array()) {
$this->search[] = array('text' => $text, 'fields' => $fields ? (array)$fields : null, 'boost' => $boost, 'fuzzy' => false);
}
function fuzzysearch($text, $fields = null, $boost = 1) {
/**
* @param [type] $text [description]
* @param [type] $fields [description]
* @param array $boost Map of field names to float values. The higher the value,
* the more important the field gets for relevancy.
*/
function fuzzysearch($text, $fields = null, $boost = array()) {
$this->search[] = array('text' => $text, 'fields' => $fields ? (array)$fields : null, 'boost' => $boost, 'fuzzy' => true);
}

View File

@ -99,6 +99,7 @@ abstract class SolrIndex extends SearchIndex {
if ($class != $field['origin'] && !is_subclass_of($class, $field['origin'])) return;
$value = $this->_getFieldValue($object, $field);
$type = isset(self::$filterTypeMap[$field['type']]) ? self::$filterTypeMap[$field['type']] : self::$filterTypeMap['*'];
if (is_array($value)) foreach($value as $sub) {
@ -169,8 +170,15 @@ abstract class SolrIndex extends SearchIndex {
Solr::service(get_class($this))->commit(false, false, false);
}
public function search($query, $offset = -1, $limit = -1) {
$service = Solr::service(get_class($this));
/**
* @param SearchQuery $query
* @param integer $offset
* @param integer $limit
* @return ArrayData Map with the following keys:
* - 'Matches': ArrayList of the matched object instances
*/
public function search(SearchQuery $query, $offset = -1, $limit = -1) {
$service = $this->getService();
SearchVariant::with(count($query->classes) == 1 ? $query->classes[0]['class'] : null)->call('alterQuery', $query, $this);
@ -186,12 +194,15 @@ abstract class SolrIndex extends SearchIndex {
$fuzzy = $search['fuzzy'] ? '~' : '';
foreach ($parts[0] as $part) {
if ($search['fields']) {
$fields = (isset($search['fields'])) ? $search['fields'] : array();
if(isset($search['boost'])) $fields = array_merge($fields, array_keys($search['boost']));
if ($fields) {
$searchq = array();
foreach ($search['fields'] as $field) {
$searchq[] = "{$field}:".$part.$fuzzy;
foreach ($fields as $field) {
$boost = (isset($search['boost'][$field])) ? '^' . $search['boost'][$field] : '';
$searchq[] = "{$field}:".$part.$fuzzy.$boost;
}
$q[] = '+('.implode(' ', $searchq).')';
$q[] = '+('.implode(' OR ', $searchq).')';
}
else {
$q[] = '+'.$part;
@ -259,27 +270,39 @@ abstract class SolrIndex extends SearchIndex {
$fq[] = ($missing ? "+{$field}:[* TO *] " : '') . '-('.implode(' ', $excludeq).')';
}
if ($q) header('X-Query: '.implode(' ', $q));
if ($fq) header('X-Filters: "'.implode('", "', $fq).'"');
if(!headers_sent()) {
if ($q) header('X-Query: '.implode(' ', $q));
if ($fq) header('X-Filters: "'.implode('", "', $fq).'"');
}
if ($offset == -1) $offset = $query->start;
if ($limit == -1) $limit = $query->limit;
if ($limit == -1) $limit = SearchQuery::$default_page_size;
$res = $service->search($q ? implode(' ', $q) : '*:*', $offset, $limit, array('fq' => implode(' ', $fq)), Apache_Solr_Service::METHOD_POST);
$res = $service->search(
$q ? implode(' ', $q) : '*:*',
$offset,
$limit,
array('fq' => implode(' ', $fq)),
Apache_Solr_Service::METHOD_POST
);
$results = new ArrayList();
foreach ($res->response->docs as $doc) {
$result = DataObject::get_by_id($doc->ClassName, $doc->ID);
if($result) $results->push($result);
if($res->getHttpStatus() >= 200 && $res->getHttpStatus() < 300) {
foreach ($res->response->docs as $doc) {
$result = DataObject::get_by_id($doc->ClassName, $doc->ID);
if($result) $results->push($result);
}
$numFound = $res->response->numFound;
} else {
$numFound = 0;
}
$ret = array();
$ret['Matches'] = new PaginatedList($results);
$ret['Matches']->setLimitItems(false);
// Tell PaginatedList how many results there are
$ret['Matches']->setTotalItems($res->response->numFound);
$ret['Matches']->setTotalItems($numFound);
// Results for current page start at $offset
$ret['Matches']->setPageStart($offset);
// Results per page
@ -287,4 +310,19 @@ abstract class SolrIndex extends SearchIndex {
return new ArrayData($ret);
}
protected $service;
/**
* @return SolrService
*/
public function getService() {
if(!$this->service) $this->service = Solr::service(get_class($this));
return $this->service;
}
public function setService(SolrService $service) {
$this->service = $service;
return $this;
}
}

48
tests/SolrIndexTest.php Normal file
View File

@ -0,0 +1,48 @@
<?php
class SolrIndexTest extends SapphireTest {
function setUpOnce() {
parent::setUpOnce();
Phockito::include_hamcrest();
}
function testBoost() {
$serviceMock = $this->getServiceMock();
$index = new SolrIndexTest_FakeIndex();
$index->setService($serviceMock);
$query = new SearchQuery();
$query->search(
'term',
null,
array('Field1' => 1.5, 'HasOneObject_Field1' => 3)
);
$index->search($query);
Phockito::verify($serviceMock)->search(
'+(Field1:term^1.5 OR HasOneObject_Field1:term^3)',
anything(), anything(), anything(), anything()
);
}
protected function getServiceMock() {
$serviceMock = Phockito::mock('SolrService');
$fakeResponse = new Apache_Solr_Response(new Apache_Solr_HttpTransport_Response(null, null, null));
Phockito::when($serviceMock)
->search(anything(), anything(), anything(), anything(), anything())
->return($fakeResponse);
return $serviceMock;
}
}
class SolrIndexTest_FakeIndex extends SolrIndex {
function init() {
$this->addClass('SearchUpdaterTest_Container');
$this->addFilterField('Field1');
$this->addFilterField('HasOneObject.Field1');
$this->addFilterField('HasManyObjects.Field1');
}
}