Skip to content

Commit

Permalink
MDL-60917 core_search: add top result section
Browse files Browse the repository at this point in the history
  • Loading branch information
Nathan Nguyen committed Oct 12, 2021
1 parent e746dc7 commit 089ebc8
Show file tree
Hide file tree
Showing 8 changed files with 545 additions and 0 deletions.
18 changes: 18 additions & 0 deletions admin/settings/plugins.php
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,24 @@
new lang_string('searchhideallcategory_desc', 'admin'),
0));

// Top result options.
$temp->add(new admin_setting_heading('searchtopresults', new lang_string('searchtopresults', 'admin'), ''));
// Max Top results.
$options = range(0, 10);
$temp->add(new admin_setting_configselect('searchmaxtopresults',
new lang_string('searchmaxtopresults', 'admin'),
new lang_string('searchmaxtopresults_desc', 'admin'),
3, $options));
// Teacher roles.
$options = [];
foreach (get_all_roles() as $role) {
$options[$role->id] = $role->shortname;
}
$temp->add(new admin_setting_configmultiselect('searchteacherroles',
new lang_string('searchteacherroles', 'admin'),
new lang_string('searchteacherroles_desc', 'admin'),
[], $options));

$temp->add(new admin_setting_heading('searchmanagement', new lang_string('searchmanagement', 'admin'),
new lang_string('searchmanagement_desc', 'admin')));

Expand Down
5 changes: 5 additions & 0 deletions lang/en/admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -1148,6 +1148,11 @@
$string['searchhideallcategory_desc'] = 'If checked, the category with all results will be hidden on the search result screen.';
$string['searchdefaultcategory'] = 'Default search category';
$string['searchdefaultcategory_desc'] = 'Results from the selected search area category will be displayed by default.';
$string['searchtopresults'] = 'Top Results';
$string['searchmaxtopresults'] = 'Maximum top results';
$string['searchmaxtopresults_desc'] = 'Specify the maximum number of top results';
$string['searchteacherroles'] = 'Teacher roles';
$string['searchteacherroles_desc'] = 'Please select all teacher roles for indexing course teacher';
$string['searchallavailablecoursesdesc'] = 'If set to search within enrolled courses only, course information (name and summary) and course content will only be searched in courses which the user is enrolled in. Otherwise, course information and course content will be searched in all courses which the user can access, such as courses with guest access enabled.';
$string['searchincludeallcourses'] = 'Include all visible courses';
$string['searchincludeallcourses_desc'] = 'If enabled, search results will include course information (name and summary) of courses which are visible to the user, even if they don\'t have access to the course content.';
Expand Down
3 changes: 3 additions & 0 deletions lang/en/search.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
$string['back'] = 'Back';
$string['beadmin'] = 'You need to be an admin user to use this page.';
$string['commenton'] = 'Comment on';
$string['content:courserole'] = '{$a->role} in {$a->course}';
$string['confirm_delete'] = 'Are you sure you want to delete the index for {$a}? Until the search area is indexed, users will not get search results from this area.';
$string['confirm_indexall'] = 'Are you sure you want to update indexed contents now? If a large amount of content needs indexing, this can take a long time. For live servers, you should normally leave indexing to the \'Global search indexing\' scheduled task.';
$string['confirm_reindexall'] = 'Are you sure you want to reindex all site contents now? If your site contains a large amount of content, this will take a long time, and users may not get full search results until it completes.';
Expand Down Expand Up @@ -115,6 +116,7 @@
$string['search:message_sent'] = 'Messages - sent';
$string['search:mycourse'] = 'My courses';
$string['search:course'] = 'Courses';
$string['search:course_teacher'] = 'Course Teacher';
$string['search:section'] = 'Course sections';
$string['search:user'] = 'Users';
$string['searcharea'] = 'Search area';
Expand All @@ -132,6 +134,7 @@
$string['thesewordsmustnotappear'] = 'These words must not appear';
$string['title'] = 'Title';
$string['tofetchtheseresults'] = 'to fetch these results';
$string['topresults'] = 'Top Results';
$string['totalsize'] = 'Total size';
$string['totime'] = 'Modified before';
$string['type'] = 'Type';
Expand Down
62 changes: 62 additions & 0 deletions search/classes/manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -1056,6 +1056,68 @@ public function search(\stdClass $formdata, $limit = 0) {
return $docs;
}

