WIP: Silverstripe 4 compatibility

Thanks to Marco Hermo and Brett Tasker for helping with this
* Bump framework/cms to ^4.0@dev
* WIP Silverstripe 4 compatibility fixes
* more replacements and patches to migrate this module to 4.0
* Update composer.json
* remove php <5.5 from travis.yml
* WIP more SS4 compatibility fixes
* WIP fix solr path to use DIR, avoid hardcoded module name
* WIP respect current include path
* WIP Namespacing and use on SearchIndex class
* Namespacing for tests
* WIP add namespaces to all classes
* Second push of Test changes + namespacing
* WIP split Solr files with multiple classes into single file / single class. Adjust namespaces
* Fix PHP errors in test
* break out search components with multiple classes into individual files and change namespaces
* Update namespacing for Search indexes and variants in tests
* Batch fixes for tests #2
* Update _config.php to use namespace
* Use root namespace in referencing Apache_Solr_Document
* Migrate task names so that the name is not fully qualified
This commit is contained in:
Elliot Sawyer 2017-02-17 12:03:51 +13:00 committed by elliot sawyer
parent 176014afaa
commit 1728a62af5
82 changed files with 1439 additions and 1164 deletions

View File

@ -5,7 +5,6 @@ language: php
sudo: false
php:
- 5.4
- 5.5
- 5.6
@ -14,8 +13,6 @@ env:
matrix:
include:
- php: 5.3
env: DB=PGSQL CORE_RELEASE=3.1
- php: 5.6
env: DB=MYSQL CORE_RELEASE=3.2
- php: 5.6

View File

@ -1,6 +1,4 @@
<?php
use SilverStripe\FullTextSearch\Search\Updaters\SearchUpdater;
global $databaseConfig;
if (isset($databaseConfig['type'])) SearchUpdater::bind_manipulation_capture();
Deprecation::notification_version('1.0.0', 'fulltextsearch');

View File

@ -1,5 +1,8 @@
<?php
namespace SilverStripe\FullTextSearch\Search;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\ClassInfo;
/**
* Base class to manage active search indexes.
*/

View File

@ -1,5 +1,7 @@
<?php
namespace SilverStripe\FullTextSearch\Search;
use SilverStripe\Core\ClassInfo;
use SilverStripe\ORM\DataObject;
/**
* Some additional introspection tools that are used often by the fulltext search code
*/
@ -41,18 +43,23 @@ class SearchIntrospection
$classes = array_unique(array_merge($classes, array_values(ClassInfo::subclassesFor($class))));
}
$idx = array_search('DataObject', $classes);
$idx = array_search('SilverStripe\View\ViewableData', $classes);
if ($idx !== false) {
array_splice($classes, 0, $idx+1);
}
if ($dataOnly) {
foreach ($classes as $i => $class) {
if (!DataObject::has_own_table($class)) {
unset($classes[$i]);
}
}
$idx = array_search('SilverStripe\Core\Object', $classes);
if ($idx !== false) {
array_splice($classes, 0, $idx+1);
}
//@todo find another way to determine if a dataobject does not have a table
// if ($dataOnly) {
// foreach ($classes as $i => $class) {
// if (!DataObject::has_own_table($class)) {
// unset($classes[$i]);
// }
// }
// }
self::$hierarchy[$key] = $classes;
}

View File

