Skip to content

Commit

Permalink
MDL-56020 search: Adding get_results external function
Browse files Browse the repository at this point in the history
  • Loading branch information
David Monllao authored and jleyva committed Aug 28, 2023
1 parent 9bfcd77 commit 49e5894
Show file tree
Hide file tree
Showing 7 changed files with 341 additions and 6 deletions.
8 changes: 8 additions & 0 deletions lib/db/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -1706,6 +1706,14 @@
'type' => 'read',
'ajax' => true
),
'core_search_get_results' => array(
'classname' => 'core_search_external',
'methodname' => 'get_results',
'classpath' => 'search/classes/external.php',
'description' => 'Search contents.',
'type' => 'read',
'capabilities' => 'moodle/search:query'
),
'core_tag_get_tagindex' => array(
'classname' => 'core_tag_external',
'methodname' => 'get_tagindex',
Expand Down
48 changes: 43 additions & 5 deletions search/classes/document.php
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,18 @@ public static function get_default_fields_definition() {
return static::$requiredfields + static::$optionalfields + static::$enginefields;
}

/**
* Returns whether a field is required or not.
*
* Considers engine fields to be optional.
*
* @param string $fieldname
* @return bool
*/
public static function field_is_required(string $fieldname): bool {
return (!empty(static::$requiredfields[$fieldname]));
}

/**
* Formats the timestamp preparing the time fields to be inserted into the search engine.
*
Expand Down Expand Up @@ -603,18 +615,31 @@ protected function apply_defaults() {
/**
* Export the document data to be used as a template context.
*
* Just delegates all the processing to export_doc_info, also used by external functions.
* Adding more info than the required one as people might be interested in extending the template.
*
* @param renderer_base $output The renderer.
* @return array
*/
public function export_for_template(\renderer_base $output) {
$docdata = $this->export_doc($output);
return $docdata;
}

/**
* Returns the current docuement information.
*
* Adding more info than the required one as themers and ws clients might be interested in showing more stuff.
*
* Although content is a required field when setting up the document, it accepts '' (empty) values
* as they may be the result of striping out HTML.
*
* SECURITY NOTE: It is the responsibility of the document to properly escape any text to be displayed.
* The renderer will output the content without any further cleaning.
*
* @param renderer_base $output The renderer.
* @return array
*/
public function export_for_template(\renderer_base $output) {
public function export_doc(\renderer_base $output) {
global $USER;

list($componentname, $areaname) = \core_search\manager::extract_areaid_parts($this->get('areaid'));
Expand All @@ -623,15 +648,18 @@ public function export_for_template(\renderer_base $output) {
$searcharea = \core_search\manager::get_search_area($this->data['areaid']);
$title = $this->is_set('title') ? $this->format_text($searcharea->get_document_display_title($this)) : '';
$data = [
'itemid' => $this->get('itemid'),
'componentname' => $componentname,
'areaname' => $areaname,
'courseurl' => course_get_url($this->get('courseid')),
'courseurl' => (course_get_url($this->get('courseid')))->out(false),
'coursefullname' => format_string($this->get('coursefullname'), true, ['context' => $context->id]),
'modified' => userdate($this->get('modified')),
'timemodified' => $this->get('modified'),
'title' => ($title !== '') ? $title : get_string('notitle', 'search'),
'docurl' => $this->get_doc_url(),
'docurl' => ($this->get_doc_url())->out(false),
'content' => $this->is_set('content') ? $this->format_text($this->get('content')) : null,
'contexturl' => $this->get_context_url(),
'contextid' => $this->get('contextid'),
'contexturl' => ($this->get_context_url())->out(false),
'description1' => $this->is_set('description1') ? $this->format_text($this->get('description1')) : null,
'description2' => $this->is_set('description2') ? $this->format_text($this->get('description2')) : null,
];
Expand Down Expand Up @@ -661,13 +689,23 @@ public function export_for_template(\renderer_base $output) {
['id' => $this->get('userid'), 'course' => $this->get('courseid')]
);
$data['userfullname'] = format_string($this->get('userfullname'), true, ['context' => $context->id]);
$data['userid'] = $this->get('userid');
}
}

if ($docicon = $this->get_doc_icon()) {
$data['icon'] = $output->image_url($docicon->get_name(), $docicon->get_component());
}

// We need to return the text formatting used for ws stuff.
$settings = \core_external\external_settings::get_instance();
if ($settings->get_raw()) {
// If this is called by a ws client and requests raw text we return the format specified by the search engine.
$data['textformat'] = $this->get_text_format();
} else {
// We convert texts to HTML by default.
$data['textformat'] = FORMAT_HTML;
}
return $data;
}

Expand Down
164 changes: 164 additions & 0 deletions search/classes/external.php
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,168 @@ public static function get_relevant_users($query, $courseid) {
}
return $result;
}

