MDL-55356 core_search: API to queue contexts for indexing
authorsam marshall <s.marshall@open.ac.uk>
Thu, 7 Sep 2017 15:38:38 +0000 (16:38 +0100)
committersam marshall <s.marshall@open.ac.uk>
Wed, 11 Oct 2017 16:17:07 +0000 (17:17 +0100)
New API \core_search\manager::request_index($context, $areaid = '')
adds the given context to a list which is intended to be indexed
later by the scheduled task.

lib/db/install.xml
lib/db/upgrade.php
search/classes/manager.php
search/tests/manager_test.php
version.php

index bd36ac7..e3c9f7c 100644 (file)
         <INDEX NAME="predictionidanduseridandactionname" UNIQUE="false" FIELDS="predictionid, userid, actionname"/>
       </INDEXES>
     </TABLE>
+    <TABLE NAME="search_index_requests" COMMENT="Records requests for (re)indexing of specific contexts. Entries will be removed from this table when indexing of that context is complete. (This table is not used for normal time-based indexing of new content.)">
+      <FIELDS>
+        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
+        <FIELD NAME="contextid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Context ID that has been requested for reindexing."/>
+        <FIELD NAME="searcharea" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false" COMMENT="Set (e.g. 'forum-post') if a specific area is to be reindexed. Blank indicates all areas."/>
+        <FIELD NAME="timerequested" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Time at which this index update was requested."/>
+        <FIELD NAME="partialarea" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false" COMMENT="If processing of this context partially completed, set to the area that needs processing next. Blank indicates not processed yet."/>
+        <FIELD NAME="partialtime" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="If processing partially completed, set to the timestamp within the next area where processing should start. 0 indicates not processed yet."/>
+      </FIELDS>
+      <KEYS>
+        <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+        <KEY NAME="contextid" TYPE="foreign" FIELDS="contextid" REFTABLE="context" REFFIELDS="id"/>
+      </KEYS>
+    </TABLE>
   </TABLES>
 </XMLDB>
\ No newline at end of file
index d0fb99f..8bf5154 100644 (file)
@@ -2601,5 +2601,31 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2017092900.00);
     }
 
+    if ($oldversion < 2017100600.01) {
+
+        // Define table search_index_requests to be created.
+        $table = new xmldb_table('search_index_requests');
+
+        // Adding fields to table search_index_requests.
+        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
+        $table->add_field('contextid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('searcharea', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('timerequested', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('partialarea', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('partialtime', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+
+        // Adding keys to table search_index_requests.
+        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+        $table->add_key('contextid', XMLDB_KEY_FOREIGN, array('contextid'), 'context', array('id'));
+
+        // Conditionally launch create table for search_index_requests.
+        if (!$dbman->table_exists($table)) {
+            $dbman->create_table($table);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2017100600.01);
+    }
+
     return true;
 }
