my changes

This commit is contained in:
Arno Kaimbacher 2018-08-06 14:30:51 +02:00
parent 28301e4312
commit 8dc1f1b048
263 changed files with 36882 additions and 4453 deletions

View file

@ -0,0 +1,257 @@
<?php
namespace App\Library\Util;
use App\Library\Util\Searchtypes;
use App\Library\Util\SolrSearchQuery;
class QueryBuilder
{
private $_logger;
private $_filterFields;
private $_searchFields;
private $_export = false;
const SEARCH_MODIFIER_CONTAINS_ALL = "contains_all";
const SEARCH_MODIFIER_CONTAINS_ANY = "contains_any";
const SEARCH_MODIFIER_CONTAINS_NONE = "contains_none";
const MAX_ROWS = 2147483647;
/**
*
* @param boolean $export
*/
public function __construct($export = false)
{
$this->_filterFields = array();
// $filters = Opus_Search_Config::getFacetFields();
// if ( !count( $filters ) ) {
// $this->_logger->debug( 'key searchengine.solr.facets is not present in config. skipping filter queries' );
// } else {
// $this->_logger->debug( 'searchengine.solr.facets is set to ' . implode( ',', $filters ) );
// }
// foreach ($filters as $filterfield) {
// if ($filterfield == 'year_inverted') {
// $filterfield = 'year';
// }
// array_push($this->_filterFields, trim($filterfield));
// }
$this->_searchFields = array('author', 'title', 'persons', 'referee', 'abstract', 'fulltext', 'year');
$this->_export = $export;
}
/**
*
* @param $request
* @return array
*/
public function createQueryBuilderInputFromRequest($request) : array
{
if (is_null($request->all())) {
throw new Application_Util_QueryBuilderException('Unable to read request data.Search cannot be performed.');
}
if (is_null($request->input('searchtype'))) {
throw new Application_Util_QueryBuilderException('Unspecified search type: unable to create query.');
}
if (!Searchtypes::isSupported($request->input('searchtype'))) {
throw new Application_Util_QueryBuilderException(
'Unsupported search type ' . $request->input('searchtype') . ' : unable to create query.'
);
}
$this->validateParamsType($request);
if ($request->input('sortfield')) {
$sorting = array($request->input('sortfield'), 'asc');
} else {
//$sorting = Opus_Search_Query::getDefaultSorting();
$sorting = array('score', 'desc' );
}
$input = array(
'searchtype' => $request->input('searchtype'),
'start' => $request->input('start'),//, Opus_Search_Query::getDefaultStart()),
'rows' => $request->input('rows'),// Opus_Search_Query::getDefaultRows()),
'sortField' => $sorting[0],
'sortOrder' => $request->input('sortorder', $sorting[1]),
'docId' => $request->input('docId'),
'query' => $request->input('query', '*:*')
);
//if ($this->_export) {
// $maxRows = self::MAX_ROWS;
// // pagination within export was introduced in OPUS 4.2.2
// $startParam = $request->input('start', 0);
// $rowsParam = $request->input('rows', $maxRows);
// $start = intval($startParam);
// $rows = intval($rowsParam);
// $input['start'] = $start > 0 ? $start : 0;
// $input['rows'] = $rows > 0 || ($rows == 0 && $rowsParam == '0') ? $rows : $maxRows;
// if ($input['start'] > $maxRows) {
// $input['start'] = $maxRows;
// }
// if ($input['rows'] + $input['start'] > $maxRows) {
// $input['rows'] = $maxRows - $start;
// }
//}
foreach ($this->_searchFields as $searchField) {
$input[$searchField] = $request->input($searchField, '');
$input[$searchField . 'modifier'] = $request->input(
$searchField . 'modifier',
self::SEARCH_MODIFIER_CONTAINS_ALL
);
}
// foreach ($this->_filterFields as $filterField) {
// $param = $filterField . 'fq';
// $input[$param] = $request->getParam($param, '');
// }
// if ($request->getParam('searchtype') === Searchtypes::COLLECTION_SEARCH
// || $request->input('searchtype') === Searchtypes::SERIES_SEARCH)
// {
// $searchParams = new Application_Util_BrowsingParams($request, $this->_logger);
// switch ($request->input('searchtype')) {
// case Searchtypes::COLLECTION_SEARCH:
// $input['collectionId'] = $searchParams->getCollectionId();
// break;
// case Searchtypes::SERIES_SEARCH:
// $input['seriesId'] = $searchParams->getSeriesId();
// break;
// }
// }
return $input;
}
/**
* Checks if all given parameters are of type string. Otherwise, throws Application_Util_QueryBuilderException.
*
* @throws //Application_Util_QueryBuilderException
*/
private function validateParamsType($request)
{
$paramNames = array(
'searchtype',
'start',
'rows',
'sortField',
'sortOrder',
'search',
'collectionId',
'seriesId'
);
foreach ($this->_searchFields as $searchField) {
array_push($paramNames, $searchField, $searchField . 'modifier');
}
foreach ($this->_filterFields as $filterField) {
array_push($paramNames, $filterField . 'fq');
}
foreach ($paramNames as $paramName) {
$paramValue = $request->input($paramName, null);
if (!is_null($paramValue) && !is_string($paramValue)) {
throw new Application_Util_QueryBuilderException('Parameter ' . $paramName . ' is not of type string');
}
}
}
/**
*
* @param array $input
* @return SolrSearchQuery
*/
public function createSearchQuery($input) : SolrSearchQuery
{
if ($input['searchtype'] === Searchtypes::SIMPLE_SEARCH) {
return $this->createSimpleSearchQuery($input);
//return $this->createAllSearchQuery($input);
}
if ($input['searchtype'] === Searchtypes::ALL_SEARCH) {
return $this->createAllSearchQuery($input);
}
return $this->createSimpleSearchQuery($input);
}
// private function createIdSearchQuery($input) {
// $this->_logger->debug("Constructing query for id search.");
// if (is_null($input['docId'])) {
// throw new Application_Exception("No id provided.", 404);
// }
// $query = new Opus_SolrSearch_Query(Opus_SolrSearch_Query::DOC_ID);
// $query->setField('id', $input['docId']);
// if ($this->_export) {
// $query->setReturnIdsOnly(true);
// }
// $this->_logger->debug("Query $query complete");
// return $query;
// }
private function createAllSearchQuery($input)
{
//$this->_logger->debug("Constructing query for all search.");
$query = new SolrSearchQuery(SolrSearchQuery::ALL_DOCS);
$query->setStart("0");//$input['start']);
//$query->setRows($input['rows']);
$query->setRows("10");
$query->setSortField($input['sortField']);
$query->setSortOrder($input['sortOrder']);
//$this->addFiltersToQuery($query, $input);
//if ($this->_export) {
// $query->setReturnIdsOnly(true);
//}
//$this->_logger->debug("Query $query complete");
return $query;
}
private function createSimpleSearchQuery($input) : SolrSearchQuery
{
// $this->_logger->debug("Constructing query for simple search.");
$solrQuery = new SolrSearchQuery(SolrSearchQuery::SIMPLE);
$solrQuery->setStart($input['start']);
$solrQuery->setRows("10");//$input['rows']);
$solrQuery->setSortField($input['sortField']);
$solrQuery->setSortOrder($input['sortOrder']);
$solrQuery->setCatchAll($input['query']);
//$this->addFiltersToQuery($solrQuery, $input);
// if ($this->_export) {
// $solrQuery->setReturnIdsOnly(true);
// }
// $this->_logger->debug("Query $solrQuery complete");
return $solrQuery;
}
private function addFiltersToQuery($query, $input)
{
foreach ($this->_filterFields as $filterField) {
$facetKey = $filterField . 'fq';
$facetValue = $input[$facetKey];
if ($facetValue !== '') {
$this->_logger->debug(
"request has facet key: $facetKey - value is: $facetValue - corresponding facet is: $filterField"
);
$query->addFilterQuery($filterField, $facetValue);
}
}
}
}