@ -1,5 +1,15 @@
<?php
namespace SilverStripe\FullTextSearch\Search\Indexes;
use SilverStripe\View\ViewableData;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DataObjectSchema;
use SilverStripe\Core\Object;
use SilverStripe\Core\ClassInfo;
use SilverStripe\FullTextSearch\Search\SearchIntrospection;
use SilverStripe\FullTextSearch\Search\Variants\SearchVariant;
use SilverStripe\FullTextSearch\Utils\MultipleArrayIterator;
/**
* SearchIndex is the base index class. Each connector will provide a subclass of this that
* provides search engine specific behavior.
@ -83,20 +93,20 @@ abstract class SearchIndex extends ViewableData
foreach (SearchIntrospection::hierarchy($source, $options['include_children']) as $dataclass) {
$singleton = singleton($dataclass);
if ($hasOne = $singleton->has_one($lookup)) {
if ($hasOne = $singleton->hasOne($lookup)) {
$class = $hasOne;
$options['lookup_chain'][] = array(
'call' => 'method', 'method' => $lookup,
'through' => 'has_one', 'class' => $dataclass, 'otherclass' => $class, 'foreignkey' => "{$lookup}ID"
);
} elseif ($hasMany = $singleton->has_many($lookup)) {
} elseif ($hasMany = $singleton->hasMany($lookup)) {
$class = $hasMany;
$options['multi_valued'] = true;
$options['lookup_chain'][] = array(
'call' => 'method', 'method' => $lookup,
'through' => 'has_many', 'class' => $dataclass, 'otherclass' => $class, 'foreignkey' => $singleton->getRemoteJoinField($lookup, 'has_many')
);
} elseif ($manyMany = $singleton->many_many($lookup)) {
} elseif ($manyMany = $singleton->manyMany($lookup)) {
$class = $manyMany[1];
$options['multi_valued'] = true;
$options['lookup_chain'][] = array(
@ -105,7 +115,7 @@ abstract class SearchIndex extends ViewableData
);
}
if ($class) {
if (is_string($class) && $class) {
if (!isset($options['origin'])) {
$options['origin'] = $dataclass;
}
@ -130,7 +140,7 @@ abstract class SearchIndex extends ViewableData
$type = null;
$fieldoptions = $options;
$fields = DataObject::database_fields($dataclass);
$fields = DataObject::getSchema()->databaseFields($class);
if (isset($fields[$field])) {
$type = $fields[$field];
@ -209,7 +219,7 @@ abstract class SearchIndex extends ViewableData
throw new Exception('Can\'t add class to Index after fields have already been added');
}
if (!DataObject::has_own_table($class)) {
if (!DataObject::getSchema()->classHasTable($class)) {
throw new InvalidArgumentException('Can\'t add classes which don\'t have data tables (no $db or $has_one set on the class)');
}
@ -286,7 +296,7 @@ abstract class SearchIndex extends ViewableData
{
foreach ($this->getClasses() as $class => $options) {
foreach (SearchIntrospection::hierarchy($class, $includeSubclasses, true) as $dataclass) {
$fields = DataObject::database_fields($dataclass);
$fields = DataObject::getSchema()->databaseFields($class);
foreach ($fields as $field => $type) {
if (preg_match('/^(\w+)\(/', $type, $match)) {
@ -599,95 +609,3 @@ abstract class SearchIndex extends ViewableData
*/
abstract public function init();
}
/**
* A search index that does nothing. Useful for testing
*/
abstract class SearchIndex_Null extends SearchIndex
{
public function add($object)
{
}
public function delete($base, $id, $state)
{
}
public function commit()
{
}
}
/**
* A search index that just records actions. Useful for testing
*/
abstract class SearchIndex_Recording extends SearchIndex
{
public $added = array();
public $deleted = array();
public $committed = false;
public function reset()
{
$this->added = array();
$this->deleted = array();
$this->committed = false;
}
public function add($object)
{
$res = array();
$res['ID'] = $object->ID;
foreach ($this->getFieldsIterator() as $name => $field) {
$val = $this->_getFieldValue($object, $field);
$res[$name] = $val;
}
$this->added[] = $res;
}
public function getAdded($fields = array())
{
$res = array();
foreach ($this->added as $added) {
$filtered = array();
foreach ($fields as $field) {
if (isset($added[$field])) {
$filtered[$field] = $added[$field];
}
}
$res[] = $filtered;
}
return $res;
}
public function delete($base, $id, $state)
{
$this->deleted[] = array('base' => $base, 'id' => $id, 'state' => $state);
}
public function commit()
{
$this->committed = true;
}
public function getIndexName()
{
return get_class($this);
}
public function getIsCommitted()
{
return $this->committed;
}
public function getService()
{
// Causes commits to the service to be redirected back to the same object
return $this;
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace SilverStripe\FullTextSearch\Search\Indexes;
use SilverStripe\FullTextSearch\Search\Indexes\SearchIndex;
/**
* A search index that does nothing. Useful for testing
*/
abstract class SearchIndex_Null extends SearchIndex
{
public function add($object)
{
}
public function delete($base, $id, $state)
{
}
public function commit()
{
}
}

View File

@ -0,0 +1,79 @@
<?php
namespace SilverStripe\FullTextSearch\Search\Indexes;
use SilverStripe\FullTextSearch\Search\Indexes\SearchIndex;
/**
* A search index that just records actions. Useful for testing
*/
abstract class SearchIndex_Recording extends SearchIndex
{
public $added = array();
public $deleted = array();
public $committed = false;
public function reset()
{
$this->added = array();
$this->deleted = array();
$this->committed = false;
}
public function add($object)
{
$res = array();
$res['ID'] = $object->ID;
foreach ($this->getFieldsIterator() as $name => $field) {
$val = $this->_getFieldValue($object, $field);
$res[$name] = $val;
}
$this->added[] = $res;
}
public function getAdded($fields = array())
{
$res = array();
foreach ($this->added as $added) {
$filtered = array();
foreach ($fields as $field) {
if (isset($added[$field])) {
$filtered[$field] = $added[$field];
}
}
$res[] = $filtered;
}
return $res;
}
public function delete($base, $id, $state)
{
$this->deleted[] = array('base' => $base, 'id' => $id, 'state' => $state);
}
public function commit()
{
$this->committed = true;
}
public function getIndexName()
{
return get_class($this);
}
public function getIsCommitted()
{
return $this->committed;
}
public function getService()
{
// Causes commits to the service to be redirected back to the same object
return $this;
}
}

View File

@ -1,5 +1,5 @@
<?php
namespace SilverStripe\FullTextSearch\Search\Processors;
/**
* Provides batching of search updates
*/

View File

@ -1,5 +1,5 @@
<?php
namespace SilverStripe\FullTextSearch\Search\Processors;
if (!interface_exists('QueuedJob')) {
return;
}

View File

@ -1,5 +1,5 @@
<?php
namespace SilverStripe\FullTextSearch\Search\Processors;
class SearchUpdateImmediateProcessor extends SearchUpdateProcessor
{
public function triggerProcessing()

View File

@ -1,5 +1,5 @@
<?php
namespace SilverStripe\FullTextSearch\Search\Processors;
class SearchUpdateMessageQueueProcessor extends SearchUpdateProcessor
{
/**

View File

@ -1,5 +1,5 @@
<?php
namespace SilverStripe\FullTextSearch\Search\Processors;
abstract class SearchUpdateProcessor
{
/**

View File

@ -1,5 +1,5 @@
<?php
namespace SilverStripe\FullTextSearch\Search\Processors;
if (!interface_exists('QueuedJob')) {
return;
}

View File

@ -1,10 +1,11 @@
<?php
namespace SilverStripe\FullTextSearch\Search\Indexes;
/**
* Represents a search query
*
* API very much still in flux.
*/
use SilverStripe\View\ViewableData;
class SearchQuery extends ViewableData
{
public static $missing = null;
@ -120,33 +121,4 @@ class SearchQuery extends ViewableData
}
}
/**
* Create one of these and pass as one of the values in filter or exclude to filter or exclude by a (possibly
* open ended) range
*/
class SearchQuery_Range
{
public $start = null;
public $end = null;
public function __construct($start = null, $end = null)
{
$this->start = $start;
$this->end = $end;
}
public function start($start)
{
$this->start = $start;
}
public function end($end)
{
$this->end = $end;
}
public function isfiltered()
{
return $this->start !== null || $this->end !== null;
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace SilverStripe\FullTextSearch\Search\Queries;
/**
* Create one of these and pass as one of the values in filter or exclude to filter or exclude by a (possibly
* open ended) range
*/
class SearchQuery_Range
{
public $start = null;
public $end = null;
public function __construct($start = null, $end = null)
{
$this->start = $start;
$this->end = $end;
}
public function start($start)
{
$this->start = $start;
}
public function end($end)
{
$this->end = $end;
}
public function isfiltered()
{
return $this->start !== null || $this->end !== null;
}
}

View File

@ -1,5 +1,8 @@
<?php
namespace SilverStripe\FullTextSearch\Search\Updaters;
use SilverStripe\ORM\DB;
/**
* This class is responsible for capturing changes to DataObjects and triggering index updates of the resulting dirty index
* items.
@ -12,6 +15,9 @@
*
* TODO: The way we bind in is awful hacky.
*/
use SilverStripe\Core\Object;
use SilverStripe\ORM\DataExtension;
class SearchUpdater extends Object
{
/**
@ -204,77 +210,5 @@ class SearchUpdater extends Object
}
}
class SearchUpdater_BindManipulationCaptureFilter implements RequestFilter
{
public function preRequest(SS_HTTPRequest $request, Session $session, DataModel $model)
{
SearchUpdater::bind_manipulation_capture();
}
public function postRequest(SS_HTTPRequest $request, SS_HTTPResponse $response, DataModel $model)
{
/* NOP */
}
}
/**
* Delete operations do not use database manipulations.
*
* If a delete has been requested, force a write on objects that should be
* indexed. This causes the object to be marked for deletion from the index.
*/
class SearchUpdater_ObjectHandler extends DataExtension
{
public function onAfterDelete()
{
// Calling delete() on empty objects does nothing
if (!$this->owner->ID) {
return;
}
// Force SearchUpdater to mark this record as dirty
$manipulation = array(
$this->owner->ClassName => array(
'fields' => array(),
'id' => $this->owner->ID,
'command' => 'update'
)
);
$this->owner->extend('augmentWrite', $manipulation);
SearchUpdater::handle_manipulation($manipulation);
}
/**
* Forces this object to trigger a re-index in the current state
*/
public function triggerReindex()
{
if (!$this->owner->ID) {
return;
}
$id = $this->owner->ID;
$class = $this->owner->ClassName;
$state = SearchVariant::current_state($class);
$base = ClassInfo::baseDataClass($class);
$key = "$id:$base:".serialize($state);
$statefulids = array(array(
'id' => $id,
'state' => $state
));
$writes = array(
$key => array(
'base' => $base,
'class' => $class,
'id' => $id,
'statefulids' => $statefulids,
'fields' => array()
)
);
SearchUpdater::process_writes($writes);
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace SilverStripe\FullTextSearch\Search\Updaters;
use SilverStripe\Control\RequestFilter;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\Session;
use SilverStripe\ORM\DataModel;
use SilverStripe\Control\HTTPResponse;
class SearchUpdater_BindManipulationCaptureFilter implements RequestFilter
{
public function preRequest(HTTPRequest $request, Session $session, DataModel $model)
{
SearchUpdater::bind_manipulation_capture();
}
public function postRequest(HTTPRequest $request, HTTPResponse $response, DataModel $model)
{
/* NOP */
}
}

View File

@ -0,0 +1,66 @@
<?php
namespace SilverStripe\FullTextSearch\Search\Updaters;
use SilverStripe\ORM\DataExtension;
/**
* Delete operations do not use database manipulations.
*
* If a delete has been requested, force a write on objects that should be
* indexed. This causes the object to be marked for deletion from the index.
*/
class SearchUpdater_ObjectHandler extends DataExtension
{
public function onAfterDelete()
{
// Calling delete() on empty objects does nothing
if (!$this->owner->ID) {
return;
}
// Force SearchUpdater to mark this record as dirty
$manipulation = array(
$this->owner->ClassName => array(
'fields' => array(),
'id' => $this->owner->ID,
'command' => 'update'
)
);
$this->owner->extend('augmentWrite', $manipulation);
SearchUpdater::handle_manipulation($manipulation);
}
/**
* Forces this object to trigger a re-index in the current state
*/
public function triggerReindex()
{
if (!$this->owner->ID) {
return;
}
$id = $this->owner->ID;
$class = $this->owner->ClassName;
$state = SearchVariant::current_state($class);
$base = ClassInfo::baseDataClass($class);
$key = "$id:$base:".serialize($state);
$statefulids = array(array(
'id' => $id,
'state' => $state
));
$writes = array(
$key => array(
'base' => $base,
'class' => $class,
'id' => $id,
'statefulids' => $statefulids,
'fields' => array()
)
);
SearchUpdater::process_writes($writes);
}
}

View File

@ -1,5 +1,6 @@
<?php
namespace SilverStripe\FullTextSearch\Search\Variants;
use SilverStripe\Core\ClassInfo;
/**
* A Search Variant handles decorators and other situations where the items to reindex or search through are modified
* from the default state - for instance, dealing with Versioned or Subsite
@ -252,31 +253,3 @@ abstract class SearchVariant
}
}
/**
* Internal utility class used to hold the state of the SearchVariant::with call
*/
class SearchVariant_Caller
{
protected $variants = null;
public function __construct($variants)
{
$this->variants = $variants;
}
public function call($method, &$a1=null, &$a2=null, &$a3=null, &$a4=null, &$a5=null, &$a6=null, &$a7=null)
{
$values = array();
foreach ($this->variants as $variant) {
if (method_exists($variant, $method)) {
$value = $variant->$method($a1, $a2, $a3, $a4, $a5, $a6, $a7);
if ($value !== null) {
$values[] = $value;
}
}
}
return $values;
}
}

View File

@ -1,4 +1,5 @@
<?php
namespace SilverStripe\FullTextSearch\Search\Variants;
class SearchVariantSiteTreeSubsitesPolyhome extends SearchVariant
{

View File

@ -1,5 +1,5 @@
<?php
namespace SilverStripe\FullTextSearch\Search\Variants;
class SearchVariantSubsites extends SearchVariant
{
public function appliesToEnvironment()

View File

@ -1,4 +1,5 @@
<?php
namespace SilverStripe\FullTextSearch\Search\Variants;
class SearchVariantVersioned extends SearchVariant
{

View File

@ -0,0 +1,38 @@
<?php
/**
* Created by PhpStorm.
* User: elliot
* Date: 21/04/17
* Time: 1:13 PM
*/
namespace SilverStripe\FullTextSearch\Search\Variants;
/**
* Internal utility class used to hold the state of the SearchVariant::with call
*/
class SearchVariant_Caller
{
protected $variants = null;
public function __construct($variants)
{
$this->variants = $variants;
}
public function call($method, &$a1=null, &$a2=null, &$a3=null, &$a4=null, &$a5=null, &$a6=null, &$a7=null)
{
$values = array();
foreach ($this->variants as $variant) {
if (method_exists($variant, $method)) {
$value = $variant->$method($a1, $a2, $a3, $a4, $a5, $a6, $a7);
if ($value !== null) {
$values[] = $value;
}
}
}
return $values;
}
}

View File

@ -1,9 +1,8 @@
<?php
namespace SilverStripe\FullTextSearch\Solr;
use Monolog\Formatter\LineFormatter;
use Monolog\Handler\StreamHandler;
use Monolog\Logger;
use Psr\Log\LoggerInterface;
use SilverStripe\Control\Director;
use SilverStripe\Core\Object;
class Solr
{
@ -149,295 +148,12 @@ class Solr
static $included = false;
if (!$included) {
set_include_path(get_include_path() . PATH_SEPARATOR . Director::baseFolder() . '/fulltextsearch/thirdparty/solr-php-client');
require_once('Apache/Solr/Service.php');
require_once('Apache/Solr/Document.php');
$solr_php_path = __DIR__. '/../..' . '/thirdparty/solr-php-client/';
set_include_path(get_include_path() . PATH_SEPARATOR . $solr_php_path);
require_once($solr_php_path . 'Apache/Solr/Service.php');
require_once($solr_php_path . 'Apache/Solr/Document.php');
$included = true;
}
}
}
/**
* Abstract class for build tasks
*/
class Solr_BuildTask extends BuildTask
{
protected $enabled = false;
/**
* Logger
*
* @var LoggerInterface
*/
protected $logger = null;
/**
* Get the current logger
*
* @return LoggerInterface
*/
public function getLogger()
{
return $this->logger;
}
/**
* Assign a new logger
*
* @param LoggerInterface $logger
*/
public function setLogger(LoggerInterface $logger)
{
$this->logger = $logger;
}
/**
* @return SearchLogFactory
*/
protected function getLoggerFactory()
{
return Injector::inst()->get('SearchLogFactory');
}
/**
* Setup task
*
* @param SS_HTTPReqest $request
*/
public function run($request)
{
$name = get_class($this);
$verbose = $request->getVar('verbose');
// Set new logger
$logger = $this
->getLoggerFactory()
->getOutputLogger($name, $verbose);
$this->setLogger($logger);
}
}
class Solr_Configure extends Solr_BuildTask
{
protected $enabled = true;
public function run($request)
{
parent::run($request);
// Find the IndexStore handler, which will handle uploading config files to Solr
$store = $this->getSolrConfigStore();
$indexes = Solr::get_indexes();
foreach ($indexes as $instance) {
try {
$this->updateIndex($instance, $store);
} catch (Exception $e) {
// We got an exception. Warn, but continue to next index.
$this
->getLogger()
->error("Failure: " . $e->getMessage());
}
}
}
/**
* Update the index on the given store
*
* @param SolrIndex $instance Instance
* @param SolrConfigStore $store
*/
protected function updateIndex($instance, $store)
{
$index = $instance->getIndexName();
$this->getLogger()->info("Configuring $index.");
// Upload the config files for this index
$this->getLogger()->info("Uploading configuration ...");
$instance->uploadConfig($store);
// Then tell Solr to use those config files
$service = Solr::service();
if ($service->coreIsActive($index)) {
$this->getLogger()->info("Reloading core ...");
$service->coreReload($index);
} else {
$this->getLogger()->info("Creating core ...");
$service->coreCreate($index, $store->instanceDir($index));
}
$this->getLogger()->info("Done");
}
/**
* Get config store
*
* @return SolrConfigStore
*/
protected function getSolrConfigStore()
{
$options = Solr::solr_options();
if (!isset($options['indexstore']) || !($indexstore = $options['indexstore'])) {
user_error('No index configuration for Solr provided', E_USER_ERROR);
}
// Find the IndexStore handler, which will handle uploading config files to Solr
$mode = $indexstore['mode'];
if ($mode == 'file') {
return new SolrConfigStore_File($indexstore);
} elseif ($mode == 'webdav') {
return new SolrConfigStore_WebDAV($indexstore);
} elseif (ClassInfo::exists($mode) && ClassInfo::classImplements($mode, 'SolrConfigStore')) {
return new $mode($indexstore);
} else {
user_error('Unknown Solr index mode '.$indexstore['mode'], E_USER_ERROR);
}
}
}
/**
* Task used for both initiating a new reindex, as well as for processing incremental batches
* within a reindex.
*
* When running a complete reindex you can provide any of the following
* - class (to limit to a single class)
* - verbose (optional)
*
* When running with a single batch, provide the following querystring arguments:
* - start
* - index
* - class
* - variantstate
* - verbose (optional)
*/
class Solr_Reindex extends Solr_BuildTask
{
protected $enabled = true;
/**
* Number of records to load and index per request
*
* @var int
* @config
*/
private static $recordsPerRequest = 200;
/**
* Get the reindex handler
*
* @return SolrReindexHandler
*/
protected function getHandler()
{
return Injector::inst()->get('SolrReindexHandler');
}
/**
* @param SS_HTTPRequest $request
*/
public function run($request)
{
parent::run($request);
// Reset state
$originalState = SearchVariant::current_state();
$this->doReindex($request);
SearchVariant::activate_state($originalState);
}
/**
* @param SS_HTTPRequest $request
*/
protected function doReindex($request)
{
$class = $request->getVar('class');
$index = $request->getVar('index');
//find the index classname by IndexName
// this is for when index names do not match the class name (this can be done by overloading getIndexName() on
// indexes
if ($index && !ClassInfo::exists($index)) {
foreach(ClassInfo::subclassesFor('SolrIndex') as $solrIndexClass) {
$reflection = new ReflectionClass($solrIndexClass);
//skip over abstract classes
if (!$reflection->isInstantiable()) {
continue;
}
//check the indexname matches the index passed to the request
if (!strcasecmp(singleton($solrIndexClass)->getIndexName(), $index)) {
//if we match, set the correct index name and move on
$index = $solrIndexClass;
break;
}
}
}
// Deprecated reindex mechanism
$start = $request->getVar('start');
if ($start !== null) {
// Run single batch directly
$indexInstance = singleton($index);
$state = json_decode($request->getVar('variantstate'), true);
$this->runFrom($indexInstance, $class, $start, $state);
return;
}
// Check if we are re-indexing a single group
// If not using queuedjobs, we need to invoke Solr_Reindex as a separate process
// Otherwise each group is processed via a SolrReindexGroupJob
$groups = $request->getVar('groups');
$handler = $this->getHandler();
if ($groups) {
// Run grouped batches (id % groups = group)
$group = $request->getVar('group');
$indexInstance = singleton($index);
$state = json_decode($request->getVar('variantstate'), true);
$handler->runGroup($this->getLogger(), $indexInstance, $state, $class, $groups, $group);
return;
}
// If run at the top level, delegate to appropriate handler
$self = get_class($this);
$handler->triggerReindex($this->getLogger(), $this->config()->recordsPerRequest, $self, $class);
}
/**
* @deprecated since version 2.0.0
*/
protected function runFrom($index, $class, $start, $variantstate)
{
DeprecationTest_Deprecation::notice('2.0.0', 'Solr_Reindex now uses a new grouping mechanism');
// Set time limit and state
increase_time_limit_to();
SearchVariant::activate_state($variantstate);
// Generate filtered list
$items = DataList::create($class)
->limit($this->config()->recordsPerRequest, $start);
// Add child filter
$classes = $index->getClasses();
$options = $classes[$class];
if (!$options['include_children']) {
$items = $items->filter('ClassName', $class);
}
// Process selected records in this class
$this->getLogger()->info("Adding $class");
foreach ($items->sort("ID") as $item) {
$this->getLogger()->debug($item->ID);
// See SearchUpdater_ObjectHandler::triggerReindex
$item->triggerReindex();
$item->destroy();
}
$this->getLogger()->info("Done");
}
}
}

View File

@ -1,134 +0,0 @@
<?php
/**
* Class SolrConfigStore
*
* The interface Solr_Configure uses to upload configuration files to Solr
*/
interface SolrConfigStore
{
/**
* Upload a file to Solr for index $index
* @param $index string - The name of an index (which is also used as the name of the Solr core for the index)
* @param $file string - A path to a file to upload. The base name of the file will be used on the remote side
* @return null
*/
public function uploadFile($index, $file);
/**
* Upload a file to Solr from a string for index $index
* @param $index string - The name of an index (which is also used as the name of the Solr core for the index)
* @param $filename string - The base name of the file to use on the remote side
* @param $strong string - The contents of the file
* @return null
*/
public function uploadString($index, $filename, $string);
/**
* Get the instanceDir to tell Solr to use for index $index
* @param $index string - The name of an index (which is also used as the name of the Solr core for the index)
*/
public function instanceDir($index);
}
/**
* Class SolrConfigStore_File
*
* A ConfigStore that uploads files to a Solr instance on a locally accessible filesystem
* by just using file copies
*/
class SolrConfigStore_File implements SolrConfigStore
{
public function __construct($config)
{
$this->local = $config['path'];
$this->remote = isset($config['remotepath']) ? $config['remotepath'] : $config['path'];
}
public function getTargetDir($index)
{
$targetDir = "{$this->local}/{$index}/conf";
if (!is_dir($targetDir)) {
$worked = @mkdir($targetDir, 0770, true);
if (!$worked) {
throw new RuntimeException(
sprintf('Failed creating target directory %s, please check permissions', $targetDir)
);
}
}
return $targetDir;
}
public function uploadFile($index, $file)
{
$targetDir = $this->getTargetDir($index);
copy($file, $targetDir.'/'.basename($file));
}
public function uploadString($index, $filename, $string)
{
$targetDir = $this->getTargetDir($index);
file_put_contents("$targetDir/$filename", $string);
}
public function instanceDir($index)
{
return $this->remote.'/'.$index;
}
}
/**
* Class SolrConfigStore_WebDAV
*
* A ConfigStore that uploads files to a Solr instance via a WebDAV server
*/
class SolrConfigStore_WebDAV implements SolrConfigStore
{
public function __construct($config)
{
$options = Solr::solr_options();
$this->url = implode('', array(
'http://',
isset($config['auth']) ? $config['auth'].'@' : '',
$options['host'].':'.(isset($config['port']) ? $config['port'] : $options['port']),
$config['path']
));
$this->remote = $config['remotepath'];
}
public function getTargetDir($index)
{
$indexdir = "{$this->url}/$index";
if (!WebDAV::exists($indexdir)) {
WebDAV::mkdir($indexdir);
}
$targetDir = "{$this->url}/$index/conf";
if (!WebDAV::exists($targetDir)) {
WebDAV::mkdir($targetDir);
}
return $targetDir;
}
public function uploadFile($index, $file)
{
$targetDir = $this->getTargetDir($index);
WebDAV::upload_from_file($file, $targetDir.'/'.basename($file));
}
public function uploadString($index, $filename, $string)
{
$targetDir = $this->getTargetDir($index);
WebDAV::upload_from_string($string, "$targetDir/$filename");
}
public function instanceDir($index)
{
return $this->remote ? "{$this->remote}/$index" : $index;
}
}

View File

@ -1,7 +1,10 @@
<?php
namespace SilverStripe\FullTextSearch\Solr;
Solr::include_client_api();
use SilverStripe\Control\Director;
use SilverStripe\FulltextSearch\Search\Indexes\SearchIndex;
abstract class SolrIndex extends SearchIndex
{
public static $fulltextTypeMap = array(

View File

@ -1,6 +1,10 @@
<?php
namespace SilverStripe\FullTextSearch\Solr\Reindex\Handlers;
use Psr\Log\LoggerInterface;
use SilverStripe\FullTextSearch\Solr\Solr;
use SilverStripe\FullTextSearch\Solr\SolrIndex;
/**
* Base class for re-indexing of solr content

View File

@ -1,6 +1,9 @@
<?php
namespace SilverStripe\FullTextSearch\Solr\Reindex\Handlers;
use Psr\Log\LoggerInterface;
use SilverStripe\FullTextSearch\Solr\SolrIndex;
/**
* Provides interface for queueing a solr reindex

View File

@ -1,6 +1,9 @@
<?php
namespace SilverStripe\FullTextSearch\Solr\Reindex\Handlers;
use Psr\Log\LoggerInterface;
use SilverStripe\FullTextSearch\Solr\SolrIndex;
/**
* Invokes an immediate reindex

View File

@ -1,5 +1,5 @@
<?php
namespace SilverStripe\FullTextSearch\Solr\Reindex\Handlers;
use Psr\Log\LoggerInterface;
if (!class_exists('MessageQueue')) {

View File

@ -1,6 +1,9 @@
<?php
namespace SilverStripe\FullTextSearch\Solr\Reindex\Handlers;
use Psr\Log\LoggerInterface;
use SilverStripe\FullTextSearch\Solr\SolrIndex;
if (!interface_exists('QueuedJob')) {
return;

View File

@ -1,5 +1,5 @@
<?php
namespace SilverStripe\FullTextSearch\Solr\Reindex\Jobs;
if (!interface_exists('QueuedJob')) {
return;
}

View File

@ -1,5 +1,5 @@
<?php
namespace SilverStripe\FullTextSearch\Solr\Reindex\Jobs;
if (!interface_exists('QueuedJob')) {
return;
}

View File

@ -1,5 +1,5 @@
<?php
namespace SilverStripe\FullTextSearch\Solr\Reindex\Jobs;
use Monolog\Logger;
use Psr\Log\LoggerInterface;

View File

@ -1,8 +1,5 @@
<?php
class Solr3Service_Core extends SolrService_Core
{
}
namespace SilverStripe\FullTextSearch\Solr\Services;
class Solr3Service extends SolrService
{

View File

@ -0,0 +1,8 @@
<?php
namespace SilverStripe\FullTextSearch\Solr\Services;
class Solr3Service_Core extends SolrService_Core
{
}

View File

@ -0,0 +1,7 @@
<?php
namespace SilverStripe\FullTextSearch\Solr\Services;
class Solr4Service extends SolrService
{
private static $core_class = 'Solr4Service_Core';
}

View File

@ -1,5 +1,7 @@
<?php
namespace SilverStripe\FullTextSearch\Solr\Services;
class Solr4Service_Core extends SolrService_Core
{
/**
@ -18,12 +20,12 @@ class Solr4Service_Core extends SolrService_Core
$rawPost = '<commit expungeDeletes="' . $expungeValue . '" waitSearcher="' . $searcherValue . '" />';
return $this->_sendRawPost($this->_updateUrl, $rawPost, $timeout);
}
/**
* @inheritdoc
* @inheritdoc
* @see Solr4Service_Core::addDocuments
*/
public function addDocument(Apache_Solr_Document $document, $allowDups = false,
public function addDocument(\Apache_Solr_Document $document, $allowDups = false,
$overwritePending = true, $overwriteCommitted = true, $commitWithin = 0
) {
return $this->addDocuments(array($document), $allowDups, $overwritePending, $overwriteCommitted, $commitWithin);
@ -51,8 +53,3 @@ class Solr4Service_Core extends SolrService_Core
return $this->add($rawPost);
}
}
class Solr4Service extends SolrService
{
private static $core_class = 'Solr4Service_Core';
}

View File

@ -1,14 +1,8 @@
<?php
namespace SilverStripe\FullTextSearch\Solr\Services;
use SilverStripe\Core\Config\Config;
use SilverStripe\FullTextSearch\Solr\Solr;
Solr::include_client_api();
/**
* The API for accessing a specific core of a Solr server. Exactly the same as Apache_Solr_Service for now.
*/
class SolrService_Core extends Apache_Solr_Service
{
}
/**
* The API for accessing the primary Solr installation, which includes both SolrService_Core,
* plus extra methods for interrogating, creating, reloading and getting SolrService_Core instances

View File

@ -0,0 +1,18 @@
<?php
/**
* Created by PhpStorm.
* User: elliot
* Date: 21/04/17
* Time: 12:45 PM
*/
namespace SilverStripe\FullTextSearch\Solr\Services;
use \Apache_Solr_Service;
/**
* The API for accessing a specific core of a Solr server. Exactly the same as Apache_Solr_Service for now.
*/
class SolrService_Core extends Apache_Solr_Service
{
}

View File

@ -0,0 +1,36 @@
<?php
namespace SilverStripe\FullTextSearch\Solr\Stores;
/**
* Class SolrConfigStore
*
* The interface Solr_Configure uses to upload configuration files to Solr
*/
interface SolrConfigStore
{
/**
* Upload a file to Solr for index $index
* @param $index string - The name of an index (which is also used as the name of the Solr core for the index)
* @param $file string - A path to a file to upload. The base name of the file will be used on the remote side
* @return null
*/
public function uploadFile($index, $file);
/**
* Upload a file to Solr from a string for index $index
* @param $index string - The name of an index (which is also used as the name of the Solr core for the index)
* @param $filename string - The base name of the file to use on the remote side
* @param $strong string - The contents of the file
* @return null
*/
public function uploadString($index, $filename, $string);
/**
* Get the instanceDir to tell Solr to use for index $index
* @param $index string - The name of an index (which is also used as the name of the Solr core for the index)
*/
public function instanceDir($index);
}

View File

@ -0,0 +1,53 @@
<?php
namespace SilverStripe\FullTextSearch\Solr\Stores;
/**
* Class SolrConfigStore_File
*
* A ConfigStore that uploads files to a Solr instance on a locally accessible filesystem
* by just using file copies
*/
class SolrConfigStore_File implements SolrConfigStore
{
public function __construct($config)
{
$this->local = $config['path'];
$this->remote = isset($config['remotepath']) ? $config['remotepath'] : $config['path'];
}
public function getTargetDir($index)
{
$targetDir = "{$this->local}/{$index}/conf";
if (!is_dir($targetDir)) {
$worked = @mkdir($targetDir, 0770, true);
if (!$worked) {
throw new RuntimeException(
sprintf('Failed creating target directory %s, please check permissions', $targetDir)
);
}
}
return $targetDir;
}
public function uploadFile($index, $file)
{
$targetDir = $this->getTargetDir($index);
copy($file, $targetDir.'/'.basename($file));
}
public function uploadString($index, $filename, $string)
{
$targetDir = $this->getTargetDir($index);
file_put_contents("$targetDir/$filename", $string);
}
public function instanceDir($index)
{
return $this->remote.'/'.$index;
}
}

View File

@ -0,0 +1,56 @@
<?php
namespace SilverStripe\FullTextSearch\Solr\Stores;
/**
* Class SolrConfigStore_WebDAV
*
* A ConfigStore that uploads files to a Solr instance via a WebDAV server
*/
class SolrConfigStore_WebDAV implements SolrConfigStore
{
public function __construct($config)
{
$options = Solr::solr_options();
$this->url = implode('', array(
'http://',
isset($config['auth']) ? $config['auth'].'@' : '',
$options['host'].':'.(isset($config['port']) ? $config['port'] : $options['port']),
$config['path']
));
$this->remote = $config['remotepath'];
}
public function getTargetDir($index)
{
$indexdir = "{$this->url}/$index";
if (!WebDAV::exists($indexdir)) {
WebDAV::mkdir($indexdir);
}
$targetDir = "{$this->url}/$index/conf";
if (!WebDAV::exists($targetDir)) {
WebDAV::mkdir($targetDir);
}
return $targetDir;
}
public function uploadFile($index, $file)
{
$targetDir = $this->getTargetDir($index);
WebDAV::upload_from_file($file, $targetDir.'/'.basename($file));
}
public function uploadString($index, $filename, $string)
{
$targetDir = $this->getTargetDir($index);
WebDAV::upload_from_string($string, "$targetDir/$filename");
}
public function instanceDir($index)
{
return $this->remote ? "{$this->remote}/$index" : $index;
}
}

View File

@ -0,0 +1,63 @@
<?php
namespace SilverStripe\FullTextSearch\Solr\Tasks;
use SilverStripe\Dev\BuildTask;
/**
* Abstract class for build tasks
*/
class Solr_BuildTask extends BuildTask
{
protected $enabled = false;
/**
* Logger
*
* @var LoggerInterface
*/
protected $logger = null;
/**
* Get the current logger
*
* @return LoggerInterface
*/
public function getLogger()
{
return Injector::inst()->get('Logger');
}
/**
* Assign a new logger
*
* @param LoggerInterface $logger
*/
public function setLogger(LoggerInterface $logger)
{
$this->logger = $logger;
}
/**
* @return SearchLogFactory
*/
protected function getLoggerFactory()
{
// return Injector::inst()->get('SearchLogFactory');
}
/**
* Setup task
*
* @param SS_HTTPReqest $request
*/
public function run($request)
{
$name = get_class($this);
$verbose = $request->getVar('verbose');
// Set new logger
$logger = $this
->getLoggerFactory();
//@todo: Cannot instantiate interface SearchLogFactory
// ->getOutputLogger($name, $verbose);
$this->setLogger($logger);
}
}

View File

@ -0,0 +1,83 @@
<?php
namespace SilverStripe\FullTextSearch\Solr\Tasks;
class Solr_Configure extends Solr_BuildTask
{
private static $segment = 'Solr_Configure';
protected $enabled = true;
public function run($request)
{
parent::run($request);
// Find the IndexStore handler, which will handle uploading config files to Solr
$store = $this->getSolrConfigStore();
$indexes = Solr::get_indexes();
foreach ($indexes as $instance) {
try {
$this->updateIndex($instance, $store);
} catch (Exception $e) {
// We got an exception. Warn, but continue to next index.
$this
->getLogger()
->error("Failure: " . $e->getMessage());
}
}
}
/**
* Update the index on the given store
*
* @param SolrIndex $instance Instance
* @param SolrConfigStore $store
*/
protected function updateIndex($instance, $store)
{
$index = $instance->getIndexName();
$this->getLogger()->addInfo("Configuring $index.");
// Upload the config files for this index
$this->getLogger()->addInfo("Uploading configuration ...");
$instance->uploadConfig($store);
// Then tell Solr to use those config files
$service = Solr::service();
if ($service->coreIsActive($index)) {
$this->getLogger()->addInfo("Reloading core ...");
$service->coreReload($index);
} else {
$this->getLogger()->addInfo("Creating core ...");
$service->coreCreate($index, $store->instanceDir($index));
}
$this->getLogger()->addInfo("Done");
}
/**
* Get config store
*
* @return SolrConfigStore
*/
protected function getSolrConfigStore()
{
$options = Solr::solr_options();
if (!isset($options['indexstore']) || !($indexstore = $options['indexstore'])) {
throw new Exception('No index configuration for Solr provided', E_USER_ERROR);
}
// Find the IndexStore handler, which will handle uploading config files to Solr
$mode = $indexstore['mode'];
if ($mode == 'file') {
return new SolrConfigStore_File($indexstore);
} elseif ($mode == 'webdav') {
return new SolrConfigStore_WebDAV($indexstore);
} elseif (ClassInfo::exists($mode) && ClassInfo::classImplements($mode, 'SolrConfigStore')) {
return new $mode($indexstore);
} else {
user_error('Unknown Solr index mode '.$indexstore['mode'], E_USER_ERROR);
}
}
}

View File

@ -0,0 +1,150 @@
<?php
namespace SilverStripe\FullTextSearch\Solr\Tasks;
/**
* Task used for both initiating a new reindex, as well as for processing incremental batches
* within a reindex.
*
* When running a complete reindex you can provide any of the following
* - class (to limit to a single class)
* - verbose (optional)
*
* When running with a single batch, provide the following querystring arguments:
* - start
* - index
* - class
* - variantstate
* - verbose (optional)
*/
class Solr_Reindex extends Solr_BuildTask
{
private static $segment = 'Solr_Reindex';
protected $enabled = true;
/**
* Number of records to load and index per request
*
* @var int
* @config
*/
private static $recordsPerRequest = 200;
/**
* Get the reindex handler
*
* @return SolrReindexHandler
*/
protected function getHandler()
{
//@todo: this needs to determine the best class from a Factory implementation
//@todo: it was 'SolrReindexHandler' but that doesn't work on 4.0
return Injector::inst()->get('SolrReindexImmediateHandler');
}
/**
* @param SS_HTTPRequest $request
*/
public function run($request)
{
parent::run($request);
// Reset state
$originalState = SearchVariant::current_state();
$this->doReindex($request);
SearchVariant::activate_state($originalState);
}
/**
* @param SS_HTTPRequest $request
*/
protected function doReindex($request)
{
$class = $request->getVar('class');
$index = $request->getVar('index');
//find the index classname by IndexName
// this is for when index names do not match the class name (this can be done by overloading getIndexName() on
// indexes
if ($index && !ClassInfo::exists($index)) {
foreach(ClassInfo::subclassesFor('SolrIndex') as $solrIndexClass) {
$reflection = new ReflectionClass($solrIndexClass);
//skip over abstract classes
if (!$reflection->isInstantiable()) {
continue;
}
//check the indexname matches the index passed to the request
if (!strcasecmp(singleton($solrIndexClass)->getIndexName(), $index)) {
//if we match, set the correct index name and move on
$index = $solrIndexClass;
break;
}
}
}
// Deprecated reindex mechanism
$start = $request->getVar('start');
if ($start !== null) {
// Run single batch directly
$indexInstance = singleton($index);
$state = json_decode($request->getVar('variantstate'), true);
$this->runFrom($indexInstance, $class, $start, $state);
return;
}
// Check if we are re-indexing a single group
// If not using queuedjobs, we need to invoke Solr_Reindex as a separate process
// Otherwise each group is processed via a SolrReindexGroupJob
$groups = $request->getVar('groups');
$handler = $this->getHandler();
if ($groups) {
// Run grouped batches (id % groups = group)
$group = $request->getVar('group');
$indexInstance = singleton($index);
$state = json_decode($request->getVar('variantstate'), true);
$handler->runGroup($this->getLogger(), $indexInstance, $state, $class, $groups, $group);
return;
}
// If run at the top level, delegate to appropriate handler
$self = get_class($this);
$handler->triggerReindex($this->getLogger(), $this->config()->recordsPerRequest, $self, $class);
}
/**
* @deprecated since version 2.0.0
*/
protected function runFrom($index, $class, $start, $variantstate)
{
DeprecationTest_Deprecation::notice('2.0.0', 'Solr_Reindex now uses a new grouping mechanism');
// Set time limit and state
increase_time_limit_to();
SearchVariant::activate_state($variantstate);
// Generate filtered list
$items = DataList::create($class)
->limit($this->config()->recordsPerRequest, $start);
// Add child filter
$classes = $index->getClasses();
$options = $classes[$class];
if (!$options['include_children']) {
$items = $items->filter('ClassName', $class);
}
// Process selected records in this class
$this->getLogger()->info("Adding $class");
foreach ($items->sort("ID") as $item) {
$this->getLogger()->debug($item->ID);
// See SearchUpdater_ObjectHandler::triggerReindex
$item->triggerReindex();
$item->destroy();
}
$this->getLogger()->info("Done");
}
}

View File

@ -1,5 +1,6 @@
<?php
namespace SilverStripe\FullTextSearch\Utils;
use Iterator;
class CombinationsArrayIterator implements Iterator
{
protected $arrays;

View File

@ -1,5 +1,6 @@
<?php
namespace SilverStripe\FullTextSearch\Utils;
use Iterator;
class MultipleArrayIterator implements Iterator
{
protected $arrays;

View File

@ -1,5 +1,5 @@
<?php
namespace SilverStripe\FullTextSearch\Utils;
class WebDAV
{
public static function curl_init($url, $method)

View File

@ -1,5 +1,5 @@
<?php
namespace SilverStripe\FullTextSearch\Utils\Logging;
use Monolog\Formatter\FormatterInterface;
use Monolog\Formatter\LineFormatter;
use Monolog\Handler\HandlerInterface;

View File

@ -1,5 +1,5 @@
<?php
namespace SilverStripe\FullTextSearch\Utils\Logging;
use Monolog\Handler\AbstractProcessingHandler;
use Monolog\Logger;

View File

@ -1,5 +1,5 @@
<?php
namespace SilverStripe\FullTextSearch\Utils\Logging;
use Psr\Log;
interface SearchLogFactory

View File

@ -20,11 +20,11 @@
}
],
"require": {
"silverstripe/framework": "~3.1",
"silverstripe/framework": "4.0.x-dev",
"monolog/monolog": "~1.15"
},
"require-dev": {
"silverstripe/cms": "~3.1",
"silverstripe/cms": "4.0.x-dev",
"hafriedlander/silverstripe-phockito": "*"
},
"extra": {

View File

@ -1,40 +1,9 @@
<?php
namespace SilverStripe\FullTextSearch\Tests;
class BatchedProcessorTest_Object extends SiteTree implements TestOnly
{
private static $db = array(
'TestText' => 'Varchar'
);
}
class BatchedProcessorTest_Index extends SearchIndex_Recording implements TestOnly
{
public function init()
{
$this->addClass('BatchedProcessorTest_Object');
$this->addFilterField('TestText');
}
}
class BatchedProcessor_QueuedJobService
{
protected $jobs = array();
public function queueJob(QueuedJob $job, $startAfter = null, $userId = null, $queueName = null)
{
$this->jobs[] = array(
'job' => $job,
'startAfter' => $startAfter
);
return $job;
}
public function getJobs()
{
return $this->jobs;
}
}
use SilverStripe\Dev\SapphireTest;
use SilverStripe\FullTextSearch\Search\FullTextSearch;
/**
* Tests {@see SearchUpdateQueuedJobProcessor}

View File

@ -0,0 +1,15 @@
<?php
namespace SilverStripe\FullTextSearch\Tests\BatchedProcessorTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\FullTextSearch\Search\Indexes\SearchIndex_Recording;
class BatchedProcessorTest_Index extends SearchIndex_Recording implements TestOnly
{
public function init()
{
$this->addClass('BatchedProcessorTest_Object');
$this->addFilterField('TestText');
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace SilverStripe\FullTextSearch\Tests\BatchedProcessorTest;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Dev\TestOnly;
class BatchedProcessorTest_Object extends SiteTree implements TestOnly
{
private static $db = array(
'TestText' => 'Varchar'
);
}

View File

@ -0,0 +1,22 @@
<?php
namespace SilverStripe\FullTextSearch\Tests\BatchedProcessorTest;
class BatchedProcessor_QueuedJobService
{
protected $jobs = array();
public function queueJob(QueuedJob $job, $startAfter = null, $userId = null, $queueName = null)
{
$this->jobs[] = array(
'job' => $job,
'startAfter' => $startAfter
);
return $job;
}
public function getJobs()
{
return $this->jobs;
}
}

View File

@ -1,5 +1,10 @@
<?php
use SilverStripe\ORM\DataObject;
use SilverStripe\FullTextSearch\Search\Indexes\SearchIndex_Recording;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\FullTextSearch\Search\Updaters\SearchUpdater;
class SearchUpdaterTest_Container extends DataObject
{
private static $db = array(

View File

@ -1,5 +1,9 @@
<?php
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\FullTextSearch\Search\Indexes\SearchIndex_Recording;
use SilverStripe\Dev\SapphireTest;
class SearchVariantSiteTreeSubsitesPolyhomeTest_Item extends SiteTree
{
// TODO: Currently theres a failure if you addClass a non-table class

View File

@ -1,5 +1,10 @@
<?php
use SilverStripe\Dev\SapphireTest;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Dev\TestOnly;
use SilverStripe\FullTextSearch\Search\Indexes\SearchIndex_Recording;
class SearchVariantVersionedTest extends SapphireTest
{
/**

View File

@ -1,5 +1,10 @@
<?php
namespace SilverStripe\FullTextSearch\Tests;
use SilverStripe\FullTextSearch\Tests\Solr4ServiceTest\Solr4ServiceTest_RecordingService;
use SilverStripe\Dev\SapphireTest;
/**
* Test solr 4.0 compatibility
*/
@ -16,7 +21,7 @@ class Solr4ServiceTest extends SapphireTest
protected function getMockDocument($id)
{
$document = new Apache_Solr_Document();
$document = new \Apache_Solr_Document();
$document->setField('id', $id);
$document->setField('title', "Item $id");
return $document;
@ -58,16 +63,3 @@ class Solr4ServiceTest extends SapphireTest
);
}
}
class Solr4ServiceTest_RecordingService extends Solr4Service_Core
{
protected function _sendRawPost($url, $rawPost, $timeout = false, $contentType = 'text/xml; charset=UTF-8')
{
return $rawPost;
}
protected function _sendRawGet($url, $timeout = false)
{
return $url;
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace SilverStripe\FullTextSearch\Tests\Solr4ServiceTest;
use SilverStripe\FullTextSearch\Solr\Services\Solr4Service_Core;
class Solr4ServiceTest_RecordingService extends Solr4Service_Core
{
protected function _sendRawPost($url, $rawPost, $timeout = false, $contentType = 'text/xml; charset=UTF-8')
{
return $rawPost;
}
protected function _sendRawGet($url, $timeout = false)
{
return $url;
}
}

View File

@ -1,5 +1,8 @@
<?php
use SilverStripe\Dev\SapphireTest;
use SilverStripe\FullTextSearch\Tests\SolrIndexSubsitesTest\SolrIndexSubsitesTest_Index;
if (class_exists('Phockito')) {
Phockito::include_hamcrest(false);
}
@ -9,7 +12,7 @@ if (class_exists('Phockito')) {
*/
class SolrIndexSubsitesTest extends SapphireTest {
public static $fixture_file = 'SolrIndexSubsitesTest.yml';
public static $fixture_file = 'SolrIndexSubsitesTest/SolrIndexSubsitesTest.yml';
/**
* @var SolrIndexSubsitesTest_Index
@ -243,13 +246,3 @@ class SolrIndexSubsitesTest extends SapphireTest {
}
}
class SolrIndexSubsitesTest_Index extends SolrIndex
{
public function init()
{
$this->addClass('File');
$this->addClass('SiteTree');
$this->addAllFulltextFields();
}
}

View File

@ -0,0 +1,15 @@
<?php
namespace SilverStripe\FullTextSearch\Tests\SolrIndexSubsitesTest;
use SilverStripe\FullTextSearch\Solr\SolrIndex;
class SolrIndexSubsitesTest_Index extends SolrIndex
{
public function init()
{
$this->addClass('File');
$this->addClass('SiteTree');
$this->addAllFulltextFields();
}
}

View File

@ -1,4 +1,11 @@
<?php
use SilverStripe\Dev\SapphireTest;
use SilverStripe\FullTextSearch\Tests\SolrIndexTest\SolrIndexTest_FakeIndex;
use SilverStripe\FullTextSearch\Tests\SolrIndexTest\SolrIndexTest_FakeIndex2;
use SilverStripe\FullTextSearch\Tests\SolrIndexTest\SolrIndexTest_BoostedIndex;
class SolrIndexTest extends SapphireTest
{
public function setUpOnce()
@ -308,54 +315,3 @@ class SolrIndexTest extends SapphireTest
);
}
}
class SolrIndexTest_FakeIndex extends SolrIndex
{
public function init()
{
$this->addClass('SearchUpdaterTest_Container');
$this->addFilterField('Field1');
$this->addFilterField('MyDate', 'Date');
$this->addFilterField('HasOneObject.Field1');
$this->addFilterField('HasManyObjects.Field1');
$this->addFilterField('ManyManyObjects.Field1');
}
}
class SolrIndexTest_FakeIndex2 extends SolrIndex
{
protected function getStoredDefault()
{
// Override isDev defaulting to stored
return 'false';
}
public function init()
{
$this->addClass('SearchUpdaterTest_Container');
$this->addFilterField('MyDate', 'Date');
$this->addFilterField('HasOneObject.Field1');
$this->addFilterField('HasManyObjects.Field1');
$this->addFilterField('ManyManyObjects.Field1');
}
}
class SolrIndexTest_BoostedIndex extends SolrIndex
{
protected function getStoredDefault()
{
// Override isDev defaulting to stored
return 'false';
}
public function init()
{
$this->addClass('SearchUpdaterTest_Container');
$this->addAllFulltextFields();
$this->setFieldBoosting('SearchUpdaterTest_Container_Field1', 1.5);
$this->addBoostedField('Field2', null, array(), 2.1);
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace SilverStripe\FullTextSearch\Tests\SolrIndexTest;
use SilverStripe\FullTextSearch\Solr\SolrIndex;
class SolrIndexTest_BoostedIndex extends SolrIndex
{
protected function getStoredDefault()
{
// Override isDev defaulting to stored
return 'false';
}
public function init()
{
$this->addClass('SearchUpdaterTest_Container');
$this->addAllFulltextFields();
$this->setFieldBoosting('SearchUpdaterTest_Container_Field1', 1.5);
$this->addBoostedField('Field2', null, array(), 2.1);
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace SilverStripe\FullTextSearch\Tests\SolrIndexTest;
use SilverStripe\FullTextSearch\Solr\SolrIndex;
class SolrIndexTest_FakeIndex extends SolrIndex
{
public function init()
{
$this->addClass('SearchUpdaterTest_Container');
$this->addFilterField('Field1');
$this->addFilterField('MyDate', 'Date');
$this->addFilterField('HasOneObject.Field1');
$this->addFilterField('HasManyObjects.Field1');
$this->addFilterField('ManyManyObjects.Field1');
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace SilverStripe\FullTextSearch\Tests\SolrIndexTest;
use SilverStripe\FullTextSearch\Solr\SolrIndex;
class SolrIndexTest_FakeIndex2 extends SolrIndex
{
protected function getStoredDefault()
{
// Override isDev defaulting to stored
return 'false';
}
public function init()
{
$this->addClass('SearchUpdaterTest_Container');
$this->addFilterField('MyDate', 'Date');
$this->addFilterField('HasOneObject.Field1');
$this->addFilterField('HasManyObjects.Field1');
$this->addFilterField('ManyManyObjects.Field1');
}
}

View File

@ -1,5 +1,11 @@
<?php
use SilverStripe\Dev\SapphireTest;
use SilverStripe\FullTextSearch\Tests\SolrVersionedTest\SolrDocumentMatcher;
use SilverStripe\FullTextSearch\Tests\SolrVersionedTest\SolrIndexVersionedTest_Object;
use SilverStripe\FullTextSearch\Tests\SolrVersionedTest\SolrVersionedTest_Index;
use SilverStripe\Versioned\Versioned;
if (class_exists('Phockito')) {
Phockito::include_hamcrest(false);
}
@ -169,65 +175,3 @@ class SolrIndexVersionedTest extends SapphireTest
->deleteById($this->getExpectedDocumentId($id, 'Live'));
}
}
class SolrVersionedTest_Index extends SolrIndex
{
public function init()
{
$this->addClass('SearchVariantVersionedTest_Item');
$this->addClass('SolrIndexVersionedTest_Object');
$this->addFilterField('TestText');
$this->addFulltextField('Content');
}
}
/**
* Non-sitetree versioned dataobject
*/
class SolrIndexVersionedTest_Object extends DataObject implements TestOnly {
private static $extensions = array(
'Versioned'
);
private static $db = array(
'Title' => 'Varchar',
'Content' => 'Text',
'TestText' => 'Varchar',
);
}
if (!class_exists('Phockito')) {
return;
}
class SolrDocumentMatcher extends Hamcrest_BaseMatcher
{
protected $properties;
public function __construct($properties)
{
$this->properties = $properties;
}
public function describeTo(\Hamcrest_Description $description)
{
$description->appendText('Apache_Solr_Document with properties '.var_export($this->properties, true));
}
public function matches($item)
{
if (! ($item instanceof Apache_Solr_Document)) {
return false;
}
foreach ($this->properties as $key => $value) {
if ($item->{$key} != $value) {
return false;
}
}
return true;
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace SilverStripe\FullTextSearch\Tests\SolrVersionedTest;
if (!class_exists('Phockito')) {
return;
}
Phockito::include_hamcrest(false);
class SolrDocumentMatcher extends Hamcrest_BaseMatcher
{
protected $properties;
public function __construct($properties)
{
$this->properties = $properties;
}
public function describeTo(\Hamcrest_Description $description)
{
$description->appendText('Apache_Solr_Document with properties '.var_export($this->properties, true));
}
public function matches($item)
{
if (! ($item instanceof Apache_Solr_Document)) {
return false;
}
foreach ($this->properties as $key => $value) {
if ($item->{$key} != $value) {
return false;
}
}
return true;
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace SilverStripe\FullTextSearch\Tests\SolrVersionedTest;
use SilverStripe\ORM\DataObject;
use SilverStripe\Dev\TestOnly;
use SilverStripe\Versioned\Versioned;
/**
* Non-sitetree versioned dataobject
*/
class SolrIndexVersionedTest_Object extends DataObject implements TestOnly {
private static $extensions = [
Versioned::class
];
private static $db = [
'Title' => 'Varchar',
'Content' => 'Text',
'TestText' => 'Varchar',
];
}

View File

@ -0,0 +1,16 @@
<?php
namespace SilverStripe\FullTextSearch\Tests\SolrVersionedTest;
use SilverStripe\FullTextSearch\Solr\SolrIndex;
class SolrVersionedTest_Index extends SolrIndex
{
public function init()
{
$this->addClass('SearchVariantVersionedTest_Item');
$this->addClass('SolrIndexVersionedTest_Object');
$this->addFilterField('TestText');
$this->addFulltextField('Content');
}
}

View File

@ -1,5 +1,10 @@
<?php
use SilverStripe\Dev\SapphireTest;
use SilverStripe\FullTextSearch\Search\FullTextSearch;
use SilverStripe\FullTextSearch\Tests\SolrReindexTest\SolrReindexTest_Variant;
/**
* Additional tests of solr reindexing processes when run with queuedjobs
*/

View File

@ -1,9 +1,9 @@
<?php
use Monolog\Handler\AbstractProcessingHandler;
use Monolog\Handler\HandlerInterface;
use Monolog\Logger;
use Psr\Log\LoggerInterface;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\FullTextSearch\Search\FullTextSearch;
use SilverStripe\FullTextSearch\Tests\SolrReindexTest\SolrReindexTest_Variant;
if (class_exists('Phockito')) {
Phockito::include_hamcrest(false);
@ -283,280 +283,3 @@ class SolrReindexTest extends SapphireTest
);
}
}
/**
* Provides a wrapper for testing SolrReindexBase
*/
class SolrReindexTest_TestHandler extends SolrReindexBase
{
public function processGroup(
LoggerInterface $logger, SolrIndex $indexInstance, $state, $class, $groups, $group, $taskName
) {
$indexName = $indexInstance->getIndexName();
$stateName = json_encode($state);
$logger->info("Called processGroup with {$indexName}, {$stateName}, {$class}, group {$group} of {$groups}");
}
public function triggerReindex(LoggerInterface $logger, $batchSize, $taskName, $classes = null)
{
$logger->info("Called triggerReindex");
}
}
class SolrReindexTest_Index extends SolrIndex implements TestOnly
{
public function init()
{
$this->addClass('SolrReindexTest_Item');
$this->addAllFulltextFields();
}
}
/**
* Does not have any variant extensions
*/
class SolrReindexTest_Item extends DataObject implements TestOnly
{
private static $extensions = array(
'SolrReindexTest_ItemExtension'
);
private static $db = array(
'Title' => 'Varchar(255)',
'Variant' => 'Int(0)'
);
}
/**
* Select only records in the current variant
*/
class SolrReindexTest_ItemExtension extends DataExtension implements TestOnly
{
/**
* Filter records on the current variant
*
* @param SQLQuery $query
* @param DataQuery $dataQuery
*/
public function augmentSQL(SQLQuery &$query, DataQuery &$dataQuery = null)
{
$variant = SolrReindexTest_Variant::get_current();
if ($variant !== null && !$query->filtersOnID()) {
$sqlVariant = Convert::raw2sql($variant);
$query->addWhere("\"Variant\" = '{$sqlVariant}'");
}
}
}
/**
* Dummy variant that selects items with field Varient matching the current value
*
* Variant states are 0 and 1, or null if disabled
*/
class SolrReindexTest_Variant extends SearchVariant implements TestOnly
{
/**
* Value of this variant (either null, 0, or 1)
*
* @var int|null
*/
protected static $current = null;
/**
* Activate this variant
*/
public static function enable()
{
self::disable();
self::$current = 0;
self::$variants = array(
'SolrReindexTest_Variant' => singleton('SolrReindexTest_Variant')
);
}
/**
* Disable this variant and reset
*/
public static function disable()
{
self::$current = null;
self::$variants = null;
self::$class_variants = array();
self::$call_instances = array();
}
public function activateState($state)
{
self::set_current($state);
}
/**
* Set the current variant to the given state
*
* @param int $current 0, 1, 2, or null (disabled)
*/
public static function set_current($current)
{
self::$current = $current;
}
/**
* Get the current state
*
* @return string|null
*/
public static function get_current()
{
// Always use string values for states for consistent json_encode value
if (isset(self::$current)) {
return (string)self::$current;
}
}
public function alterDefinition($class, $index)
{
$self = get_class($this);
$this->addFilterField($index, '_testvariant', array(
'name' => '_testvariant',
'field' => '_testvariant',
'fullfield' => '_testvariant',
'base' => ClassInfo::baseDataClass($class),
'origin' => $class,
'type' => 'Int',
'lookup_chain' => array(array('call' => 'variant', 'variant' => $self, 'method' => 'currentState'))
));
}
public function alterQuery($query, $index)
{
// I guess just calling it _testvariant is ok?
$query->filter('_testvariant', $this->currentState());
}
public function appliesTo($class, $includeSubclasses)
{
return $class === 'SolrReindexTest_Item' ||
($includeSubclasses && is_subclass_of($class, 'SolrReindexTest_Item', true));
}
public function appliesToEnvironment()
{
// Set to null to disable
return self::$current !== null;
}
public function currentState()
{
return self::get_current();
}
public function reindexStates()
{
// Always use string values for states for consistent json_encode value
return array('0', '1', '2');
}
}
/**
* Test logger for recording messages
*/
class SolrReindexTest_RecordingLogger extends Logger implements TestOnly
{
/**
* @var SolrReindexTest_Handler
*/
protected $testHandler = null;
public function __construct($name = 'testlogger', array $handlers = array(), array $processors = array())
{
parent::__construct($name, $handlers, $processors);
$this->testHandler = new SolrReindexTest_Handler();
$this->pushHandler($this->testHandler);
}
/**
* @return array
*/
public function getMessages()
{
return $this->testHandler->getMessages();
}
/**
* Clear all messages
*/
public function clear()
{
$this->testHandler->clear();
}
/**
* Get messages with the given filter
*
* @param string $containing
* @return array Filtered array
*/
public function filterMessages($containing)
{
return array_values(array_filter(
$this->getMessages(),
function ($content) use ($containing) {
return stripos($content, $containing) !== false;
}
));
}
/**
* Count all messages containing the given substring
*
* @param string $containing Message to filter by
* @return int
*/
public function countMessages($containing = null)
{
if ($containing) {
$messages = $this->filterMessages($containing);
} else {
$messages = $this->getMessages();
}
return count($messages);
}
}
/**
* Logger for recording messages for later retrieval
*/
class SolrReindexTest_Handler extends AbstractProcessingHandler implements TestOnly
{
/**
* Messages
*
* @var array
*/
protected $messages = array();
/**
* Get all messages
*
* @return array
*/
public function getMessages()
{
return $this->messages;
}
public function clear()
{
$this->messages = array();
}
protected function write(array $record)
{
$this->messages[] = $record['message'];
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace SilverStripe\FullTextSearch\Tests\SolrReindexTest;
use Monolog\Handler\AbstractProcessingHandler;
use SilverStripe\Dev\TestOnly;
/**
* Logger for recording messages for later retrieval
*/
class SolrReindexTest_Handler extends AbstractProcessingHandler implements TestOnly
{
/**
* Messages
*
* @var array
*/
protected $messages = array();
/**
* Get all messages
*
* @return array
*/
public function getMessages()
{
return $this->messages;
}
public function clear()
{
$this->messages = array();
}
protected function write(array $record)
{
$this->messages[] = $record['message'];
}
}

View File

@ -0,0 +1,15 @@
<?php
namespace SilverStripe\FullTextSearch\Tests\SolrReindexTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\FullTextSearch\Solr\SolrIndex;
class SolrReindexTest_Index extends SolrIndex implements TestOnly
{
public function init()
{
$this->addClass('SolrReindexTest_Item');
$this->addAllFulltextFields();
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace SilverStripe\FullTextSearch\Tests\SolrReindexTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
use SilverStripe\FullTextSearch\Tests\SolrReindexTest\SolrReindexTest_ItemExtension;
/**
* Does not have any variant extensions
*/
class SolrReindexTest_Item extends DataObject implements TestOnly
{
private static $extensions = [
SolrReindexTest_ItemExtension::class
];
private static $db = array(
'Title' => 'Varchar(255)',
'Variant' => 'Int(0)'
);
}

View File

@ -0,0 +1,31 @@
<?php
namespace SilverStripe\FullTextSearch\Tests\SolrReindexTest;
use SilverStripe\ORM\DataExtension;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\Queries\SQLSelect;
use SilverStripe\ORM\DataQuery;
use SilverStripe\FullTextSearch\Tests\SolrReindexTest\SolrReindexTest_Variant;
use SilverStripe\Core\Convert;
/**
* Select only records in the current variant
*/
class SolrReindexTest_ItemExtension extends DataExtension implements TestOnly
{
/**
* Filter records on the current variant
*
* @param SQLQuery $query
* @param DataQuery $dataQuery
*/
public function augmentSQL(SQLSelect $query, DataQuery $dataQuery = NULL)
{
$variant = SolrReindexTest_Variant::get_current();
if ($variant !== null && !$query->filtersOnID()) {
$sqlVariant = Convert::raw2sql($variant);
$query->addWhere("\"Variant\" = '{$sqlVariant}'");
}
}
}

View File

@ -0,0 +1,74 @@
<?php
namespace SilverStripe\FullTextSearch\Tests\SolrReindexTest;
use Monolog\Logger;
use SilverStripe\Dev\TestOnly;
use SilverStripe\FullTextSearch\Tests\SolrReindexTest\SolrReindexTest_Handler;
/**
* Test logger for recording messages
*/
class SolrReindexTest_RecordingLogger extends Logger implements TestOnly
{
/**
* @var SolrReindexTest_Handler
*/
protected $testHandler = null;
public function __construct($name = 'testlogger', array $handlers = array(), array $processors = array())
{
parent::__construct($name, $handlers, $processors);
$this->testHandler = new SolrReindexTest_Handler();
$this->pushHandler($this->testHandler);
}
/**
* @return array
*/
public function getMessages()
{
return $this->testHandler->getMessages();
}
/**
* Clear all messages
*/
public function clear()
{
$this->testHandler->clear();
}
/**
* Get messages with the given filter
*
* @param string $containing
* @return array Filtered array
*/
public function filterMessages($containing)
{
return array_values(array_filter(
$this->getMessages(),
function ($content) use ($containing) {
return stripos($content, $containing) !== false;
}
));
}
/**
* Count all messages containing the given substring
*
* @param string $containing Message to filter by
* @return int
*/
public function countMessages($containing = null)
{
if ($containing) {
$messages = $this->filterMessages($containing);
} else {
$messages = $this->getMessages();
}
return count($messages);
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace SilverStripe\FullTextSearch\Tests\SolrReindexTest;
use SilverStripe\FullTextSearch\Solr\Reindex\Handlers\SolrReindexBase;
use Psr\Log\LoggerInterface;
use SilverStripe\FullTextSearch\Solr\SolrIndex;
/**
* Provides a wrapper for testing SolrReindexBase
*/
class SolrReindexTest_TestHandler extends SolrReindexBase
{
public function processGroup(
LoggerInterface $logger, SolrIndex $indexInstance, $state, $class, $groups, $group, $taskName
) {
$indexName = $indexInstance->getIndexName();
$stateName = json_encode($state);
$logger->info("Called processGroup with {$indexName}, {$stateName}, {$class}, group {$group} of {$groups}");
}
public function triggerReindex(LoggerInterface $logger, $batchSize, $taskName, $classes = null)
{
$logger->info("Called triggerReindex");
}
}

View File

@ -0,0 +1,117 @@
<?php
namespace SilverStripe\FullTextSearch\Tests\SolrReindexTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\FullTextSearch\Search\Variants\SearchVariant;
/**
* Dummy variant that selects items with field Varient matching the current value
*
* Variant states are 0 and 1, or null if disabled
*/
class SolrReindexTest_Variant extends SearchVariant implements TestOnly
{
/**
* Value of this variant (either null, 0, or 1)
*
* @var int|null
*/
protected static $current = null;
/**
* Activate this variant
*/
public static function enable()
{
self::disable();
self::$current = 0;
self::$variants = array(
'SolrReindexTest_Variant' => singleton('SolrReindexTest_Variant')
);
}
/**
* Disable this variant and reset
*/
public static function disable()
{
self::$current = null;
self::$variants = null;
self::$class_variants = array();
self::$call_instances = array();
}
public function activateState($state)
{
self::set_current($state);
}
/**
* Set the current variant to the given state
*
* @param int $current 0, 1, 2, or null (disabled)
*/
public static function set_current($current)
{
self::$current = $current;
}
/**
* Get the current state
*
* @return string|null
*/
public static function get_current()
{
// Always use string values for states for consistent json_encode value
if (isset(self::$current)) {
return (string)self::$current;
}
}
public function alterDefinition($class, $index)
{
$self = get_class($this);
$this->addFilterField($index, '_testvariant', array(
'name' => '_testvariant',
'field' => '_testvariant',
'fullfield' => '_testvariant',
'base' => ClassInfo::baseDataClass($class),
'origin' => $class,
'type' => 'Int',
'lookup_chain' => array(array('call' => 'variant', 'variant' => $self, 'method' => 'currentState'))
));
}
public function alterQuery($query, $index)
{
// I guess just calling it _testvariant is ok?
$query->filter('_testvariant', $this->currentState());
}
public function appliesTo($class, $includeSubclasses)
{
return $class === 'SolrReindexTest_Item' ||
($includeSubclasses && is_subclass_of($class, 'SolrReindexTest_Item', true));
}
public function appliesToEnvironment()
{
// Set to null to disable
return self::$current !== null;
}
public function currentState()
{
return self::get_current();
}
public function reindexStates()
{
// Always use string values for states for consistent json_encode value
return array('0', '1', '2');
}
}