/**
* Search for top ranked result.
* @param \stdClass $formdata search query data
* @return array|document[]
*/
public function search_top(\stdClass $formdata): array {
global $USER;

// Return if the config value is set to 0.
$maxtopresult = get_config('core', 'searchmaxtopresults');
if (empty($maxtopresult)) {
return [];
}

// Only process if 'searchenablecategories' is set.
if (self::is_search_area_categories_enabled() && !empty($formdata->cat)) {
$cat = self::get_search_area_category_by_name($formdata->cat);
$formdata->areaids = array_keys($cat->get_areas());
} else {
return [];
}
$docs = $this->search($formdata);

// Look for course, teacher and course content.
$coursedocs = [];
$courseteacherdocs = [];
$coursecontentdocs = [];
$otherdocs = [];
foreach ($docs as $doc) {
if ($doc->get('areaid') === 'core_course-course' && stripos($doc->get('title'), $formdata->q) !== false) {
$coursedocs[] = $doc;
} else if (strpos($doc->get('areaid'), 'course_teacher') !== false
&& stripos($doc->get('content'), $formdata->q) !== false) {
$courseteacherdocs[] = $doc;
} else if (strpos($doc->get('areaid'), 'mod_') !== false) {
$coursecontentdocs[] = $doc;
} else {
$otherdocs[] = $doc;
}
}

// Swap current courses to top.
$enroledcourses = $this->get_my_courses(false);
// Move current courses of the user to top.
foreach ($enroledcourses as $course) {
$completion = new \completion_info($course);
if (!$completion->is_course_complete($USER->id)) {
foreach ($coursedocs as $index => $doc) {
$areaid = $doc->get('areaid');
if ($areaid == 'core_course-course' && $course->id == $doc->get('courseid')) {
unset($coursedocs[$index]);
array_unshift($coursedocs, $doc);
}
}
}
}

$maxtopresult = get_config('core', 'searchmaxtopresults');
$result = array_merge($coursedocs, $courseteacherdocs, $coursecontentdocs, $otherdocs);
return array_slice($result, 0, $maxtopresult);
}

/**
* Build a list of course ids to limit the search based on submitted form data.
*
Expand Down
19 changes: 19 additions & 0 deletions search/classes/output/renderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,25 @@ public function render_results($results, $page, $totalcount, $url, $cat = null)
return $content;
}

/**
* Top results content
*
* @param \core_search\document[] $results Search Results
* @return string content of the top result section
*/
public function render_top_results($results): string {
$content = $this->output->box_start('topresults');
$content .= $this->output->heading(get_string('topresults', 'core_search'));
$content .= \html_writer::tag('hr', '');
$resultshtml = array();
foreach ($results as $hit) {
$resultshtml[] = $this->render_result($hit);
}
$content .= \html_writer::tag('div', implode('<hr/>', $resultshtml), array('class' => 'search-results'));
$content .= $this->output->box_end();
return $content;
}

/**
* Displaying search results.
*
Expand Down
4 changes: 4 additions & 0 deletions search/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,10 @@
$mform->display();

if (!empty($results)) {
$topresults = $search->search_top($data);
if (!empty($topresults)) {
echo $searchrenderer->render_top_results($topresults);
}
echo $searchrenderer->render_results($results->results, $results->actualpage, $results->totalcount, $url, $cat);

\core_search\manager::trigger_search_results_viewed([
Expand Down
216 changes: 216 additions & 0 deletions search/tests/top_result_test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.

/**
* Test for top results
*
* @package core_search
* @author Nathan Nguyen <nathannguyen@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/

defined('MOODLE_INTERNAL') || die();

global $CFG;
require_once(__DIR__ . '/fixtures/testable_core_search.php');
require_once(__DIR__ . '/fixtures/mock_search_area.php');

/**
* Test for top results
*
* @package core_search
* @author Nathan Nguyen <nathannguyen@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class search_top_result_testcase extends advanced_testcase {

/** @var stdClass course 1 */
protected $course1;
/** @var stdClass course 2 */
protected $course2;
/** @var stdClass user 1 */
protected $user1;
/** @var stdClass user 2 */
protected $user2;
/** @var stdClass user 3 */
protected $user3;
/** @var stdClass search engine */
protected $search;