index c8425a1..47b4b23 100644 (file)
@@ -637,6 +637,8 @@ class manager {
      * @return bool Whether there was any updated document or not.
      */
     public function index($fullindex = false, $timelimit = 0, \progress_trace $progress = null) {
+        global $DB;
+
         // Cannot combine time limit with reindex.
         if ($timelimit && $fullindex) {
             throw new \coding_exception('Cannot apply time limit when reindexing');
@@ -687,6 +689,10 @@ class manager {
 
             if ($fullindex === true) {
                 $referencestarttime = 0;
+
+                // For full index, we delete any queued context index requests, as those will
+                // obviously be met by the full index.
+                $DB->delete_records('search_index_requests');
             } else {
                 $partial = get_config($componentconfigname, $varname . '_partial');
                 if ($partial) {
@@ -905,4 +911,36 @@ class manager {
 
         return false;
     }
+
+    /**
+     * Requests that a specific context is indexed by the scheduled task. The context will be
+     * added to a queue which is processed by the task.
+     *
+     * This is used after a restore to ensure that restored items are indexed, even though their
+     * modified time will be older than the latest indexed.
+     *
+     * @param \context $context Context to index within
+     * @param string $areaid Area to index, '' = all areas
+     */
+    public static function request_index(\context $context, $areaid = '') {
+        global $DB;
+
+        // Check through existing requests for this context or any parent context.
+        list ($contextsql, $contextparams) = $DB->get_in_or_equal(
+                $context->get_parent_context_ids(true));
+        $existing = $DB->get_records_select('search_index_requests',
+                'contextid ' . $contextsql, $contextparams, '', 'id, searcharea, partialarea');
+        foreach ($existing as $rec) {
+            // If we haven't started processing the existing request yet, and it covers the same
+            // area (or all areas) then that will be sufficient so don't add anything else.
+            if ($rec->partialarea === '' && ($rec->searcharea === $areaid || $rec->searcharea === '')) {
+                return;
+            }
+        }
+
+        // No suitable existing request, so add a new one.
+        $newrecord = [ 'contextid' => $context->id, 'searcharea' => $areaid,
+                'timerequested' => time(), 'partialarea' => '', 'partialtime' => 0 ];
+        $DB->insert_record('search_index_requests', $newrecord);
+    }
 }
index 9ae2a4d..0c1984c 100644 (file)
@@ -560,4 +560,82 @@ class search_manager_testcase extends advanced_testcase {
         $this->assertTrue(testable_core_search::is_search_area('\\mod_forum\\search\\post'));
         $this->assertTrue(testable_core_search::is_search_area('mod_forum\\search\\post'));
     }
+
+    /**
+     * Tests the request_index function used for reindexing certain contexts. This only tests
+     * adding things to the request list, it doesn't test that they are actually indexed by the
+     * scheduled task.
+     */
+    public function test_request_index() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        $course1 = $this->getDataGenerator()->create_course();
+        $course1ctx = context_course::instance($course1->id);
+        $course2 = $this->getDataGenerator()->create_course();
+        $course2ctx = context_course::instance($course2->id);
+        $forum1 = $this->getDataGenerator()->create_module('forum', ['course' => $course1->id]);
+        $forum1ctx = context_module::instance($forum1->cmid);
+        $forum2 = $this->getDataGenerator()->create_module('forum', ['course' => $course2->id]);
+        $forum2ctx = context_module::instance($forum2->cmid);
+
+        // Initially no requests.
+        $this->assertEquals(0, $DB->count_records('search_index_requests'));
+
+        // Request update for course 1, all areas.
+        \core_search\manager::request_index($course1ctx);
+
+        // Check all details of entry.
+        $results = array_values($DB->get_records('search_index_requests'));
+        $this->assertCount(1, $results);
+        $this->assertEquals($course1ctx->id, $results[0]->contextid);
+        $this->assertEquals('', $results[0]->searcharea);
+        $now = time();
+        $this->assertLessThanOrEqual($now, $results[0]->timerequested);
+        $this->assertGreaterThan($now - 10, $results[0]->timerequested);
+        $this->assertEquals('', $results[0]->partialarea);
+        $this->assertEquals(0, $results[0]->partialtime);
+
+        // Request forum 1, all areas; not added as covered by course 1.
+        \core_search\manager::request_index($forum1ctx);
+        $this->assertEquals(1, $DB->count_records('search_index_requests'));
+
+        // Request forum 1, specific area; not added as covered by course 1 all areas.
+        \core_search\manager::request_index($forum1ctx, 'forum-post');
+        $this->assertEquals(1, $DB->count_records('search_index_requests'));
+
+        // Request course 1 again, specific area; not added as covered by all areas.
+        \core_search\manager::request_index($course1ctx, 'forum-post');
+        $this->assertEquals(1, $DB->count_records('search_index_requests'));
+
+        // Request course 1 again, all areas; not needed as covered already.
+        \core_search\manager::request_index($course1ctx);
+        $this->assertEquals(1, $DB->count_records('search_index_requests'));
+
+        // Request course 2, specific area.
+        \core_search\manager::request_index($course2ctx, 'label-activity');
+        // Note: I'm ordering by ID for convenience - this is dangerous in real code (see MDL-43447)
+        // but in a unit test it shouldn't matter as nobody is using clustered databases for unit
+        // test.
+        $results = array_values($DB->get_records('search_index_requests', null, 'id'));
+        $this->assertCount(2, $results);
+        $this->assertEquals($course1ctx->id, $results[0]->contextid);
+        $this->assertEquals($course2ctx->id, $results[1]->contextid);
+        $this->assertEquals('label-activity', $results[1]->searcharea);
+
+        // Request forum 2, same specific area; not added.
+        \core_search\manager::request_index($forum2ctx, 'label-activity');
+        $this->assertEquals(2, $DB->count_records('search_index_requests'));
+
+        // Request forum 2, different specific area; added.
+        \core_search\manager::request_index($forum2ctx, 'forum-post');
+        $this->assertEquals(3, $DB->count_records('search_index_requests'));
+
+        // Request forum 2, all areas; also added. (Note: This could obviously remove the previous
+        // one, but for simplicity, I didn't make it do that; also it could perhaps cause problems
+        // if we had already begun processing the previous entry.)
+        \core_search\manager::request_index($forum2ctx);
+        $this->assertEquals(4, $DB->count_records('search_index_requests'));
+    }
 }
index a8c58dd..0a6f2d6 100644 (file)
@@ -29,7 +29,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2017100600.00;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2017100600.01;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.