From fb3935fcb9295a449c013248ae9f5d3fa9d3d5e5 Mon Sep 17 00:00:00 2001 From: Jonas Raoni Soares da Silva Date: Tue, 11 Apr 2023 15:23:43 +0300 Subject: [PATCH 01/10] pkp/pkp-lib#8710 Added filter to exclude submission IDs --- classes/submission/Collector.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/classes/submission/Collector.php b/classes/submission/Collector.php index f2c219e8904..824a8d06cde 100644 --- a/classes/submission/Collector.php +++ b/classes/submission/Collector.php @@ -62,6 +62,7 @@ abstract class Collector implements CollectorInterface public ?array $stageIds = null; public ?array $doiStatuses = null; public ?bool $hasDois = null; + public ?array $excludeIds = null; /** @var array Which DOI types should be considered when checking if a submission has DOIs set */ public array $enabledDoiTypes = []; @@ -210,6 +211,15 @@ public function searchPhrase(?string $phrase): AppCollector return $this; } + /** + * Ensure the given submission IDs are not included + */ + public function excludeIds(?array $ids): AppCollector + { + $this->excludeIds = $ids; + return $this; + } + /** * Limit the number of objects retrieved */ @@ -444,6 +454,9 @@ public function getQueryBuilder(): Builder $this->addHasDoisFilterToQuery($q); }); + // By whether any child pub objects have DOIs assigned + $q->when($this->excludeIds !== null, fn (Builder $q) => $q->whereNotIn('s.submission_id', $this->excludeIds)); + // Limit and offset results for pagination if (isset($this->count)) { $q->limit($this->count); From 3415f3dcbba684fefa3436792e5a9b7734f0672a Mon Sep 17 00:00:00 2001 From: Jonas Raoni Soares da Silva Date: Tue, 11 Apr 2023 15:30:12 +0300 Subject: [PATCH 02/10] pkp/pkp-lib#8710 Updated assignedTo exception to ignore only the author name --- classes/submission/Collector.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/classes/submission/Collector.php b/classes/submission/Collector.php index 824a8d06cde..e790d143dfe 100644 --- a/classes/submission/Collector.php +++ b/classes/submission/Collector.php @@ -428,7 +428,13 @@ public function getQueryBuilder(): Builder ->when(is_array($this->assignedTo), function ($q) { $q->leftJoin('review_assignments AS ra', 'ra.submission_id', '=', 'p.submission_id') ->whereIn('ra.reviewer_id', $this->assignedTo) - ->whereNull('ra.reviewer_id'); + ->where(fn (Builder $q) => $q + ->whereNull('ra.reviewer_id') + ->orWhereNotIn('aus.setting_name', [ + Identity::IDENTITY_SETTING_GIVENNAME, + Identity::IDENTITY_SETTING_FAMILYNAME + ]) + ); }) ->where(DB::raw('lower(aus.setting_value)'), 'LIKE', $likePattern)->addBinding($word); }); From 10c3aca25b92db4be29b03a9923ab80ae55387ed Mon Sep 17 00:00:00 2001 From: Jonas Raoni Soares da Silva Date: Wed, 12 Apr 2023 23:45:59 +0300 Subject: [PATCH 03/10] pkp/pkp-lib#8710 Added $allowShortWords to the SubmissionSearchIndex::filterKeywords() --- classes/search/SubmissionSearchIndex.php | 44 +++++++++++------------- 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/classes/search/SubmissionSearchIndex.php b/classes/search/SubmissionSearchIndex.php index 0db9dd85207..3a310a6a89d 100644 --- a/classes/search/SubmissionSearchIndex.php +++ b/classes/search/SubmissionSearchIndex.php @@ -30,15 +30,15 @@ abstract class SubmissionSearchIndex /** * Split a string into a clean array of keywords * - * @param string $text + * @param string|array $text * @param bool $allowWildcards * - * @return array of keywords + * @return string[] of keywords */ - public function filterKeywords($text, $allowWildcards = false) + public static function filterKeywords($text, $allowWildcards = false, bool $allowShortWords = false): array { $minLength = Config::getVar('search', 'min_word_length'); - $stopwords = $this->_loadStopwords(); + $stopwords = static::loadStopwords(); // Join multiple lines into a single string if (is_array($text)) { @@ -56,11 +56,11 @@ public function filterKeywords($text, $allowWildcards = false) // FIXME Do not perform further filtering for some fields, e.g., author names? - // Remove stopwords $keywords = []; - foreach ($words as $k) { - if (!isset($stopwords[$k]) && PKPString::strlen($k) >= $minLength && !is_numeric($k)) { - $keywords[] = PKPString::substr($k, 0, self::SEARCH_KEYWORD_MAX_LENGTH); + foreach ($words as $word) { + // Ignores: stop words, short words (when $allowShortWords is false) and words composed solely of numbers + if (empty($stopwords[$word]) && ($allowShortWords || PKPString::strlen($word) >= $minLength) && !is_numeric($word)) { + $keywords[] = PKPString::substr($word, 0, static::SEARCH_KEYWORD_MAX_LENGTH); } } return $keywords; @@ -70,26 +70,22 @@ public function filterKeywords($text, $allowWildcards = false) * Return list of stopwords. * FIXME: Should this be locale-specific? * - * @return array with stopwords as keys + * @return array Stop words (in lower case) as keys and 1 as value */ - protected function _loadStopwords() + protected static function loadStopwords() { static $searchStopwords; - if (!isset($searchStopwords)) { - // Load stopwords only once per request - $searchStopwords = array_count_values( - array_filter( - array_map('trim', file(dirname(__FILE__, 5) . '/' . self::SEARCH_STOPWORDS_FILE)), - function ($a) { - return !empty($a) && $a[0] != '#'; - } - ) - ); - $searchStopwords[''] = 1; - } - - return $searchStopwords; + return $searchStopwords ??= array_fill_keys( + collect(file(base_path(static::SEARCH_STOPWORDS_FILE))) + ->map(fn (string $word) => trim($word)) + // Ignore comments/line-breaks + ->filter(fn (string $word) => !empty($word) && $word[0] !== '#') + // Include a map for empty words + ->push('') + ->toArray(), + 1 + ); } /** From f8d67f86a2540aee3f5e35d29b476da2fcb3cb64 Mon Sep 17 00:00:00 2001 From: Jonas Raoni Soares da Silva Date: Wed, 12 Apr 2023 23:48:20 +0300 Subject: [PATCH 04/10] pkp/pkp-lib#8710 Updated searchPhrase filter to look for matches at the indexed content, and to use a "OR" based search instead to bring more results --- classes/submission/Collector.php | 111 ++++++++++++++++++++----------- 1 file changed, 72 insertions(+), 39 deletions(-) diff --git a/classes/submission/Collector.php b/classes/submission/Collector.php index e790d143dfe..818f953293c 100644 --- a/classes/submission/Collector.php +++ b/classes/submission/Collector.php @@ -28,6 +28,7 @@ use PKP\facades\Locale; use PKP\identity\Identity; use PKP\plugins\Hook; +use PKP\search\SubmissionSearch; use PKP\security\Role; use PKP\submission\reviewRound\ReviewRound; @@ -290,6 +291,8 @@ public function getQueryBuilder(): Builder $q->whereIn('s.context_id', $this->contextIds); } + // Prepare keywords, but allows short words + $keywords = collect(Application::getSubmissionSearchIndex()->filterKeywords($this->searchPhrase, false, true))->unique(); switch ($this->orderBy) { case self::ORDERBY_DATE_PUBLISHED: $q->addSelect(['po.date_published']); @@ -403,46 +406,76 @@ public function getQueryBuilder(): Builder ->where(DB::raw('(' . $sub->toSql() . ')'), '=', '0'); } - // search phrase - if ($this->searchPhrase !== null) { - $likePattern = DB::raw("CONCAT('%', LOWER(?), '%')"); - $words = explode(' ', $this->searchPhrase); - foreach ($words as $word) { - $q->where(function ($query) use ($word, $likePattern) { - $query->whereIn('s.submission_id', function ($query) use ($word, $likePattern) { - $query->select('p.submission_id')->from('publications AS p') - ->join('publication_settings AS ps', 'p.publication_id', '=', 'ps.publication_id') - ->where('ps.setting_name', '=', 'title') - ->where(DB::raw('LOWER(ps.setting_value)'), 'LIKE', $likePattern)->addBinding($word); - }); - $query->orWhereIn('s.submission_id', function ($query) use ($word, $likePattern) { - $query->select('p.submission_id')->from('publications AS p') - ->join('authors AS au', 'au.publication_id', '=', 'p.publication_id') - ->join('author_settings AS aus', 'aus.author_id', '=', 'au.author_id') - ->whereIn('aus.setting_name', [ - Identity::IDENTITY_SETTING_GIVENNAME, - Identity::IDENTITY_SETTING_FAMILYNAME, - 'orcid' - ]) - // Don't permit reviewers to search on author names - ->when(is_array($this->assignedTo), function ($q) { - $q->leftJoin('review_assignments AS ra', 'ra.submission_id', '=', 'p.submission_id') - ->whereIn('ra.reviewer_id', $this->assignedTo) - ->where(fn (Builder $q) => $q - ->whereNull('ra.reviewer_id') - ->orWhereNotIn('aus.setting_name', [ - Identity::IDENTITY_SETTING_GIVENNAME, - Identity::IDENTITY_SETTING_FAMILYNAME - ]) - ); - }) - ->where(DB::raw('lower(aus.setting_value)'), 'LIKE', $likePattern)->addBinding($word); - }); - if (ctype_digit((string) $word)) { - $query->orWhere('s.submission_id', '=', $word); - } - }); + // Search phrase + if ($keywords->count()) { + if(!empty($this->assignedTo)) { + // Holds a single random row to check whether we have any assignment + $q->leftJoinSub(fn (Builder $q) => $q + ->from('review_assignments', 'ra') + ->whereIn('ra.reviewer_id', $this->assignedTo) + ->select(DB::raw('1 AS value')) + ->limit(1), + 'any_assignment', 'any_assignment.value', '=', DB::raw('1') + ); } + $likePattern = DB::raw("CONCAT('%', LOWER(?), '%')"); + // Builds the filters + $q->where(fn (Builder $q) => $keywords + ->map(fn (string $keyword) => $q + // Look for matches on the indexed data + ->orWhereExists(fn (Builder $query) => $query + ->from('submission_search_objects', 'sso') + ->join('submission_search_object_keywords AS ssok', 'sso.object_id', '=', 'ssok.object_id') + ->join("submission_search_keyword_list AS sskl", "sskl.keyword_id", '=', "ssok.keyword_id") + ->where("sskl.keyword_text", '=', DB::raw("CONCAT(LOWER(?), '%')"))->addBinding($keyword) + ->whereColumn('s.submission_id', '=', 'sso.submission_id') + // Don't permit reviewers to search on author names + ->when(!empty($this->assignedTo), fn (Builder $q) => $q + ->where(fn (Builder $q) => $q + ->whereNull('any_assignment.value') + ->orWhere('sso.type', '!=', SubmissionSearch::SUBMISSION_SEARCH_AUTHOR) + ) + ) + ) + // Search on the publication title + ->orWhereIn('s.submission_id', fn (Builder $query) => $query + ->select('p.submission_id')->from('publications AS p') + ->join('publication_settings AS ps', 'p.publication_id', '=', 'ps.publication_id') + ->where('ps.setting_name', '=', 'title') + ->where(DB::raw('LOWER(ps.setting_value)'), 'LIKE', $likePattern) + ->addBinding($keyword) + ) + // Search on the author name and ORCID + ->orWhereIn('s.submission_id', fn (Builder $query) => $query + ->select('p.submission_id') + ->from('publications AS p') + ->join('authors AS au', 'au.publication_id', '=', 'p.publication_id') + ->join('author_settings AS aus', 'aus.author_id', '=', 'au.author_id') + ->whereIn('aus.setting_name', [ + Identity::IDENTITY_SETTING_GIVENNAME, + Identity::IDENTITY_SETTING_FAMILYNAME, + 'orcid' + ]) + // Don't permit reviewers to search on author names + ->when(!empty($this->assignedTo), fn (Builder $q) => $q + ->where(fn (Builder $q) => $q + ->whereNull('any_assignment.value') + ->orWhereNotIn('aus.setting_name', [ + Identity::IDENTITY_SETTING_GIVENNAME, + Identity::IDENTITY_SETTING_FAMILYNAME + ]) + ) + ) + ->where(DB::raw('LOWER(aus.setting_value)'), 'LIKE', $likePattern) + ->addBinding($keyword) + ) + // Search for exact submission ID + ->when( + ($numericWords = $keywords->filter(fn (string $keyword) => ctype_digit($keyword)))->count(), + fn (Builder $query) => $query->orWhereIn('s.submission_id', $numericWords) + ) + ) + ); } if (isset($this->categoryIds)) { From 5a6b884b7feea9d83e4cf6cd2c541c8636e80fda Mon Sep 17 00:00:00 2001 From: Jonas Raoni Soares da Silva Date: Wed, 12 Apr 2023 23:50:18 +0300 Subject: [PATCH 05/10] pkp/pkp-lib#8710 Added sort by search ranking --- classes/submission/Collector.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/classes/submission/Collector.php b/classes/submission/Collector.php index 818f953293c..d28b98fa2b4 100644 --- a/classes/submission/Collector.php +++ b/classes/submission/Collector.php @@ -43,6 +43,7 @@ abstract class Collector implements CollectorInterface public const ORDERBY_LAST_MODIFIED = 'lastModified'; public const ORDERBY_SEQUENCE = 'sequence'; public const ORDERBY_TITLE = 'title'; + public const ORDERBY_SEARCH_RANKING = 'ranking'; public const ORDER_DIR_ASC = 'ASC'; public const ORDER_DIR_DESC = 'DESC'; @@ -327,6 +328,23 @@ public function getQueryBuilder(): Builder $q->addSelect([DB::raw($coalesceTitles)]); $q->orderBy(DB::raw($coalesceTitles), $this->orderDirection); break; + case self::ORDERBY_SEARCH_RANKING: + if (!$keywords->count()) { + $q->orderBy('s.date_submitted', $this->orderDirection); + break; + } + // Retrieves the number of matches for all keywords + $orderByMatchCount = DB::table('submission_search_objects', 'sso') + ->join("submission_search_object_keywords AS ssok", "ssok.object_id", '=', 'sso.object_id') + ->join("submission_search_keyword_list AS sskl", "sskl.keyword_id", '=', "ssok.keyword_id") + ->whereIn("sskl.keyword_text", $keywords->map(fn () => DB::raw("CONCAT(LOWER(?), '%')")))->addBinding($keywords->toArray()) + ->whereColumn('s.submission_id', '=', 'sso.submission_id') + ->selectRaw('COUNT(0)'); + // Retrieves the number of distinct matched keywords + $orderByDistinctKeyword = (clone $orderByMatchCount)->groupBy('sskl.keyword_id'); + $q->orderBy($orderByDistinctKeyword, $this->orderDirection) + ->orderBy($orderByMatchCount, $this->orderDirection); + break; case self::ORDERBY_DATE_SUBMITTED: default: $q->orderBy('s.date_submitted', $this->orderDirection); From 9b66011afc007cb59624ffbd7207b98a245cdb7e Mon Sep 17 00:00:00 2001 From: Jonas Raoni Soares da Silva Date: Wed, 12 Apr 2023 23:50:47 +0300 Subject: [PATCH 06/10] pkp/pkp-lib#8710 Formatting --- classes/submission/Collector.php | 79 +++++++++++++++----------------- 1 file changed, 37 insertions(+), 42 deletions(-) diff --git a/classes/submission/Collector.php b/classes/submission/Collector.php index d28b98fa2b4..f2f5f429a58 100644 --- a/classes/submission/Collector.php +++ b/classes/submission/Collector.php @@ -312,18 +312,18 @@ public function getQueryBuilder(): Builder case self::ORDERBY_TITLE: $locale = Locale::getLocale(); $q->leftJoin('publications as publication_tlp', 's.current_publication_id', '=', 'publication_tlp.publication_id') - ->leftJoin('publication_settings as publication_tlps', function (JoinClause $join) use ($locale) { + ->leftJoin('publication_settings as publication_tlps', fn (JoinClause $join) => $join->on('publication_tlp.publication_id', '=', 'publication_tlps.publication_id') ->where('publication_tlps.setting_name', '=', 'title') ->where('publication_tlps.setting_value', '!=', '') - ->where('publication_tlps.locale', '=', $locale); - }); + ->where('publication_tlps.locale', '=', $locale) + ); $q->leftJoin('publications as publication_tlpl', 's.current_publication_id', '=', 'publication_tlpl.publication_id') - ->leftJoin('publication_settings as publication_tlpsl', function (JoinClause $join) { + ->leftJoin('publication_settings as publication_tlpsl', fn (JoinClause $join) => $join->on('publication_tlp.publication_id', '=', 'publication_tlpsl.publication_id') ->on('publication_tlpsl.locale', '=', 's.locale') - ->where('publication_tlpsl.setting_name', '=', 'title'); - }); + ->where('publication_tlpsl.setting_name', '=', 'title') + ); $coalesceTitles = 'COALESCE(publication_tlps.setting_value, publication_tlpsl.setting_value)'; $q->addSelect([DB::raw($coalesceTitles)]); $q->orderBy(DB::raw($coalesceTitles), $this->orderDirection); @@ -369,10 +369,10 @@ public function getQueryBuilder(): Builder if ($this->isOverdue) { $q->leftJoin('review_assignments as raod', 'raod.submission_id', '=', 's.submission_id') - ->leftJoin('review_rounds as rr', function ($table) { - $table->on('rr.submission_id', '=', 's.submission_id'); - $table->on('raod.review_round_id', '=', 'rr.review_round_id'); - }); + ->leftJoin('review_rounds as rr', fn (Builder $table) => + $table->on('rr.submission_id', '=', 's.submission_id') + ->on('raod.review_round_id', '=', 'rr.review_round_id') + ); // Only get overdue assignments on active review rounds $q->whereNotIn('rr.status', [ ReviewRound::REVIEW_ROUND_STATUS_RESUBMIT_FOR_REVIEW, @@ -380,38 +380,37 @@ public function getQueryBuilder(): Builder ReviewRound::REVIEW_ROUND_STATUS_ACCEPTED, ReviewRound::REVIEW_ROUND_STATUS_DECLINED, ]); - $q->where(function ($q) { - $q->where('raod.declined', '<>', 1); - $q->where('raod.cancelled', '<>', 1); - $q->where(function ($q) { - $q->where('raod.date_due', '<', Core::getCurrentDate(strtotime('tomorrow'))); - $q->whereNull('raod.date_completed'); - }); - $q->orWhere(function ($q) { - $q->where('raod.date_response_due', '<', Core::getCurrentDate(strtotime('tomorrow'))); - $q->whereNull('raod.date_confirmed'); - }); - }); + $q->where(fn (Builder $q) => + $q->where('raod.declined', '<>', 1) + ->where('raod.cancelled', '<>', 1) + ->where(fn (Builder $q) => + $q->where('raod.date_due', '<', Core::getCurrentDate(strtotime('tomorrow'))) + ->whereNull('raod.date_completed') + ) + ->orWhere(fn (Builder $q) => + $q->where('raod.date_response_due', '<', Core::getCurrentDate(strtotime('tomorrow'))) + ->whereNull('raod.date_confirmed') + ) + ); } if (is_array($this->assignedTo)) { - $q->whereIn('s.submission_id', function ($q) { + $q->whereIn('s.submission_id', fn (Builder $q) => $q->select('s.submission_id') ->from('submissions AS s') - ->leftJoin('stage_assignments as sa', function ($q) { + ->leftJoin('stage_assignments as sa', fn (Builder $q) => $q->on('s.submission_id', '=', 'sa.submission_id') - ->whereIn('sa.user_id', $this->assignedTo); - }); - - $q->leftJoin('review_assignments as ra', function ($table) { - $table->on('s.submission_id', '=', 'ra.submission_id'); - $table->where('ra.declined', '=', (int) 0); - $table->where('ra.cancelled', '=', (int) 0); - $table->whereIn('ra.reviewer_id', $this->assignedTo); - }); - $q->whereNotNull('sa.stage_assignment_id') - ->orWhereNotNull('ra.review_id'); - }); + ->whereIn('sa.user_id', $this->assignedTo) + ) + ->leftJoin('review_assignments as ra', fn (Builder $table) => + $table->on('s.submission_id', '=', 'ra.submission_id') + ->where('ra.declined', '=', (int) 0) + ->where('ra.cancelled', '=', (int) 0) + ->whereIn('ra.reviewer_id', $this->assignedTo) + ) + ->whereNotNull('sa.stage_assignment_id') + ->orWhereNotNull('ra.review_id') + ); } elseif ($this->assignedTo === self::UNASSIGNED) { $sub = DB::table('stage_assignments') ->select(DB::raw('count(stage_assignments.stage_assignment_id)')) @@ -502,14 +501,10 @@ public function getQueryBuilder(): Builder } // By any child pub object's DOI status - $q->when($this->doiStatuses !== null, function (Builder $q) { - $this->addDoiStatusFilterToQuery($q); - }); + $q->when($this->doiStatuses !== null, fn (Builder $q) => $this->addDoiStatusFilterToQuery($q)); // By whether any child pub objects have DOIs assigned - $q->when($this->hasDois !== null, function (Builder $q) { - $this->addHasDoisFilterToQuery($q); - }); + $q->when($this->hasDois !== null, fn (Builder $q) => $this->addHasDoisFilterToQuery($q)); // By whether any child pub objects have DOIs assigned $q->when($this->excludeIds !== null, fn (Builder $q) => $q->whereNotIn('s.submission_id', $this->excludeIds)); From eb9b5ba083dfde75bc7f48223a7bcfcc6e4ada8d Mon Sep 17 00:00:00 2001 From: Jonas Raoni Soares da Silva Date: Thu, 13 Apr 2023 16:48:01 +0300 Subject: [PATCH 07/10] pkp/pkp-lib#8710 Added $allowNumericWords to the SubmissionSearchIndex::filterKeywords() --- classes/search/SubmissionSearchIndex.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/classes/search/SubmissionSearchIndex.php b/classes/search/SubmissionSearchIndex.php index 3a310a6a89d..76b2c61e461 100644 --- a/classes/search/SubmissionSearchIndex.php +++ b/classes/search/SubmissionSearchIndex.php @@ -35,7 +35,7 @@ abstract class SubmissionSearchIndex * * @return string[] of keywords */ - public static function filterKeywords($text, $allowWildcards = false, bool $allowShortWords = false): array + public static function filterKeywords($text, $allowWildcards = false, bool $allowShortWords = false, bool $allowNumericWords = false): array { $minLength = Config::getVar('search', 'min_word_length'); $stopwords = static::loadStopwords(); @@ -58,8 +58,8 @@ public static function filterKeywords($text, $allowWildcards = false, bool $allo $keywords = []; foreach ($words as $word) { - // Ignores: stop words, short words (when $allowShortWords is false) and words composed solely of numbers - if (empty($stopwords[$word]) && ($allowShortWords || PKPString::strlen($word) >= $minLength) && !is_numeric($word)) { + // Ignores: stop words, short words (when $allowShortWords is false) and words composed solely of numbers (when $allowNumericWords is false) + if (empty($stopwords[$word]) && ($allowShortWords || PKPString::strlen($word) >= $minLength) && ($allowNumericWords || !is_numeric($word))) { $keywords[] = PKPString::substr($word, 0, static::SEARCH_KEYWORD_MAX_LENGTH); } } From ee59ab4545ef67fa9c298b2e8e37b0247b6388b8 Mon Sep 17 00:00:00 2001 From: Jonas Raoni Soares da Silva Date: Fri, 25 Aug 2023 16:58:52 +0300 Subject: [PATCH 08/10] pkp/pkp-lib#8710 Fixed sorting and numeric search (submission ID) --- classes/submission/Collector.php | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/classes/submission/Collector.php b/classes/submission/Collector.php index f2f5f429a58..d45a8a65534 100644 --- a/classes/submission/Collector.php +++ b/classes/submission/Collector.php @@ -292,8 +292,10 @@ public function getQueryBuilder(): Builder $q->whereIn('s.context_id', $this->contextIds); } - // Prepare keywords, but allows short words - $keywords = collect(Application::getSubmissionSearchIndex()->filterKeywords($this->searchPhrase, false, true))->unique(); + // Prepare keywords (allows short and numeric words) + $keywords = collect(Application::getSubmissionSearchIndex()->filterKeywords($this->searchPhrase, false, true, true))->unique(); + + // Setup the order by switch ($this->orderBy) { case self::ORDERBY_DATE_PUBLISHED: $q->addSelect(['po.date_published']); @@ -335,13 +337,18 @@ public function getQueryBuilder(): Builder } // Retrieves the number of matches for all keywords $orderByMatchCount = DB::table('submission_search_objects', 'sso') - ->join("submission_search_object_keywords AS ssok", "ssok.object_id", '=', 'sso.object_id') - ->join("submission_search_keyword_list AS sskl", "sskl.keyword_id", '=', "ssok.keyword_id") - ->whereIn("sskl.keyword_text", $keywords->map(fn () => DB::raw("CONCAT(LOWER(?), '%')")))->addBinding($keywords->toArray()) + ->join('submission_search_object_keywords AS ssok', 'ssok.object_id', '=', 'sso.object_id') + ->join('submission_search_keyword_list AS sskl', 'sskl.keyword_id', '=', 'ssok.keyword_id') + ->where(fn (Builder $q) => + $keywords->map(fn (string $keyword) => $q + ->orWhere('sskl.keyword_text', '=', DB::raw('LOWER(?)')) + ->addBinding($keyword) + ) + ) ->whereColumn('s.submission_id', '=', 'sso.submission_id') ->selectRaw('COUNT(0)'); // Retrieves the number of distinct matched keywords - $orderByDistinctKeyword = (clone $orderByMatchCount)->groupBy('sskl.keyword_id'); + $orderByDistinctKeyword = (clone $orderByMatchCount)->select(DB::raw('COUNT(DISTINCT sskl.keyword_id)')); $q->orderBy($orderByDistinctKeyword, $this->orderDirection) ->orderBy($orderByMatchCount, $this->orderDirection); break; @@ -425,6 +432,7 @@ public function getQueryBuilder(): Builder // Search phrase if ($keywords->count()) { + $likePattern = DB::raw("CONCAT('%', LOWER(?), '%')"); if(!empty($this->assignedTo)) { // Holds a single random row to check whether we have any assignment $q->leftJoinSub(fn (Builder $q) => $q @@ -435,7 +443,6 @@ public function getQueryBuilder(): Builder 'any_assignment', 'any_assignment.value', '=', DB::raw('1') ); } - $likePattern = DB::raw("CONCAT('%', LOWER(?), '%')"); // Builds the filters $q->where(fn (Builder $q) => $keywords ->map(fn (string $keyword) => $q @@ -443,8 +450,8 @@ public function getQueryBuilder(): Builder ->orWhereExists(fn (Builder $query) => $query ->from('submission_search_objects', 'sso') ->join('submission_search_object_keywords AS ssok', 'sso.object_id', '=', 'ssok.object_id') - ->join("submission_search_keyword_list AS sskl", "sskl.keyword_id", '=', "ssok.keyword_id") - ->where("sskl.keyword_text", '=', DB::raw("CONCAT(LOWER(?), '%')"))->addBinding($keyword) + ->join('submission_search_keyword_list AS sskl', 'sskl.keyword_id', '=', 'ssok.keyword_id') + ->where('sskl.keyword_text', '=', DB::raw('LOWER(?)'))->addBinding($keyword) ->whereColumn('s.submission_id', '=', 'sso.submission_id') // Don't permit reviewers to search on author names ->when(!empty($this->assignedTo), fn (Builder $q) => $q @@ -486,13 +493,16 @@ public function getQueryBuilder(): Builder ->where(DB::raw('LOWER(aus.setting_value)'), 'LIKE', $likePattern) ->addBinding($keyword) ) - // Search for exact submission ID + // Search for the exact submission ID ->when( ($numericWords = $keywords->filter(fn (string $keyword) => ctype_digit($keyword)))->count(), fn (Builder $query) => $query->orWhereIn('s.submission_id', $numericWords) ) ) ); + } elseif (strlen($this->searchPhrase)) { + // If there's search text, but no keywords could be extracted from it, force the query to return nothing + $q->whereRaw('1 = 0'); } if (isset($this->categoryIds)) { From 33d7922e5baa95b12ec427531cf96fd2a1a646e8 Mon Sep 17 00:00:00 2001 From: Jonas Raoni Soares da Silva Date: Fri, 25 Aug 2023 17:36:30 +0300 Subject: [PATCH 09/10] pkp/pkp-lib#8710 Updated comments --- classes/submission/Collector.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/classes/submission/Collector.php b/classes/submission/Collector.php index d45a8a65534..b8ad72046c1 100644 --- a/classes/submission/Collector.php +++ b/classes/submission/Collector.php @@ -510,13 +510,13 @@ public function getQueryBuilder(): Builder ->whereIn('pc.category_id', $this->categoryIds); } - // By any child pub object's DOI status + // Filter by any child pub object's DOI status $q->when($this->doiStatuses !== null, fn (Builder $q) => $this->addDoiStatusFilterToQuery($q)); - // By whether any child pub objects have DOIs assigned + // Filter by whether any child pub objects have DOIs assigned $q->when($this->hasDois !== null, fn (Builder $q) => $this->addHasDoisFilterToQuery($q)); - // By whether any child pub objects have DOIs assigned + // Filter out excluded submission IDs $q->when($this->excludeIds !== null, fn (Builder $q) => $q->whereNotIn('s.submission_id', $this->excludeIds)); // Limit and offset results for pagination From f477ec8e5f3cfacfe4db7b4b523adb1263b7d320 Mon Sep 17 00:00:00 2001 From: Jonas Raoni Soares da Silva Date: Fri, 25 Aug 2023 18:02:44 +0300 Subject: [PATCH 10/10] pkp/pkp-lib#8710 Added optional maxSearchKeywords parameter --- classes/submission/Collector.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/classes/submission/Collector.php b/classes/submission/Collector.php index b8ad72046c1..221179c4ea6 100644 --- a/classes/submission/Collector.php +++ b/classes/submission/Collector.php @@ -60,6 +60,7 @@ abstract class Collector implements CollectorInterface public string $orderBy = self::ORDERBY_DATE_SUBMITTED; public string $orderDirection = 'DESC'; public ?string $searchPhrase = null; + public ?int $maxSearchKeywords = null; public ?array $statuses = null; public ?array $stageIds = null; public ?array $doiStatuses = null; @@ -207,9 +208,10 @@ public function assignedTo($assignedTo): AppCollector /** * Limit results to submissions matching this search query */ - public function searchPhrase(?string $phrase): AppCollector + public function searchPhrase(?string $phrase, ?int $maxSearchKeywords = null): AppCollector { $this->searchPhrase = $phrase; + $this->maxSearchKeywords = $maxSearchKeywords; return $this; } @@ -293,7 +295,9 @@ public function getQueryBuilder(): Builder } // Prepare keywords (allows short and numeric words) - $keywords = collect(Application::getSubmissionSearchIndex()->filterKeywords($this->searchPhrase, false, true, true))->unique(); + $keywords = collect(Application::getSubmissionSearchIndex()->filterKeywords($this->searchPhrase, false, true, true)) + ->unique() + ->take($this->maxSearchKeywords ?? PHP_INT_MAX); // Setup the order by switch ($this->orderBy) {