/**
* get_results parameters.
*
* @since Moodle 3.2
* @return external_function_parameters
*/
public static function get_results_parameters() {
return new external_function_parameters(
array(
'q' => new external_value(PARAM_NOTAGS, 'the search query'),
'filters' => new external_single_structure(
array(
'title' => new external_value(PARAM_NOTAGS, 'result title', VALUE_OPTIONAL),
'areaids' => new external_multiple_structure(
new external_value(PARAM_RAW, 'areaid'), 'restrict results to these areas', VALUE_DEFAULT, []
),
'courseids' => new external_multiple_structure(
new external_value(PARAM_INT, 'courseid'), 'restrict results to these courses', VALUE_DEFAULT, []
),
'contextids' => new external_multiple_structure(
new external_value(PARAM_INT, 'contextid'), 'restrict results to these context', VALUE_DEFAULT, []
),
'userids' => new external_multiple_structure(
new external_value(PARAM_INT, 'userid'), 'restrict results to these users', VALUE_DEFAULT, []
),
'groupids' => new external_multiple_structure(
new external_value(PARAM_INT, 'groupid'), 'restrict results to these groups', VALUE_DEFAULT, []
),
'mycoursesonly' => new external_value(PARAM_BOOL, 'result title', VALUE_OPTIONAL),
'order' => new external_value(PARAM_ALPHA, 'result title', VALUE_OPTIONAL),
'timestart' => new external_value(PARAM_INT, 'result title', VALUE_DEFAULT, 0),
'timeend' => new external_value(PARAM_INT, 'result title', VALUE_DEFAULT, 0)
), 'filters to apply', VALUE_OPTIONAL
),
'page' => new external_value(PARAM_INT, 'results page number starting from 0, defaults to the first page',
VALUE_DEFAULT)
)
);
}


/*
* Gets global search results based on the provided query and filters.
*
* @param string $q
* @param array $filters
* @param int $page
* @return array
*/
public static function get_results($q, $filters = [], $page = 0) {
global $PAGE;

$params = self::validate_parameters(self::get_results_parameters(), array(
'q' => $q,
'filters' => $filters,
'page' => $page)
);

$system = \context_system::instance();
\external_api::validate_context($system);

require_capability('moodle/search:query', $system);

if (\core_search\manager::is_global_search_enabled() === false) {
throw new \moodle_exception('globalsearchdisabled', 'search');
}

$search = \core_search\manager::instance();

$data = new \stdClass();
$data->q = $params['q'];

if (!empty($params['filters']['title'])) {
$data->title = $params['filters']['title'];
}

if (!empty($params['filters']['areaids'])) {
$data->areaids = $params['filters']['areaids'];
}

if (!empty($params['filters']['courseids'])) {
$data->courseids = $params['filters']['courseids'];
}

if (!empty($params['filters']['contextids'])) {
$data->contextids = $params['filters']['contextids'];
}

if (!empty($params['filters']['userids'])) {
$data->userids = $params['filters']['userids'];
}

if (!empty($params['filters']['groupids'])) {
$data->groupids = $params['filters']['groupids'];
}

if (!empty($params['filters']['timestart'])) {
$data->timestart = $params['filters']['timestart'];
}
if (!empty($params['filters']['timeend'])) {
$data->timeend = $params['filters']['timeend'];
}

$docs = $search->paged_search($data, $page);

$return = [
'totalcount' => $docs->totalcount,
'warnings' => [],
'results' => []
];

// Convert results to simple data structures.
if ($docs) {
foreach ($docs->results as $doc) {
$return['results'][] = $doc->export_doc($PAGE->get_renderer('core'));
}
}
return $return;
}

