Server : Apache/2.4.41 (Ubuntu) System : Linux journalup 5.4.0-198-generic #218-Ubuntu SMP Fri Sep 27 20:18:53 UTC 2024 x86_64 User : www-data ( 33) PHP Version : 7.4.33 Disable Function : pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,pcntl_unshare, Directory : /var/www/html/lib/pkp/classes/search/ |
<?php /** * @file classes/search/SubmissionSearch.inc.php * * Copyright (c) 2014-2020 Simon Fraser University * Copyright (c) 2003-2020 John Willinsky * Distributed under the GNU GPL v3. For full terms see the file docs/COPYING. * * @class SubmissionSearch * @ingroup search * @see SubmissionSearchDAO * * @brief Class for retrieving search results. * * FIXME: NEAR; precedence w/o parens?; stemming; weighted counting */ // Search types define('SUBMISSION_SEARCH_AUTHOR', 0x00000001); define('SUBMISSION_SEARCH_TITLE', 0x00000002); define('SUBMISSION_SEARCH_ABSTRACT', 0x00000004); define('SUBMISSION_SEARCH_DISCIPLINE', 0x00000008); define('SUBMISSION_SEARCH_SUBJECT', 0x00000010); define('SUBMISSION_SEARCH_KEYWORD', 0x00000011); define('SUBMISSION_SEARCH_TYPE', 0x00000020); define('SUBMISSION_SEARCH_COVERAGE', 0x00000040); define('SUBMISSION_SEARCH_GALLEY_FILE', 0x00000080); define('SUBMISSION_SEARCH_SUPPLEMENTARY_FILE', 0x00000100); define('SUBMISSION_SEARCH_INDEX_TERMS', 0x00000078); define('SUBMISSION_SEARCH_DEFAULT_RESULT_LIMIT', 20); import('lib.pkp.classes.search.SubmissionSearchIndex'); abstract class SubmissionSearch { /** * Constructor */ function __construct() { } /** * Parses a search query string. * Supports +/-, AND/OR, parens * @param $query * @return array of the form ('+' => <required>, '' => <optional>, '-' => excluded) */ function _parseQuery($query) { $count = PKPString::regexp_match_all('/(\+|\-|)("[^"]+"|\(|\)|[^\s\)]+)/', $query, $matches); $pos = 0; return $this->_parseQueryInternal($matches[1], $matches[2], $pos, $count); } /** * Query parsing helper routine. * Returned structure is based on that used by the Search::QueryParser Perl module. */ function _parseQueryInternal($signTokens, $tokens, &$pos, $total) { $return = array('+' => array(), '' => array(), '-' => array()); $postBool = $preBool = ''; $submissionSearchIndex = Application::getSubmissionSearchIndex(); $notOperator = PKPString::strtolower(__('search.operator.not')); $andOperator = PKPString::strtolower(__('search.operator.and')); $orOperator = PKPString::strtolower(__('search.operator.or')); while ($pos < $total) { if (!empty($signTokens[$pos])) $sign = $signTokens[$pos]; else if (empty($sign)) $sign = '+'; $token = PKPString::strtolower($tokens[$pos++]); switch ($token) { case $notOperator: $sign = '-'; break; case ')': return $return; case '(': $token = $this->_parseQueryInternal($signTokens, $tokens, $pos, $total); default: $postBool = ''; if ($pos < $total) { $peek = PKPString::strtolower($tokens[$pos]); if ($peek == $orOperator) { $postBool = 'or'; $pos++; } else if ($peek == $andOperator) { $postBool = 'and'; $pos++; } } $bool = empty($postBool) ? $preBool : $postBool; $preBool = $postBool; if ($bool == 'or') $sign = ''; if (is_array($token)) { $k = $token; } else { $k = $submissionSearchIndex->filterKeywords($token, true); } if (!empty($k)) $return[$sign][] = $k; $sign = ''; break; } } return $return; } /** * Takes an unordered list of search result data, flattens it, orders it * and excludes unwanted results. * @param $unorderedResults array An unordered list of search data (article ID * as key and ranking data as values). * @param $orderBy string One of the values returned by ArticleSearch::getResultSetOrderingOptions(); * @param $orderDir string 'asc' or 'desc', see ArticleSearch::getResultSetOrderingDirectionOptions(); * @param $exclude array A list of article IDs to exclude from the result. * @return array An ordered and flattened list of article IDs. */ function _getMergedArray($context, &$keywords, $publishedFrom, $publishedTo) { $resultsPerKeyword = Config::getVar('search', 'results_per_keyword'); $resultCacheHours = Config::getVar('search', 'result_cache_hours'); if (!is_numeric($resultsPerKeyword)) $resultsPerKeyword = 100; if (!is_numeric($resultCacheHours)) $resultCacheHours = 24; $mergedKeywords = array('+' => array(), '' => array(), '-' => array()); foreach ($keywords as $type => $keyword) { if (!empty($keyword['+'])) $mergedKeywords['+'][] = array('type' => $type, '+' => $keyword['+'], '' => array(), '-' => array()); if (!empty($keyword[''])) $mergedKeywords[''][] = array('type' => $type, '+' => array(), '' => $keyword[''], '-' => array()); if (!empty($keyword['-'])) $mergedKeywords['-'][] = array('type' => $type, '+' => array(), '' => $keyword['-'], '-' => array()); } return $this->_getMergedKeywordResults($context, $mergedKeywords, null, $publishedFrom, $publishedTo, $resultsPerKeyword, $resultCacheHours); } /** * Recursive helper for _getMergedArray. */ function _getMergedKeywordResults($context, &$keyword, $type, $publishedFrom, $publishedTo, $resultsPerKeyword, $resultCacheHours) { $mergedResults = null; if (isset($keyword['type'])) { $type = $keyword['type']; } foreach ($keyword['+'] as $phrase) { $results = $this->_getMergedPhraseResults($context, $phrase, $type, $publishedFrom, $publishedTo, $resultsPerKeyword, $resultCacheHours); if ($mergedResults === null) { $mergedResults = $results; } else { foreach ($mergedResults as $submissionId => $data) { if (isset($results[$submissionId])) { $mergedResults[$submissionId]['count'] += $results[$submissionId]['count']; } else { unset($mergedResults[$submissionId]); } } } } if ($mergedResults == null) { $mergedResults = array(); } if (!empty($mergedResults) || empty($keyword['+'])) { foreach ($keyword[''] as $phrase) { $results = $this->_getMergedPhraseResults($context, $phrase, $type, $publishedFrom, $publishedTo, $resultsPerKeyword, $resultCacheHours); foreach ($results as $submissionId => $data) { if (isset($mergedResults[$submissionId])) { $mergedResults[$submissionId]['count'] += $data['count']; } else if (empty($keyword['+'])) { $mergedResults[$submissionId] = $data; } } } foreach ($keyword['-'] as $phrase) { $results = $this->_getMergedPhraseResults($context, $phrase, $type, $publishedFrom, $publishedTo, $resultsPerKeyword, $resultCacheHours); foreach ($results as $submissionId => $count) { if (isset($mergedResults[$submissionId])) { unset($mergedResults[$submissionId]); } } } } return $mergedResults; } /** * Recursive helper for _getMergedArray. */ function _getMergedPhraseResults($context, &$phrase, $type, $publishedFrom, $publishedTo, $resultsPerKeyword, $resultCacheHours) { if (isset($phrase['+'])) { return $this->_getMergedKeywordResults($context, $phrase, $type, $publishedFrom, $publishedTo, $resultsPerKeyword, $resultCacheHours); } $mergedResults = array(); $searchDao = $this->getSearchDao(); return $searchDao->getPhraseResults( $context, $phrase, $publishedFrom, $publishedTo, $type, $resultsPerKeyword, $resultCacheHours ); } /** * Return an array of search results matching the supplied * keyword IDs in decreasing order of match quality. * Keywords are supplied in an array of the following format: * $keywords[SUBMISSION_SEARCH_AUTHOR] = array('John', 'Doe'); * $keywords[SUBMISSION_SEARCH_...] = array(...); * $keywords[null] = array('Matches', 'All', 'Fields'); * @param $request Request * @param $context object The context to search * @param $keywords array List of keywords * @param $error string a reference to a variable that will * contain an error message if the search service produces * an error. * @param $publishedFrom object Search-from date * @param $publishedTo object Search-to date * @param $rangeInfo Information on the range of results to return * @param $exclude array An array of article IDs to exclude from the result. * @return VirtualArrayIterator An iterator with one entry per retrieved * article containing the article, published submission, issue, context, etc. */ function retrieveResults($request, $context, $keywords, &$error, $publishedFrom = null, $publishedTo = null, $rangeInfo = null, $exclude = array()) { // Pagination if ($rangeInfo && $rangeInfo->isValid()) { $page = $rangeInfo->getPage(); $itemsPerPage = $rangeInfo->getCount(); } else { $page = 1; $itemsPerPage = SUBMISSION_SEARCH_DEFAULT_RESULT_LIMIT; } // Result set ordering. list($orderBy, $orderDir) = $this->getResultSetOrdering($request); // Check whether a search plug-in jumps in to provide ranked search results. $totalResults = null; $hookResult = HookRegistry::call( 'SubmissionSearch::retrieveResults', array(&$context, &$keywords, $publishedFrom, $publishedTo, $orderBy, $orderDir, $exclude, $page, $itemsPerPage, &$totalResults, &$error, &$results) ); // If no search plug-in is activated then fall back to the // default database search implementation. if ($hookResult === false) { // Parse the query. foreach($keywords as $searchType => $query) { $keywords[$searchType] = $this->_parseQuery($query); } // Fetch all the results from all the keywords into one array // (mergedResults), where mergedResults[submission_id] // = sum of all the occurences for all keywords associated with // that article ID. $mergedResults = $this->_getMergedArray($context, $keywords, $publishedFrom, $publishedTo); // Convert mergedResults into an array (frequencyIndicator => // $submissionId). // The frequencyIndicator is a synthetically-generated number, // where higher is better, indicating the quality of the match. // It is generated here in such a manner that matches with // identical frequency do not collide. $results = $this->getSparseArray($mergedResults, $orderBy, $orderDir, $exclude); $totalResults = count($results); // Use only the results for the specified page. $offset = $itemsPerPage * ($page-1); $length = max($totalResults - $offset, 0); $length = min($itemsPerPage, $length); if ($length == 0) { $results = array(); } else { $results = array_slice( $results, $offset, $length ); } } // Take the range of results and retrieve the Article, Journal, // and associated objects. $results = $this->formatResults($results, $request->getUser()); // Return the appropriate iterator. import('lib.pkp.classes.core.VirtualArrayIterator'); return new VirtualArrayIterator($results, $totalResults, $page, $itemsPerPage); } /** * Return the available options for the result * set ordering direction. * @return array */ function getResultSetOrderingDirectionOptions() { return array( 'asc' => __('search.results.orderDir.asc'), 'desc' => __('search.results.orderDir.desc') ); } /** * Return the currently selected result * set ordering option (default: descending relevance). * @param $request Request * @return array An array with the order field as the * first entry and the order direction as the second * entry. */ function getResultSetOrdering($request) { // Order field. $orderBy = $request->getUserVar('orderBy'); $orderByOptions = $this->getResultSetOrderingOptions($request); if (is_null($orderBy) || !in_array($orderBy, array_keys($orderByOptions))) { $orderBy = 'score'; } // Ordering direction. $orderDir = $request->getUserVar('orderDir'); $orderDirOptions = $this->getResultSetOrderingDirectionOptions(); if (is_null($orderDir) || !in_array($orderDir, array_keys($orderDirOptions))) { $orderDir = $this->getDefaultOrderDir($orderBy); } return array($orderBy, $orderDir); } // // Methods to be implemented by subclasses. // /** * See implementation of retrieveResults for a description of this * function. * * Note that this function is also called externally to fetch * results for the title index, and possibly elsewhere. * * @param $results array * @param $user User optional (if availability information is desired) * @return array */ abstract function formatResults($results, $user = null); /** * Return the available options for result set ordering. * @param $request Request * @return array */ abstract function getResultSetOrderingOptions($request); /** * See implementation of retrieveResults for a description of this * function. */ abstract protected function getSparseArray($unorderedResults, $orderBy, $orderDir, $exclude); /** * Return the default order direction. * @param $orderBy string * @return string */ abstract protected function getDefaultOrderDir($orderBy); /** * Return the search DAO * @return DAO */ abstract protected function getSearchDao(); }