View file

@ -0,0 +1,452 @@
<?php
namespace App\Library\Util;
/**
* Implements API for describing search queries.
*
* @note This part of Opus search API differs from Solr in terminology in that
* all requests for searching documents are considered "queries" with a
* "filter" used to describe conditions matching documents has to met.
* In opposition to Solr's "filter queries" this API supports "subfilters"
* to reduce confusions on differences between filters, queries and
* filter queries. Thus wording is mapped like this
*
* Solr --> Opus
* "request" --> "query"
* "query" --> "filter"
* "filter query" --> "subfilter"
*
* @method int getStart( int $default = null )
* @method int getRows( int $default = null )
* @method string[] getFields( array $default = null )
* @method array getSort( array $default = null )
* @method bool getUnion( bool $default = null )
* @method Opus_Search_Filter_Base getFilter( Opus_Search_Filter_Base $default = null )
* @method Opus_Search_Facet_Set getFacet( Opus_Search_Facet_Set $default = null )
* @method $this setStart( int $offset )
* @method $this setRows( int $count )
* @method $this setFields( $fields )
* @method $this setSort( $sorting )
* @method $this setUnion( bool $isUnion )
* @method $this setFilter( Opus_Search_Filter_Base $filter ) assigns condition to be met by resulting documents
* @method $this setFacet( Opus_Search_Facet_Set $facet )
* @method $this addFields( string $fields )
* @method $this addSort( $sorting )
*/
class SearchParameter
{
protected $_data;
public function reset()
{
$this->_data = array(
'start' => null,
'rows' => null,
'fields' => null,
'sort' => null,
'union' => null,
'filter' => null,
'facet' => null,
'subfilters' => null,
);
}
public function __construct()
{
$this->reset();
}
/**
* Tests if provided name is actually name of known parameter normalizing it
* on return.
*
* @throws InvalidArgumentException unless providing name of existing parameter
* @param string $name name of parameter to access
* @return string normalized name of existing parameter
*/
protected function isValidParameter($name)
{
if (!array_key_exists(strtolower(trim($name)), $this->_data)) {
throw new InvalidArgumentException('invalid query parameter: ' . $name);
}
return strtolower(trim($name));
}
/**
* Normalizes one or more field names or set of comma-separated field names
* into set of field names.
*
* @param string|string[] $input one or more field names or comma-separated lists of fields' names
* @return string[] list of field names
*/
protected function normalizeFields($input)
{
if (!is_array($input)) {
$input = array($input);
}
$output = array();
foreach ($input as $field) {
if (!is_string($field)) {
throw new InvalidArgumentException('invalid type of field selector');
}
$fieldNames = preg_split('/[\s,]+/', $field, null, PREG_SPLIT_NO_EMPTY);
foreach ($fieldNames as $name) {
if (!preg_match('/^(?:\*|[a-z_][a-z0-9_]*)$/i', $name)) {
throw new InvalidArgumentException('malformed field selector: ' . $name);
}
$output[] = $name;
}
}
if (!count($input)) {
throw new InvalidArgumentException('missing field selector');
}
return $output;
}
/**
* Parses provided parameter for describing some sorting direction.
*
* @param string|bool $ascending one out of true, false, "asc" or "desc"
* @return bool true if parameter is considered requesting to sort in ascending order
*/
protected function normalizeDirection($ascending)
{
if (!strcasecmp($ascending, 'asc')) {
$ascending = true;
} elseif (!strcasecmp($ascending, 'desc')) {
$ascending = false;
} elseif ($ascending !== false && $ascending !== true) {
throw new InvalidArgumentException('invalid sorting direction selector');
}
return $ascending;
}
/**
* Retrieves value of selected query parameter.
*
* @param string $name name of parameter to read
* @param mixed $defaultValue value to retrieve if parameter hasn't been set internally
* @return mixed value of selected parameter, default if missing internally
*/
public function get($name, $defaultValue = null)
{
$name = $this->isValidParameter($name);
return is_null($this->_data[$name]) ? $defaultValue : $this->_data[$name];
}
/**
* Sets value of selected query parameter.
*
* @throws InvalidArgumentException in case of invalid arguments (e.g. on trying to add value to single-value param)
* @param string $name name of query parameter to adjust
* @param string[]|array|string|int $value value of query parameter to write
* @param bool $adding true for adding given parameter to any existing one
* @return $this
*/
public function set($name, $value, $adding = false) //filter, "aa", false
{
$name = $this->isValidParameter($name);
switch ($name) {
case 'start':
case 'rows':
if ($adding) {
throw new InvalidArgumentException('invalid parameter access on ' . $name);
}
if (!is_scalar($value) || !ctype_digit(trim($value))) {
throw new InvalidArgumentException('invalid parameter value on ' . $name);
}
$this->_data[$name] = intval($value);
break;
case 'fields':
$fields = $this->normalizeFields($value);
if ($adding && is_null($this->_data['fields'])) {
$adding = false;
}
if ($adding) {
$this->_data['fields'] = array_merge($this->_data['fields'], $fields);
} else {
if (!count($fields)) {
throw new InvalidArgumentException('setting empty set of fields rejected');
}
$this->_data['fields'] = $fields;
}
$this->_data['fields'] = array_unique($this->_data['fields']);
break;
case 'sort':
if (!is_array($value)) {
$value = array($value, true);
}
switch (count($value)) {
case 2:
$fields = array_shift($value);
$ascending = array_shift($value);
break;
case 1:
$fields = array_shift($value);
$ascending = true;
break;
default:
throw new InvalidArgumentException('invalid sorting selector');
}
$this->addSorting($fields, $ascending, !$adding);
break;
case 'union':
if ($adding) {
throw new InvalidArgumentException('invalid parameter access on ' . $name);
}
$this->_data[$name] = !!$value;
break;
case 'filter':
if ($adding) {
throw new InvalidArgumentException('invalid parameter access on ' . $name);
}
// if ( !( $value instanceof Opus_Search_Filter_Base ) ) {
// throw new InvalidArgumentException( 'invalid filter' );
// }
$this->_data[$name] = $value;
break;
case 'facet':
if ($adding) {
throw new InvalidArgumentException('invalid parameter access on ' . $name);
}
if (!($value instanceof Opus_Search_Facet_Set)) {
throw new InvalidArgumentException('invalid facet options');
}
$this->_data[$name] = $value;
break;
case 'subfilters':
throw new RuntimeException('invalid access on sub filters');
}
return $this;
}
public function __get($name)
{
return $this->get($name);
}
public function __isset($name)
{
return !is_null($this->get($name));
}
public function __set($name, $value)
{
$this->set($name, $value, false);
}
public function __call($method, $arguments)
{
if (preg_match('/^(get|set|add)([a-z]+)$/i', $method, $matches)) {
$property = $this->isValidParameter($matches[2]);
switch (strtolower($matches[1])) {
case 'get':
return $this->get($property, @$arguments[0]);
case 'set':
$this->set($property, @$arguments[0], false);
return $this;
case 'add':
$this->set($property, @$arguments[0], true);
return $this;
}
}
throw new RuntimeException('invalid method: ' . $method);
}
/**
* Adds request for sorting by some field in desired order.
*
* @param string|string[] $field one or more field names to add sorting (as array and/or comma-separated string)
* @param bool $ascending true or "asc" for ascending by all given fields
* @param bool $reset true for dropping previously declared sorting
* @return $this fluent interface
*/
public function addSorting($field, $ascending = true, $reset = false)
{
$fields = $this->normalizeFields($field);
$ascending = $this->normalizeDirection($ascending);
if (!count($fields)) {
throw new InvalidArgumentException('missing field for sorting result');
}
if ($reset || !is_array($this->_data['sort'])) {
$this->_data['sort'] = array();
}
foreach ($fields as $field) {
if ($field === '*') {
throw new InvalidArgumentException('invalid request for sorting by all fields (*)');
}
$this->_data['sort'][$field] = $ascending ? 'asc' : 'desc';
}
return $this;
}
/**
* Declares some subfilter.
*
* @note In Solr a search includes a "query" and optionally one or more
* "filter query". This API intends different terminology for the
* whole search request is considered a "query" with a "filter" used
* to select actually desired documents by matching conditions. In
* context with this terminology "subfilter" was used to describe what
* is "filter query" in Solr world: some named query to be included on
* selecting documents in database with some benefits regarding
* performance, server-side result caching and non-affecting score.
*
* @see http://wiki.apache.org/solr/CommonQueryParameters#fq
*
* @param string $name name of query (used for server-side caching)
* @param Opus_Search_Filter_Base $subFilter filter to be satisfied by all matching documents in addition
* @return $this fluent interface
*/
public function setSubFilter($name, Opus_Search_Filter_Base $subFilter)
{
if (!is_string($name) || !$name) {
throw new InvalidArgumentException('invalid sub filter name');
}
if (!is_array($this->_data['subfilters'])) {
$this->_data['subfilters'] = array($name => $subFilter);
} else {
$this->_data['subfilters'][$name] = $subFilter;
}
return $this;
}
/**
* Removes some previously defined subfilter from current query again.
*
* @note This isn't affecting server-side caching of selected filter but
* reverting some parts of query compiled on client-side.
*
* @see Opus_Search_Query::setSubFilter()
*
* @param string $name name of filter to remove from query again
* @return $this fluent interface
*/
public function removeSubFilter($name)
{
if (!is_string($name) || !$name) {
throw new InvalidArgumentException('invalid sub filter name');
}
if (is_array($this->_data['subfilters'])) {
if (array_key_exists($name, $this->_data['subfilters'])) {
unset($this->_data['subfilters'][$name]);
}
if (!count($this->_data['subfilters'])) {
$this->_data['subfilters'] = null;
}
}
return $this;
}
/**
* Retrieves named map of subfilters to include on querying search engine.
*
* @return Opus_Search_Filter_Base[]
*/
public function getSubFilters()
{
return $this->_data['subfilters'];
}
public static function getParameterDefault($name, $fallbackIfMissing, $oldName = null)
{
$config = Opus_Search_Config::getDomainConfiguration();
$defaults = $config->parameterDefaults;
if ($defaults instanceof Zend_Config) {
return $defaults->get($name, $fallbackIfMissing);
}
if ($oldName) {
return $config->get($oldName, $fallbackIfMissing);
}
return $fallbackIfMissing;
}
/**
* Retrieves configured default offset for paging results.
*
* @return int
*/
public static function getDefaultStart()
{
return static::getParameterDefault('start', 0);
}
/**
* Retrieves configured default number of rows to show (per page).
*
* @return int
*/
public static function getDefaultRows()
{
return static::getParameterDefault('rows', 10, 'numberOfDefaultSearchResults');
}
/**
* Retrieves configured default sorting.
*
* @return string[]
*/
public static function getDefaultSorting()
{
$sorting = static::getParameterDefault('sortField', 'score desc');
$parts = preg_split('/[\s,]+/', trim($sorting), null, PREG_SPLIT_NO_EMPTY);
$sorting = array(array_shift($parts));
if (!count($parts)) {
$sorting[] = 'desc';
} else {
$dir = array_shift($parts);
if (strcasecmp($dir, 'asc') || strcasecmp($dir, 'desc')) {
$dir = 'desc';
}
$sorting[] = strtolower($dir);
}
return $sorting;
}
/**
* Retrieves configured name of field to use for sorting results by default.
*
* @return string
*/
public static function getDefaultSortingField()
{
$sorting = static::getDefaultSorting();
return $sorting[0];
}
}

