Thursday, January 26, 2017

Adding search to your Moodle plugin - part two

Part Two - Indexing Custom Information

In the last post, I created the basic code required to allow Moodle's global search feature search my plugin's "name" and "intro" information fields. Now I want to extend that functionality to include the "subtitle" and "additional info" fields.

Reading the documentation, this looks like it should be an extension of the functionality I wrote for the basic case. The information fields I want to return, both belong to an activity instance. And the case I already created is for that activity instance. So I want to look at extending the class I already created.

If I decide to provide index data from other sources, then I would be creating new classes as described in the documentation following sections.

One of the concerns I have with my approach of continuing extending the base_activity class, rather than the base_mod class, is that the documentation says that I should use base_activity "to index Moodle activities basic data like the activity name and description". And that base_mod should be used for "other specific activity data". The two extra fields I want to include, aren't part of the main activity table of my plugin, but rather are stored in a second table called "questionnaire_survey". There is a one-to-one relationship though, and the information is considered part of the activity information, so I still think this the correct approach. But, to get that extra data, I will have to override the database function that gets all of that data.

Looking at the /search/classes/base_activity.php file, I see that the main function to retrieve and return the activity data is the get_recordset_by_timestamp function:

    public function get_recordset_by_timestamp($modifiedfrom = 0) {
        global $DB;
        return $DB->get_recordset_select($this->get_module_name(),
                   static::MODIFIED_FIELD_NAME . ' >= ?',
                   array($modifiedfrom),
                   static::MODIFIED_FIELD_NAME . ' ASC');
    }

This is the one I will override in my class. In its current form, it returns all of the fields from the main activity table according to the "timemodified" field. Since I want to JOIN the "questionnaire_survey" with this table, I am going to have to be selective about the fields that are returned, as there is overlap.

I open my questionnaire/classes/search/activity.php file and add the following function (full file here):

    public function get_recordset_by_timestamp($modifiedfrom = 0) {
        global $DB;

        $sql = 'SELECT q.*, s.subtitle, s.info ' .
            'FROM {questionnaire} q ' .
            'INNER JOIN {questionnaire_survey} s ON q.sid = s.id ' .
            'WHERE q.timemodified >= ? ' .
            'ORDER BY q.timemodified ASC';

        return $DB->get_recordset_sql($sql, [$modifiedfrom]);
    }

Since I haven't told the search engine anywhere that I want to include the two new fields I've added, there must still be some changes to make. I install the new code, reindex the global search and test that things work. While there are no failures, I also cannot get any matches from the "subtitle" or "additional info" fields, confirming that there is more to do.

Looking at the documentation again, the next function that is presented is get_document. From the description, this function takes a record from the query results of get_recordset_by_timestamp function, and constructs a \core_search\document object with all of the data needed for indexing. My guess, is that I need to add my two new fields here. I'm going to override this function in my class.

The main function returns a constructed object with the data added to it as variables. I believe I can override this and take the returned function from the parent, add my new data to it, and return it. When I'm done, the function looks like this:

    public function get_document($record, $options = []) {
        // Get the default implementation.
        $doc = parent::get_document($record, $options);

        // Add the subtitle and additional info fields.
        $doc->set('subtitle', content_to_text($record->subtitle, false));
        $doc->set('additionalinfo', content_to_text($record->info, $record->introformat));

        return $doc;
    }

I add the code to my site, purge all the caches and reindex the search engine. But, searching for these fields is not returning my data. On the search areas admin page, I have been using "Update indexed contents". But, since none of my questionnaire activities have actually changed, nothing is being re-indexed. Instead, I need to use the "Reindex all site contents" button. When I do this, I get an error indicating:

    Coding error detected, it must be fixed by a programmer: "subtitle" field does not exist.

It would appear that using "subtitle" is not correct.

The documentation shows an example of using get_document(), and it uses "description1" and "description2" as the fields being set. Its likely that I can't use "subtitle" and "additionalinfo" as  field names in the $doc->set() function.

The \core_search\document class is defined in the /search/classes/document.php file. The set() function uses a static array to look for specific field names it is allowed to set. The names I used, "subtitle" and "additionalinfo" are not part of those. However "description1" and "description2" are. This confirms that I need to change those functions. The set() function throws an exception when an unknown field is used, and that explains the error I saw.

My new function now looks like this (full file here):

    public function get_document($record, $options = []) {
        // Get the default implementation.
        $doc = parent::get_document($record, $options);

        // Add the subtitle and additional info fields.
        $doc->set('description1', content_to_text($record->subtitle, false));
        $doc->set('description2', content_to_text($record->info, $record->introformat));

        return $doc;
    }

When I load it, and reindex all of the site contents, my search works. I now see content found from a subtitle.


So it seems that I can add indexed fields to the a activity level search, but only two. And they need to be referred to as "description1" and "description2". I'm guessing to add more than that, I would need to create more classes using base_mod instead of base_activity.

I'll explore that more in the next post, as well as how to search for more parts of my plugin.