/**
* Returns description of method get_results.
*
* @return external_single_structure
*/
public static function get_results_returns() {

return new external_single_structure(
array(
'totalcount' => new external_value(PARAM_INT, 'Total number of results'),
'results' => new external_multiple_structure(
new external_single_structure(
array(
'itemid' => new external_value(PARAM_INT, 'unique id in the search area scope'),
'componentname' => new external_value(PARAM_ALPHANUMEXT, 'component name'),
'areaname' => new external_value(PARAM_ALPHANUMEXT, 'search area name'),
'courseurl' => new external_value(PARAM_URL, 'result course url'),
'coursefullname' => new external_value(PARAM_RAW, 'result course fullname'),
'timemodified' => new external_value(PARAM_INT, 'result modified time'),
'title' => new external_value(PARAM_RAW, 'result title'),
'docurl' => new external_value(PARAM_URL, 'result url'),
'content' => new external_value(PARAM_RAW, 'result contents', VALUE_OPTIONAL),
'contextid' => new external_value(PARAM_INT, 'result context id'),
'contexturl' => new external_value(PARAM_URL, 'result context url'),
'description1' => new external_value(PARAM_RAW, 'extra result contents, depends on the search area', VALUE_OPTIONAL),
'description2' => new external_value(PARAM_RAW, 'extra result contents, depends on the search area', VALUE_OPTIONAL),
'multiplefiles' => new external_value(PARAM_INT, 'whether multiple files are returned or not', VALUE_OPTIONAL),
'filenames' => new external_multiple_structure(
new external_value(PARAM_RAW, 'result file name', VALUE_OPTIONAL)
, 'result file names if present',
VALUE_OPTIONAL
),
'filename' => new external_value(PARAM_RAW, 'result file name if present', VALUE_OPTIONAL),
'userid' => new external_value(PARAM_INT, 'user id', VALUE_OPTIONAL),
'userurl' => new external_value(PARAM_URL, 'user url', VALUE_OPTIONAL),
'userfullname' => new external_value(PARAM_RAW, 'user fullname', VALUE_OPTIONAL),
'textformat' => new external_value(PARAM_INT, 'text fields format, it is the same for all of them')
), 'Search result'
), 'Search results', VALUE_OPTIONAL
)
)
);
}
}
7 changes: 6 additions & 1 deletion search/engine/solr/classes/document.php
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ public static function import_time_from_engine($time) {
}

/**
* Overwritten to use markdown format as we use markdown for solr highlighting.
* Overwritten to use HTML (highlighting).
*
* @return int
*/
Expand All @@ -129,6 +129,11 @@ protected function get_text_format() {
/**
* Formats a text string coming from the search engine.
*
* Even if this is called through an external function it is fine to return HTML as
* HTML is considered solr's search engine text format. An external function can ask
* for raw text, but this just means that it will not pass through format_text, no that
* we can not add HTML.
*
* @param string $text Text to format
* @return string HTML text to be renderer
*/
Expand Down
4 changes: 4 additions & 0 deletions search/templates/result.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@
* filename
* multiplefiles
* filenames
* itemid
* contextid
* userid
* timemodified
Example context (json):
{
Expand Down
Loading

0 comments on commit 49e5894

Please sign in to comment.