View file

@ -0,0 +1,290 @@
<?php
namespace App\Library\Util;
use Carbon\Carbon;
/**
* Describes local document as a match in context of a related search query.
*/
class SearchResultMatch
{
/**
* @var mixed
*/
protected $id = null;
/**
* @var Opus_Document
*/
protected $doc = null;
/**
* @var float
*/
protected $score = null;
/**
* @var Opus_Date
*/
protected $serverDateModified = null;
/**
* @var
*/
protected $fulltextIdSuccess = null;
/**
* @var
*/
protected $fulltextIdFailure = null;
/**
* Caches current document's mapping of containing serieses into document's
* number in either series.
*
* @var string[]
*/
protected $seriesNumbers = null;
/**
* Collects all additional information related to current match.
*
* @var array
*/
protected $data = array();
public function __construct($matchId)
{
$this->id = $matchId;
}
public static function create($matchId)
{
return new static($matchId);
}
/**
* Retrieves ID of document matching related search query.
*
* @return mixed
*/
public function getId()
{
return $this->id;
}
/**
* Retrieves instance of Opus_Document related to current match.
*
* @throws Opus_Model_NotFoundException
* @return Opus_Document
*/
public function getDocument()
{
if (is_null($this->doc)) {
$this->doc = new Opus_Document($this->id);
}
return $this->doc;
}
/**
* Assigns score of match in context of related search.
*
* @param $score
* @return $this
*/
public function setScore($score)
{
if (!is_null($this->score)) {
throw new RuntimeException('score has been set before');
}
$this->score = floatval($score);
return $this;
}
/**
* Retrieves score of match in context of related search.
*
* @return float|null null if score was not set
*/
public function getScore()
{
return $this->score;
}
/**
* Retrieves matching document's number in series selected by its ID.
*
* This method is provided for downward compatibility. You are advised to
* inspect document's model for this locally available information rather
* than relying on search engine returning it.
*
* @deprecated
* @return string
*/
public function getSeriesNumber($seriesId)
{
if (!$seriesId) {
return null;
}
if (!is_array($this->seriesNumbers)) {
$this->seriesNumbers = array();
foreach ($this->getDocument()->getSeries() as $linkedSeries) {
$id = $linkedSeries->getModel()->getId();
$number = $linkedSeries->getNumber();
$this->seriesNumbers[$id] = $number;
}
}
return array_key_exists($seriesId, $this->seriesNumbers) ? $this->seriesNumbers[$seriesId] : null;
}
/**
* Assigns timestamp of last modification to document as tracked in search
* index.
*
* @note This information is temporarily overloading related timestamp in
* local document.
*
* @param {int} $timestamp Unix timestamp of last modification tracked in search index
* @return $this fluent interface
*/
public function setServerDateModified($timestamp)
{
if (!is_null($this->serverDateModified)) {
throw new RuntimeException('timestamp of modification has been set before');
}
//$this->serverDateModified = new Opus_Date();
//$this->serverDateModified = Carbon::createFromTimestamp($timestamp);
$this->serverDateModified = Carbon::createFromTimestamp($timestamp)->toDateTimeString();
// if ( ctype_digit( $timestamp = trim( $timestamp ) ) ) {
// $this->serverDateModified->setUnixTimestamp( intval( $timestamp ) );
// } else {
// $this->serverDateModified->setFromString( $timestamp );
// }
return $this;
}
/**
* Provides timestamp of last modification preferring value provided by
* search engine over value stored locally in document.
*
* @note This method is used by Opus to detect outdated records in search
* index.
*
* @return string //old Opusdate
*/
public function getServerDateModified()
{
if (!is_null($this->serverDateModified)) {
return $this->serverDateModified;
}
return $this->getDocument()->getServerDateModified();
}
public function setFulltextIDsSuccess($value)
{
if (!is_null($this->fulltextIdSuccess)) {
throw new RuntimeException('successful fulltext IDs have been set before');
}
$this->fulltextIdSuccess = $value;
return $this;
}
public function getFulltextIDsSuccess()
{
if (!is_null($this->fulltextIdSuccess)) {
return $this->fulltextIdSuccess;
}
return null;
}
public function setFulltextIDsFailure($value)
{
if (!is_null($this->fulltextIdFailure)) {
throw new RuntimeException('failed fulltext IDs have been set before');
}
$this->fulltextIdFailure = $value;
return $this;
}
public function getFulltextIDsFailure()
{
if (!is_null($this->fulltextIdFailure)) {
return $this->fulltextIdFailure;
}
return null;
}
/**
* Passes all unknown method invocations to related instance of
* Opus_Document.
*
* @param string $method name of locally missing/protected method
* @param mixed[] $args arguments used on invoking that method
* @return mixed
*/
public function __call($method, $args)
{
return call_user_func_array(array($this->getDocument(), $method), $args);
}
/**
* Passes access on locally missing/protected property to related instance
* of Opus_Document.
*
* @param string $name name of locally missing/protected property
* @return mixed value of property
*/
public function __get($name)
{
return $this->getDocument()->{$name};
}
/**
* Attaches named asset to current match.
*
* Assets are additional information on match provided by search engine.
*
* @param string $name
* @param mixed $value
* @return $this fluent interface
*/
public function setAsset($name, $value)
{
$this->data[$name] = $value;
return $this;
}
/**
* Retrieves selected asset attached to current match or null if asset was
* not assigned to match.
*
* @param string $name
* @return mixed|null
*/
public function getAsset($name)
{
return isset($this->data[$name]) ? $this->data[$name] : null;
}
/**
* Tests if selected asset has been attached to current match.
*
* @param string $name name of asset to test
* @return bool true if asset was assigned to current match
*/
public function hasAsset($name) : bool
{
return array_key_exists($name, $this->data);
}
}