/**
* Prepare test and users.
*/
private function prepare_test_courses_and_users(): void {
global $DB;

$this->setAdminUser();

// Search engine.
$this->search = testable_core_search::instance(new \search_simpledb\engine());

// Set default configurations.
set_config('searchallavailablecourses', 1);
set_config('searchincludeallcourses', 1);
set_config('searchenablecategories', true);
set_config('enableglobalsearch', true);
set_config('searchmaxtopresults', 3);
$teacher = $DB->get_record('role', ['shortname' => 'teacher']);
$editingteacher = $DB->get_record('role', ['shortname' => 'editingteacher']);
set_config('searchteacherroles', "$teacher->id, $editingteacher->id");

// Generate test data.
$generator = $this->getDataGenerator();

// Courses.
$this->course1 = $generator->create_course(['fullname' => 'Top course result 1']);
$this->course2 = $generator->create_course(['fullname' => 'Top course result 2']);

// User 1.
$urecord1 = new \stdClass();
$urecord1->firstname = "User 1";
$urecord1->lastname = "Test";
$this->user1 = $generator->create_user($urecord1);

// User 2.
$urecord2 = new \stdClass();
$urecord2->firstname = "User 2";
$urecord2->lastname = "Test";
$this->user2 = $generator->create_user($urecord2);

// User 3.
$urecord3 = new \stdClass();
$urecord3->firstname = "User 3";
$urecord3->lastname = "Test";
$this->user3 = $generator->create_user($urecord3);
}

/**
* Test course ranking
*/
public function test_search_course_rank(): void {
$this->resetAfterTest();
$this->prepare_test_courses_and_users();
$this->setUser($this->user1);

// Search query.
$data = new \stdClass();
$data->q = 'Top course result';
$data->cat = 'core-all';

// Course 1 at the first index.
$this->run_index();
$docs = $this->search->search_top($data);
$this->assertEquals('Top course result 1', $docs[0]->get('title'));
$this->assertEquals('Top course result 2', $docs[1]->get('title'));

// Enrol user to course 2.
$this->getDataGenerator()->enrol_user($this->user1->id, $this->course2->id, 'student');

// Course 2 at the first index.
$this->run_index();
$docs = $this->search->search_top($data);
$this->assertEquals('Top course result 2', $docs[0]->get('title'));
$this->assertEquals('Top course result 1', $docs[1]->get('title'));
}

/**
* Test without teacher indexing
*/
public function test_search_with_no_course_teacher_indexing(): void {
$this->resetAfterTest();
$this->prepare_test_courses_and_users();
set_config('searchteacherroles', "");
$this->getDataGenerator()->enrol_user($this->user1->id, $this->course1->id, 'teacher');

// Search query.
$data = new \stdClass();
$data->q = 'Top course result';
$data->cat = 'core-all';

// Only return the course.
$this->run_index();
$docs = $this->search->search_top($data);
$this->assertCount(2, $docs);
$this->assertEquals('Top course result 1', $docs[0]->get('title'));
$this->assertEquals('Top course result 2', $docs[1]->get('title'));
}

/**
* Test with teacher indexing
*/
public function test_search_with_course_teacher_indexing(): void {
$this->resetAfterTest();
$this->prepare_test_courses_and_users();

$this->getDataGenerator()->enrol_user($this->user1->id, $this->course1->id, 'teacher');
$this->getDataGenerator()->enrol_user($this->user2->id, $this->course1->id, 'student');

// Search query.
$data = new \stdClass();
$data->q = 'Top course result 1';
$data->cat = 'core-all';

// Return the course and the teachers.
$this->run_index();
$docs = $this->search->search_top($data);
$this->assertEquals('Top course result 1', $docs[0]->get('title'));
$this->assertEquals('User 1 Test', $docs[1]->get('title'));
}

/**
* Test with teacher indexing
*/
public function test_search_with_course_teacher_content_indexing(): void {
$this->resetAfterTest();
$this->prepare_test_courses_and_users();

// Create forums as course content.
$generator = $this->getDataGenerator();

// Course Teacher.
$this->getDataGenerator()->enrol_user($this->user1->id, $this->course1->id, 'teacher');

// Forums.
$generator->create_module('forum',
['course' => $this->course1->id, 'name' => 'Forum 1, does not contain the keyword']);
$generator->create_module('forum',
['course' => $this->course2->id, 'name' => 'Forum 2, contains keyword Top course result 1']);

$this->run_index();

// Search query.
$data = new \stdClass();
$data->q = 'Top course result 1';
$data->cat = 'core-all';

// Return the course and the teacher and the forum.
$docs = $this->search->search_top($data);
$this->assertEquals('Top course result 1', $docs[0]->get('title'));
$this->assertEquals('User 1 Test', $docs[1]->get('title'));
$this->assertEquals('Forum 2, contains keyword Top course result 1', $docs[2]->get('title'));
}

/**
* Execute indexing
*/
private function run_index(): void {
// Indexing.
$this->waitForSecond();
$this->search->index(false, 0);
}
}
Loading

0 comments on commit 089ebc8

Please sign in to comment.