diff --git a/composer.json b/composer.json index 031f427..724c782 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,8 @@ "zfcampus/zf-hal": "~1.0", "zfcampus/zf-mvc-auth": "~1.0", "zfcampus/zf-rest": "~1.0", - "predis/predis": "1.0.3" + "predis/predis": "1.0.3", + "ruflin/elastica":"2.3.1" }, "require-dev": { diff --git a/config/application.config.php b/config/application.config.php index bf26707..f0bd504 100644 --- a/config/application.config.php +++ b/config/application.config.php @@ -37,6 +37,7 @@ 'Core', 'DoctrineModule', 'DoctrineORMModule', + 'Search', ], // Load only on www.* requests APP_URI_FRONTEND => [ diff --git a/config/autoload/elastica.php.dist b/config/autoload/elastica.php.dist new file mode 100644 index 0000000..0f99dfd --- /dev/null +++ b/config/autoload/elastica.php.dist @@ -0,0 +1,21 @@ + [ + 'servers' => [ + 'host' => '127.0.0.1', + 'port' => 9200, + ] + ] +]; diff --git a/module/Search/config/module.config.php b/module/Search/config/module.config.php index 2670143..8adef2b 100644 --- a/module/Search/config/module.config.php +++ b/module/Search/config/module.config.php @@ -1,3 +1,10 @@ [ + 'factories' => [ + 'search.client.elastic' => 'Search\Client\Factory\ElasticClientFactory', + 'search.index.user' => 'Search\Index\Service\Factory\UserIndexServiceFactory', + 'search.query.user' => 'Search\Query\Service\Factory\UserQueryServiceFactory', + ], + ], +]; diff --git a/module/Search/src/Search/Client/ElasticClient.php b/module/Search/src/Search/Client/ElasticClient.php new file mode 100644 index 0000000..333563a --- /dev/null +++ b/module/Search/src/Search/Client/ElasticClient.php @@ -0,0 +1,15 @@ + + */ +namespace Search\Client; + +use Elastica\Client as ElasticaClient; +use Search\Client\Interfaces\SearchClientInterface; + +class ElasticClient extends ElasticaClient implements SearchClientInterface +{ +} diff --git a/module/Search/src/Search/Client/Factory/ElasticClientFactory.php b/module/Search/src/Search/Client/Factory/ElasticClientFactory.php new file mode 100644 index 0000000..d8f9d95 --- /dev/null +++ b/module/Search/src/Search/Client/Factory/ElasticClientFactory.php @@ -0,0 +1,29 @@ + + */ +namespace Search\Client\Factory; + +use Search\Client\ElasticClient; +use Zend\ServiceManager\FactoryInterface; +use Zend\ServiceManager\ServiceLocatorInterface; + +class ElasticClientFactory implements FactoryInterface +{ + /** + * Create elastica client + * + * @param ServiceLocatorInterface $sm + * @return ElasticClient + */ + public function createService(ServiceLocatorInterface $sm) + { + $config = $sm->get('Config'); + $clientConfig = isset($config['elastica']) ? $config['elastica'] : array(); + + return new ElasticClient($clientConfig); + } +} diff --git a/module/Search/src/Search/Client/Factory/SolrClientFactory.php b/module/Search/src/Search/Client/Factory/SolrClientFactory.php new file mode 100644 index 0000000..a77405c --- /dev/null +++ b/module/Search/src/Search/Client/Factory/SolrClientFactory.php @@ -0,0 +1,26 @@ + + */ +namespace Search\Client\Factory; + +use Zend\ServiceManager\FactoryInterface; +use Zend\ServiceManager\ServiceLocatorInterface; + +class SolrClientFactory implements FactoryInterface +{ + /** + * Create Solr client + * + * @param ServiceLocatorInterface $sm + * @return SolrClient + */ + public function createService(ServiceLocatorInterface $sm) + { + //TODO: Return a new Solr Client here + // return new SolrClient($clientConfig); + } +} diff --git a/module/Search/src/Search/Client/Interfaces/SearchClientInterface.php b/module/Search/src/Search/Client/Interfaces/SearchClientInterface.php new file mode 100644 index 0000000..6ddc63e --- /dev/null +++ b/module/Search/src/Search/Client/Interfaces/SearchClientInterface.php @@ -0,0 +1,12 @@ + + */ +namespace Search\Client\Interfaces; + +interface SearchClientInterface +{ +} diff --git a/module/Search/src/Search/Exception/MissingDataException.php b/module/Search/src/Search/Exception/MissingDataException.php new file mode 100644 index 0000000..e1d402c --- /dev/null +++ b/module/Search/src/Search/Exception/MissingDataException.php @@ -0,0 +1,12 @@ + + */ +namespace Search\Exception; + +class MissingDataException extends \InvalidArgumentException +{ +} diff --git a/module/Search/src/Search/Index/Service/AbstractIndexService.php b/module/Search/src/Search/Index/Service/AbstractIndexService.php new file mode 100644 index 0000000..18879e5 --- /dev/null +++ b/module/Search/src/Search/Index/Service/AbstractIndexService.php @@ -0,0 +1,31 @@ + + */ +namespace Search\Index\Service; + +use Search\Exception\MissingDataException; +use Search\Service\AbstractSearchService; + +class AbstractIndexService extends AbstractSearchService +{ + /** + * + * @param array $data + * @param integer $version + * @return null + */ + public function index(array $data, $version = 1) + { + if (!array_key_exists('id', $data)) { + throw new MissingDataException('`id` field is requeired to index data'); + } + + //TODO: this creation will be moved to a builder + $document = new \Elastica\Document($data['id'], $data); + $this->getType($version)->addDocument($document); + } +} diff --git a/module/Search/src/Search/Index/Service/Factory/UserIndexServiceFactory.php b/module/Search/src/Search/Index/Service/Factory/UserIndexServiceFactory.php new file mode 100644 index 0000000..7447280 --- /dev/null +++ b/module/Search/src/Search/Index/Service/Factory/UserIndexServiceFactory.php @@ -0,0 +1,28 @@ + + */ +namespace Search\Index\Service\Factory; + +use Search\Index\Service\UserIndexService; +use Zend\ServiceManager\FactoryInterface; +use Zend\ServiceManager\ServiceLocatorInterface; + +class UserIndexServiceFactory implements FactoryInterface +{ + /** + * Create service + * + * @param ServiceLocatorInterface $serviceLocator + * @return UserIndexService + */ + public function createService(ServiceLocatorInterface $sm) + { + $userIndexService = new UserIndexService($sm->get('search.client.elastic')); + + return $userIndexService; + } +} diff --git a/module/Search/src/Search/Index/Service/UserIndexService.php b/module/Search/src/Search/Index/Service/UserIndexService.php new file mode 100644 index 0000000..920ed5f --- /dev/null +++ b/module/Search/src/Search/Index/Service/UserIndexService.php @@ -0,0 +1,25 @@ + + */ +namespace Search\Index\Service; + +class UserIndexService extends AbstractIndexService +{ + /** + * @var string|array + */ + protected $index = [ + 1 => 'users' + ]; + + /** + * @var string|array + */ + protected $type = [ + 1 => 'user' + ]; +} diff --git a/module/Search/src/Search/Paginator/Adapter/ElasticsearchAdapter.php b/module/Search/src/Search/Paginator/Adapter/ElasticsearchAdapter.php new file mode 100644 index 0000000..3121830 --- /dev/null +++ b/module/Search/src/Search/Paginator/Adapter/ElasticsearchAdapter.php @@ -0,0 +1,52 @@ + + */ +namespace Search\Paginator\Adapter; + +use Zend\Paginator\Adapter\ArrayAdapter; + +class ElasticsearchAdapter extends ArrayAdapter +{ + protected $meta = []; + + /** + * Constructor. + * + * @param array $array ArrayAdapter to paginate + */ + public function __construct(array $array = array(), $count=0) + { + $this->array = $array; + $this->count = $count; + } + + /** + * Returns an array of items for a page. + * + * @param int $offset Page offset + * @param int $itemCountPerPage Number of items per page + * @return array + */ + public function getItems($offset, $itemCountPerPage) + { + return $this->array; + } + + public function getMeta($key) + { + if (isset($this->meta[$key])) { + return $this->meta[$key]; + } + + return null; + } + + public function setMeta($key, $value) + { + $this->meta[$key] = $value; + } +} diff --git a/module/Search/src/Search/Query/Service/AbstractQueryService.php b/module/Search/src/Search/Query/Service/AbstractQueryService.php new file mode 100644 index 0000000..fca03ff --- /dev/null +++ b/module/Search/src/Search/Query/Service/AbstractQueryService.php @@ -0,0 +1,131 @@ + + */ +namespace Search\Query\Service; + +use Elastica\Query as ElasticaQuery; +use Elastica\QueryBuilder; +use Elastica\ResultSet; +use Search\Paginator\Adapter\ElasticsearchAdapter; +use Search\Service\AbstractSearchService; +use Zend\Paginator\Paginator; + +class AbstractQueryService extends AbstractSearchService +{ + const VIEW_LIST = 'list'; + const VIEW_SHORT = 'short'; + const VIEW_DETAIL = 'detail'; + + /** + * Creating a Zend\Paginator\Paginator from Elastica\ResultSet. + * This paginator can be passed directly zf-hal or whereever you want. + * This method use ElasticsearchAdapter as ArrayAdapter + * + * @param ResultSet $resultSet Elastica result data + * @return Paginator Created paginator from resultset with ElasticsearchAdapter. + */ + public function buildPaginator(Resultset $resultSet) + { + $itemsPerPage = empty($resultSet->getQuery()->getParam('size')) ? $this->getDefaultPageSize() : $resultSet->getQuery()->getParam('size'); + $currentPage = (int)ceil($resultSet->getQuery()->getParam('size') / $itemsPerPage) + 1; + + $results = []; + foreach ($resultSet->getResults() as $result) { + $data = $result->getData(); + $data['_score'] = $result->getScore(); + $results[] = $data; + } + + $adapter = new ElasticsearchAdapter($results, $resultSet->getTotalHits()); + $adapter->setMeta('aggs', $resultSet->getAggregations()); + + $paginator = new Paginator($adapter); + $paginator->setCurrentPageNumber($currentPage); + $paginator->setItemCountPerPage($itemsPerPage); + + return $paginator; + } + + /** + * Search on Elasticsearch + * Example Usage: + * + * With Elastica Query Builder + * $qb = new QueryBuilder(); + * $query = $qb->query()->match_all(); + * $mainQuery = new \Elastica\Query($query); + * $this->doSearch($mainQuery, 1, 10); + * + * @param ElasticaQuery $query + * @param int $page + * @param int $itemPerPage + * @param string $type Already defined strings at self. + * @param int $version Search version for index and type + * @return Resultset + */ + protected function doSearch(ElasticaQuery $query, $page, $itemsPerPage, $type = self::VIEW_LIST, $version = 1) + { + $query->setFrom(($page - 1) * $itemsPerPage) + ->setSize($itemsPerPage); + if ($type === self::VIEW_LIST) { + if ($this->getListViewFields()) { + $query->setSource($this->getListViewFields()); + } + } elseif ($type === self::VIEW_SHORT) { + if ($this->getShortViewFields()) { + $query->setSource($this->getShortViewFields()); + } + } elseif ($type === self::VIEW_DETAIL) { + if ($this->getDetailViewFields()) { + $query->setSource($this->getDetailViewFields()); + } + } + + return $this->getClient() + ->getIndex($this->getIndexName($version)) + ->getType($this->getTypeName($version)) + ->search($query); + } + + /** + * Return fields on listing Mode + * If array is empty, it won't set a _source. And query returns all fields + * + * @return array + */ + protected function getListViewFields() + { + return array(); + } + /** + * Return fields on listing Mode + * If array is empty, it won't set a _source. And query returns all fields + * + * @return array + */ + protected function getShortViewFields() + { + return array(); + } + /** + * Return fields on detail Mode + * If array is empty, it won't set a _source. And query returns all fields + * + * @return array + */ + protected function getDetailViewFields() + { + return array(); + } + + protected function getDefaultPageSize() + { + return 10; + } +} diff --git a/module/Search/src/Search/Query/Service/Factory/UserQueryServiceFactory.php b/module/Search/src/Search/Query/Service/Factory/UserQueryServiceFactory.php new file mode 100644 index 0000000..36cc8bb --- /dev/null +++ b/module/Search/src/Search/Query/Service/Factory/UserQueryServiceFactory.php @@ -0,0 +1,26 @@ + + */ +namespace Search\Query\Service\Factory; + +use Search\Query\Service\UserQueryService; +use Zend\ServiceManager\FactoryInterface; +use Zend\ServiceManager\ServiceLocatorInterface; + +class UserQueryServiceFactory implements FactoryInterface +{ + /** + * Create service + * + * @param ServiceLocatorInterface $serviceLocator + * @return UserIndexService + */ + public function createService(ServiceLocatorInterface $sm) + { + return new UserQueryService($sm->get('search.client.elastic')); + } +} diff --git a/module/Search/src/Search/Query/Service/UserQueryService.php b/module/Search/src/Search/Query/Service/UserQueryService.php new file mode 100644 index 0000000..7003149 --- /dev/null +++ b/module/Search/src/Search/Query/Service/UserQueryService.php @@ -0,0 +1,58 @@ + + */ +namespace Search\Query\Service; + +class UserQueryService extends AbstractQueryService +{ + /** + * @var string|array + */ + protected $index = [ + 1 => 'users' + ]; + + /** + * @var string|array + */ + protected $type = [ + 1 => 'user' + ]; + + /** + * + * User searcher method. + * Example Usage : + * $userSearcher = $this->getServiceLocator()->get('search.query.user'); + * $result = $userSearcher->search(['name' => 'Haydar']); + * + * @param array $params + * @return array + */ + public function search(array $params) + { + $qb = new \Elastica\QueryBuilder(); + $query = new \Elastica\Query( + $qb->query() + ->match_all() + ); + $resultSet = $this->doSearch($query, 1, 10); + $paginator = $this->buildPaginator($resultSet); + + return $paginator; + } + + public function getFieldWhitelistForQueries() + { + return [ + 'name' => [ + 'type' => 'term', + 'field' => 'name', + ] + ]; + } +} diff --git a/module/Search/src/Search/Service/AbstractSearchService.php b/module/Search/src/Search/Service/AbstractSearchService.php new file mode 100644 index 0000000..fd1413d --- /dev/null +++ b/module/Search/src/Search/Service/AbstractSearchService.php @@ -0,0 +1,106 @@ + + */ +namespace Search\Service; + +use Search\Client\Interfaces\SearchClientInterface; +use Search\Service\Interfaces\SearchServiceInterface; + +class AbstractSearchService implements SearchServiceInterface +{ + /** + * ElasticsearchClient $client + */ + protected $client; + + /** + * @var string|array + */ + protected $index; + + /** + * @var string|array + */ + protected $type; + + /** + * + * @param ElasticsearchClient $client + */ + public function __construct(SearchClientInterface $client) + { + $this->client = $client; + } + + public function getClient() + { + return $this->client; + } + + /** + * Return client index name + * + * @var int $version + * @return string client Index Name + */ + public function getIndexName($version = 1) + { + if (is_array($this->index)) { + if (isset($this->index[$version])) { + return $this->index[$version]; + } else { + throw new \Exception('You should select correct version for index name!'); + } + } else { + return $this->index[$version]; + } + } + + /** + * Return index of index + * + * @var int $version + * @return mixed Index of searcher + */ + public function getIndex($version = 1) + { + return $this->getClient()->getIndex($this->getIndexName($version)); + } + + + /** + * Return searcher index name + * + * @var int $version + * @return string Searcher Index Name + */ + public function getTypeName($version = 1) + { + if (is_array($this->type)) { + if (isset($this->type[$version])) { + return $this->type[$version]; + } else { + throw new \Exception('You should select correct version for type name!'); + } + } else { + return $this->type[$version]; + } + } + + + /** + * Return type of index + * @var int $version + * @return mixed type of searcher + */ + public function getType($version = 1) + { + return $this->getCLient() + ->getIndex($this->getIndexName($version)) + ->getType($this->getTypeName($version)); + } +} diff --git a/module/Search/src/Search/Service/Interfaces/SearchServiceInterface.php b/module/Search/src/Search/Service/Interfaces/SearchServiceInterface.php new file mode 100644 index 0000000..b83366b --- /dev/null +++ b/module/Search/src/Search/Service/Interfaces/SearchServiceInterface.php @@ -0,0 +1,13 @@ + + */ +namespace Search\Service\Interfaces; + +interface SearchServiceInterface +{ + public function getClient(); +}