View file

@ -0,0 +1,30 @@
<?php
namespace App\Library\Util;
class Searchtypes
{
const SIMPLE_SEARCH = 'simple';
const ADVANCED_SEARCH = 'advanced';
const AUTHOR_SEARCH = 'authorsearch';
const COLLECTION_SEARCH = 'collection';
const LATEST_SEARCH = 'latest';
const ALL_SEARCH = 'all';
const SERIES_SEARCH = 'series';
const ID_SEARCH = 'id';
public static function isSupported($searchtype)
{
$supportedTypes = array (
self::SIMPLE_SEARCH,
self::ADVANCED_SEARCH,
self::AUTHOR_SEARCH,
self::COLLECTION_SEARCH,
self::LATEST_SEARCH,
self::ALL_SEARCH,
self::SERIES_SEARCH,
self::ID_SEARCH
);
return in_array($searchtype, $supportedTypes);
}
}

View file

@ -0,0 +1,413 @@
<?php
namespace App\Library\Util;
/**
* Encapsulates all parameter values needed to build the Solr query URL.
*/
class SolrSearchQuery
{
// currently available search types
const SIMPLE = 'simple';
const ADVANCED = 'advanced';
const FACET_ONLY = 'facet_only';
const LATEST_DOCS = 'latest';
const ALL_DOCS = 'all_docs';
const DOC_ID = 'doc_id';
const DEFAULT_START = 0;
const DEFAULT_ROWS = 10;
// java.lang.Integer.MAX_VALUE
const MAX_ROWS = 2147483647;
const DEFAULT_SORTFIELD = 'score';
const DEFAULT_SORTORDER = 'desc';
const SEARCH_MODIFIER_CONTAINS_ALL = "contains_all";
const SEARCH_MODIFIER_CONTAINS_ANY = "contains_any";
const SEARCH_MODIFIER_CONTAINS_NONE = "contains_none";
private $start = self::DEFAULT_START;
private $rows = self::DEFAULT_ROWS;
private $sortField = self::DEFAULT_SORTFIELD;
private $sortOrder = self::DEFAULT_SORTORDER;
private $filterQueries = array();
private $catchAll;
private $searchType;
private $modifier;
private $fieldValues = array();
private $escapingEnabled = true;
private $q;
private $facetField;
private $returnIdsOnly = false;
private $seriesId = null;
/**
*
* @param string $searchType
*/
public function __construct($searchType = self::SIMPLE)
{
//$this->invalidQCache();
$this->q = null;
if ($searchType === self::SIMPLE || $searchType === self::ADVANCED || $searchType === self::ALL_DOCS) {
$this->searchType = $searchType;
return;
}
if ($searchType === self::FACET_ONLY) {
$this->searchType = self::FACET_ONLY;
$this->setRows(0);
return;
}
if ($searchType === self::LATEST_DOCS) {
$this->searchType = self::LATEST_DOCS;
$this->sortField = 'server_date_published';
$this->sortOrder = 'desc';
return;
}
if ($searchType === self::DOC_ID) {
$this->searchType = self::DOC_ID;
return;
}
}
public function getSearchType()
{
return $this->searchType;
}
public function getFacetField()
{
return $this->facetField;
}
public function setFacetField($facetField)
{
$this->facetField = $facetField;
}
public function getStart()
{
return $this->start;
}
public function setStart($start)
{
$this->start = $start;
}
public static function getDefaultRows()
{
return SolrSearchQuery::getDefaultRows();
}
public function getRows()
{
return $this->rows;
}
public function setRows($rows)
{
$this->rows = $rows;
}
public function getSortField()
{
return $this->sortField;
}
public function setSortField($sortField)
{
if ($sortField === self::DEFAULT_SORTFIELD) {
if ($this->searchType === self::ALL_DOCS) {
// change the default sortfield for searchtype all
// since sorting by relevance does not make any sense here
$this->sortField = 'server_date_published';
} else {
$this->sortField = self::DEFAULT_SORTFIELD;
}
return;
}
$this->sortField = $sortField;
if (strpos($sortField, 'doc_sort_order_for_seriesid_') !== 0 && strpos($sortField, 'server_date_published') !== 0) {
// add _sort to the end of $sortField if not already done
$suffix = '_sort';
if (substr($sortField, strlen($sortField) - strlen($suffix)) !== $suffix) {
$this->sortField .= $suffix;
}
}
}
public function getSortOrder()
{
return $this->sortOrder;
}
public function setSortOrder($sortOrder)
{
$this->sortOrder = $sortOrder;
}
public function getSeriesId()
{
return $this->seriesId;
}
/**
*
* @return array An array that contains all specified filter queries.
*/
public function getFilterQueries()
{
return $this->filterQueries;
}
/**
*
* @param string $filterField The field that should be used in a filter query.
* @param string $filterValue The field value that should be used in a filter query.
*/
public function addFilterQuery($filterField, $filterValue)
{
if ($filterField == 'has_fulltext') {
$filterQuery = $filterField . ':' . $filterValue;
} else {
$filterQuery = '{!raw f=' . $filterField . '}' . $filterValue;
}
array_push($this->filterQueries, $filterQuery);
// we need to store the ID of the requested series here,
// since we need it later to build the index field name
if ($filterField === 'series_ids') {
$this->seriesId = $filterValue;
}
}
/**
*
* @param array $filterQueries An array of queries that should be used as filter queries.
*/
public function setFilterQueries($filterQueries)
{
$this->filterQueries = $filterQueries;
}
public function getCatchAll()
{
return $this->catchAll;
}
public function setCatchAll($catchAll)
{
$this->catchAll = $catchAll;
$this->invalidQCache();
}
/**
*
* @param string $name
* @param string $value
* @param string $modifier
*/
public function setField($name, $value, $modifier = self::SEARCH_MODIFIER_CONTAINS_ALL)
{
if (!empty($value)) {
$this->fieldValues[$name] = $value;
$this->modifier[$name] = $modifier;
$this->invalidQCache();
}
}
/**
*
* @param string $name
* @return Returns null if no values was specified for the given field name.
*/
public function getField($name)
{
if (array_key_exists($name, $this->fieldValues)) {
return $this->fieldValues[$name];
}
return null;
}
/**
*
* @param string $fieldname
* @return returns null if no modifier was specified for the given field name.
*/
public function getModifier($fieldname)
{
if (array_key_exists($fieldname, $this->modifier)) {
return $this->modifier[$fieldname];
}
return null;
}
public function getQ()
{
if (is_null($this->q)) {
// earlier cached query was marked as invalid: perform new setup of query cache
$this->q = $this->setupQCache();
}
// return cached result (caching is done here since building q is an expensive operation)
return $this->q;
}
private function setupQCache()
{
if ($this->searchType === self::SIMPLE) {
if ($this->getCatchAll() === '*:*') {
return $this->catchAll;
}
return $this->escape($this->getCatchAll());
}
if ($this->searchType === self::FACET_ONLY || $this->searchType === self::LATEST_DOCS || $this->searchType === self::ALL_DOCS) {
return '*:*';
}
if ($this->searchType === self::DOC_ID) {
return 'id:' . $this->fieldValues['id'];
}
return $this->buildAdvancedQString();
}
private function invalidQCache()
{
$this->q = null;
}
private function buildAdvancedQString()
{
$q = "{!lucene q.op=AND}";
$first = true;
foreach ($this->fieldValues as $fieldname => $fieldvalue) {
if ($first) {
$first = false;
} else {
$q .= ' ';
}
if ($this->modifier[$fieldname] === self::SEARCH_MODIFIER_CONTAINS_ANY) {
$q .= $this->combineSearchTerms($fieldname, $fieldvalue, 'OR');
continue;
}
if ($this->modifier[$fieldname] === self::SEARCH_MODIFIER_CONTAINS_NONE) {
$q .= '-' . $this->combineSearchTerms($fieldname, $fieldvalue, 'OR');
continue;
}
// self::SEARCH_MODIFIER_CONTAINS_ALL
$q .= $this->combineSearchTerms($fieldname, $fieldvalue);
}
return $q;
}
private function combineSearchTerms($fieldname, $fieldvalue, $conjunction = null)
{
$result = $fieldname . ':(';
$firstTerm = true;
$queryTerms = preg_split("/[\s]+/", $this->escape($fieldvalue), null, PREG_SPLIT_NO_EMPTY);
foreach ($queryTerms as $queryTerm) {
if ($firstTerm) {
$firstTerm = false;
} else {
$result .= is_null($conjunction) ? " " : " $conjunction ";
}
$result .= $queryTerm;
}
$result .= ')';
return $result;
}
public function disableEscaping()
{
$this->invalidQCache();
$this->escapingEnabled = false;
}
/**
* Escape Lucene's special query characters specified in
* http://lucene.apache.org/java/3_0_2/queryparsersyntax.html#Escaping%20Special%20Characters
* Escaping currently ignores * and ? which are used as wildcard operators.
* Additionally, double-quotes are not escaped and a double-quote is added to
* the end of $query in case it contains an odd number of double-quotes.
* @param string $query The query which needs to be escaped.
*/
private function escape($query)
{
if (!$this->escapingEnabled) {
return $query;
}
$query = trim($query);
// add one " to the end of $query if it contains an odd number of "
if (substr_count($query, '"') % 2 == 1) {
$query .= '"';
}
// escape special characters (currently ignore " \* \?) outside of ""
$insidePhrase = false;
$result = '';
foreach (explode('"', $query) as $phrase) {
if ($insidePhrase) {
$result .= '"' . $phrase . '"';
} else {
$result .= preg_replace(
'/(\+|-|&&|\|\||!|\(|\)|\{|}|\[|]|\^|~|:|\\\)/',
'\\\$1',
$this->lowercaseWildcardQuery($phrase)
);
}
$insidePhrase = !$insidePhrase;
}
return $result;
}
private function lowercaseWildcardQuery($query)
{
// check if $query is a wildcard query
if (strpos($query, '*') === false && strpos($query, '?') === false) {
return $query;
}
// lowercase query
return strtolower($query);
}
public function __toString()
{
if ($this->searchType === self::SIMPLE) {
return 'simple search with query ' . $this->getQ();
}
if ($this->searchType === self::FACET_ONLY) {
return 'facet only search with query *:*';
}
if ($this->searchType === self::LATEST_DOCS) {
return 'search for latest documents with query *:*';
}
if ($this->searchType === self::ALL_DOCS) {
return 'search for all documents';
}
if ($this->searchType === self::DOC_ID) {
return 'search for document id ' . $this->getQ();
}
return 'advanced search with query ' . $this->getQ();
}
/**
*
* @param boolean $returnIdsOnly
*/
public function setReturnIdsOnly($returnIdsOnly)
{
$this->returnIdsOnly = $returnIdsOnly;
}
/**
* @return boolean
*/
public function isReturnIdsOnly()
{
return $this->returnIdsOnly;
}
}

View file

@ -0,0 +1,94 @@
<?php
namespace App\Library\Util;
use App\Library\Search\SolariumAdapter;
use App\Library\Search\SearchResult;
use Illuminate\Support\Facades\Log;
class SolrSearchSearcher
{
/*
* Holds numbers of facets
*/
private $facetArray;
public function __construct()
{
}
/**
*
* @param SolrSearchQuery $query
* @param bool $validateDocIds check document IDs coming from Solr index against database
* @return SearchResult
* @throws //Opus_SolrSearch_Exception If Solr server responds with an error or the response is empty.
*/
public function search(SolrSearchQuery $query, bool $validateDocIds = true) : SearchResult
{
try {
//Opus_Log::get()->debug("query: " . $query->getQ());
// get service adapter for searching
// $service = SearchService::selectSearchingService( null, 'solr' );
$service = new SolariumAdapter("solr", config('solarium'));
$filterText = $query->getQ();//"*:*"
// basically create query
$requestParameter = $service->createQuery()
->setFilter($filterText)
->setStart($query->getStart())
->setRows($query->getRows());
//start:0
// rows:1
// fields:null
// sort:null
// union:null
// filter:"aa"
// facet:null
// subfilters:null
$requestParameter->setFields(array('*', 'score'));
$searchResult = $service->customSearch($requestParameter);
//if ( $validateDocIds )
//{
// $searchResult->dropLocallyMissingMatches();
//}
return $searchResult;
} catch (Exception $e) {
return $this->mapException(null, $e);
}
// catch ( Opus_Search_InvalidServiceException $e ) {
// return $this->mapException( Opus_SolrSearch_Exception::SERVER_UNREACHABLE, $e );
// }
// catch( Opus_Search_InvalidQueryException $e ) {
// return $this->mapException( Opus_SolrSearch_Exception::INVALID_QUERY, $e );
// }
}
/**
* @param mixed $type
* @param //Exception $previousException
* @throws //Opus_SolrSearch_Exception
* @return no-return
*/
private function mapException($type, Exception $previousException)
{
$msg = 'Solr server responds with an error ' . $previousException->getMessage();
//Opus_Log::get()->err($msg);
Log::error($msg);
//throw new Opus_SolrSearch_Exception($msg, $type, $previousException);
}
public function setFacetArray($array)
{
$this->facetArray = $array;
}
}