MDL-55356 core_search: New manager API index_context
[moodle.git] / search / classes / engine.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * Base class for search engines.
19  *
20  * All search engines must extend this class.
21  *
22  * @package   core_search
23  * @copyright 2015 Daniel Neis
24  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25  */
27 namespace core_search;
29 defined('MOODLE_INTERNAL') || die();
31 /**
32  * Base class for search engines.
33  *
34  * All search engines must extend this class.
35  *
36  * @package   core_search
37  * @copyright 2015 Daniel Neis
38  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
39  */
40 abstract class engine {
42     /**
43      * The search engine configuration.
44      *
45      * @var stdClass
46      */
47     protected $config = null;
49     /**
50      * Last executed query error, if there was any.
51      * @var string
52      */
53     protected $queryerror = null;
55     /**
56      * @var array Internal cache.
57      */
58     protected $cachedareas = array();
60     /**
61      * @var array Internal cache.
62      */
63     protected $cachedcourses = array();
65     /**
66      * User data required to show their fullnames. Indexed by userid.
67      *
68      * @var stdClass[]
69      */
70     protected static $cachedusers = array();
72     /**
73      * @var string Frankenstyle plugin name.
74      */
75     protected $pluginname = null;
77     /**
78      * Initialises the search engine configuration.
79      *
80      * Search engine availability should be checked separately.
81      *
82      * @return void
83      */
84     public function __construct() {
86         $classname = get_class($this);
87         if (strpos($classname, '\\') === false) {
88             throw new \coding_exception('"' . $classname . '" class should specify its component namespace and it should be named engine.');
89         } else if (strpos($classname, '_') === false) {
90             throw new \coding_exception('"' . $classname . '" class namespace should be its frankenstyle name');
91         }
93         // This is search_xxxx config.
94         $this->pluginname = substr($classname, 0, strpos($classname, '\\'));
95         if ($config = get_config($this->pluginname)) {
96             $this->config = $config;
97         } else {
98             $this->config = new stdClass();
99         }
100     }
102     /**
103      * Returns a course instance checking internal caching.
104      *
105      * @param int $courseid
106      * @return stdClass
107      */
108     protected function get_course($courseid) {
109         if (!empty($this->cachedcourses[$courseid])) {
110             return $this->cachedcourses[$courseid];
111         }
113         // No need to clone, only read.
114         $this->cachedcourses[$courseid] = get_course($courseid, false);
116         return $this->cachedcourses[$courseid];
117     }
119     /**
120      * Returns user data checking the internal static cache.
121      *
122      * Including here the minimum required user information as this may grow big.
123      *
124      * @param int $userid
125      * @return stdClass
126      */
127     public function get_user($userid) {
128         global $DB;
130         if (empty(self::$cachedusers[$userid])) {
131             $fields = get_all_user_name_fields(true);
132             self::$cachedusers[$userid] = $DB->get_record('user', array('id' => $userid), 'id, ' . $fields);
133         }
134         return self::$cachedusers[$userid];
135     }
137     /**
138      * Returns a search instance of the specified area checking internal caching.
139      *
140      * @param string $areaid Area id
141      * @return \core_search\base
142      */
143     protected function get_search_area($areaid) {
145         if (isset($this->cachedareas[$areaid]) && $this->cachedareas[$areaid] === false) {
146             // We already checked that area and it is not available.
147             return false;
148         }
150         if (!isset($this->cachedareas[$areaid])) {
151             // First result that matches this area.
153             $this->cachedareas[$areaid] = \core_search\manager::get_search_area($areaid);
154             if ($this->cachedareas[$areaid] === false) {
155                 // The area does not exist or it is not available any more.
157                 $this->cachedareas[$areaid] = false;
158                 return false;
159             }
161             if (!$this->cachedareas[$areaid]->is_enabled()) {
162                 // We skip the area if it is not enabled.
164                 // Marking it as false so next time we don' need to check it again.
165                 $this->cachedareas[$areaid] = false;
167                 return false;
168             }
169         }
171         return $this->cachedareas[$areaid];
172     }
174     /**
175      * Returns a document instance prepared to be rendered.
176      *
177      * @param \core_search\base $searcharea
178      * @param array $docdata
179      * @return \core_search\document
180      */
181     protected function to_document(\core_search\base $searcharea, $docdata) {
183         list($componentname, $areaname) = \core_search\manager::extract_areaid_parts($docdata['areaid']);
184         $doc = \core_search\document_factory::instance($docdata['itemid'], $componentname, $areaname, $this);
185         $doc->set_data_from_engine($docdata);
186         $doc->set_doc_url($searcharea->get_doc_url($doc));
187         $doc->set_context_url($searcharea->get_context_url($doc));
189         // Uses the internal caches to get required data needed to render the document later.
190         $course = $this->get_course($doc->get('courseid'));
191         $doc->set_extra('coursefullname', $course->fullname);
193         if ($doc->is_set('userid')) {
194             $user = $this->get_user($doc->get('userid'));
195             $doc->set_extra('userfullname', fullname($user));
196         }
198         return $doc;
199     }
201     /**
202      * Loop through given iterator of search documents
203      * and and have the search engine back end add them
204      * to the index.
205      *
206      * @param iterator $iterator the iterator of documents to index
207      * @param searcharea $searcharea the area for the documents to index
208      * @param array $options document indexing options
209      * @return array Processed document counts
210      */
211     public function add_documents($iterator, $searcharea, $options) {
212         $numrecords = 0;
213         $numdocs = 0;
214         $numdocsignored = 0;
215         $lastindexeddoc = 0;
216         $firstindexeddoc = 0;
217         $partial = false;
219         foreach ($iterator as $document) {
220             // Stop if we have exceeded the time limit (and there are still more items). Always
221             // do at least one second's worth of documents otherwise it will never make progress.
222             if ($lastindexeddoc !== $firstindexeddoc &&
223                     !empty($options['stopat']) && microtime(true) >= $options['stopat']) {
224                 $partial = true;
225                 break;
226             }
228             if (!$document instanceof \core_search\document) {
229                 continue;
230             }
232             if (isset($options['lastindexedtime']) && $options['lastindexedtime'] == 0) {
233                 // If we have never indexed this area before, it must be new.
234                 $document->set_is_new(true);
235             }
237             if ($options['indexfiles']) {
238                 // Attach files if we are indexing.
239                 $searcharea->attach_files($document);
240             }
242             if ($this->add_document($document, $options['indexfiles'])) {
243                 $numdocs++;
244             } else {
245                 $numdocsignored++;
246             }
248             $lastindexeddoc = $document->get('modified');
249             if (!$firstindexeddoc) {
250                 $firstindexeddoc = $lastindexeddoc;
251             }
252             $numrecords++;
253         }
255         return array($numrecords, $numdocs, $numdocsignored, $lastindexeddoc, $partial);
256     }
258     /**
259      * Returns the plugin name.
260      *
261      * @return string Frankenstyle plugin name.
262      */
263     public function get_plugin_name() {
264         return $this->pluginname;
265     }
267     /**
268      * Gets the document class used by this search engine.
269      *
270      * Search engines can overwrite \core_search\document with \search_ENGINENAME\document class.
271      *
272      * Looks for a document class in the current search engine namespace, falling back to \core_search\document.
274      * Publicly available because search areas do not have access to the engine details,
275      * \core_search\document_factory accesses this function.
276      *
277      * @return string
278      */
279     public function get_document_classname() {
280         $classname = $this->pluginname . '\\document';
281         if (!class_exists($classname)) {
282             $classname = '\\core_search\\document';
283         }
284         return $classname;
285     }
287     /**
288      * Run any pre-indexing operations.
289      *
290      * Should be overwritten if the search engine needs to do any pre index preparation.
291      *
292      * @param bool $fullindex True if a full index will be performed
293      * @return void
294      */
295     public function index_starting($fullindex = false) {
296         // Nothing by default.
297     }
299     /**
300      * Run any post indexing operations.
301      *
302      * Should be overwritten if the search engine needs to do any post index cleanup.
303      *
304      * @param int $numdocs The number of documents that were added to the index
305      * @param bool $fullindex True if a full index was performed
306      * @return void
307      */
308     public function index_complete($numdocs = 0, $fullindex = false) {
309         // Nothing by default.
310     }
312     /**
313      * Do anything that may need to be done before an area is indexed.
314      *
315      * @param \core_search\base $searcharea The search area that was complete
316      * @param bool $fullindex True if a full index is being performed
317      * @return void
318      */
319     public function area_index_starting($searcharea, $fullindex = false) {
320         // Nothing by default.
321     }
323     /**
324      * Do any area cleanup needed, and do anything to confirm contents.
325      *
326      * Return false to prevent the search area completed time and stats from being updated.
327      *
328      * @param \core_search\base $searcharea The search area that was complete
329      * @param int $numdocs The number of documents that were added to the index
330      * @param bool $fullindex True if a full index is being performed
331      * @return bool True means that data is considered indexed
332      */
333     public function area_index_complete($searcharea, $numdocs = 0, $fullindex = false) {
334         return true;
335     }
337     /**
338      * Optimizes the search engine.
339      *
340      * Should be overwritten if the search engine can optimize its contents.
341      *
342      * @return void
343      */
344     public function optimize() {
345         // Nothing by default.
346     }
348     /**
349      * Does the system satisfy all the requirements.
350      *
351      * Should be overwritten if the search engine has any system dependencies
352      * that needs to be checked.
353      *
354      * @return bool
355      */
356     public function is_installed() {
357         return true;
358     }
360     /**
361      * Returns any error reported by the search engine when executing the provided query.
362      *
363      * It should be called from static::execute_query when an exception is triggered.
364      *
365      * @return string
366      */
367     public function get_query_error() {
368         return $this->queryerror;
369     }
371     /**
372      * Returns the total number of documents available for the most recent call to execute_query.
373      *
374      * This can be an estimate, but should get more accurate the higher the limited passed to execute_query is.
375      * To do that, the engine can use (actual result returned count + count of unchecked documents), or
376      * (total possible docs - docs that have been checked and rejected).
377      *
378      * Engine can limit to manager::MAX_RESULTS if there is cost to determining more.
379      * If this cannot be computed in a reasonable way, manager::MAX_RESULTS may be returned.
380      *
381      * @return int
382      */
383     abstract public function get_query_total_count();
385     /**
386      * Return true if file indexing is supported and enabled. False otherwise.
387      *
388      * @return bool
389      */
390     public function file_indexing_enabled() {
391         return false;
392     }
394     /**
395      * Clears the current query error value.
396      *
397      * @return void
398      */
399     public function clear_query_error() {
400         $this->queryerror = null;
401     }
403     /**
404      * Is the server ready to use?
405      *
406      * This should also check that the search engine configuration is ok.
407      *
408      * @return true|string Returns true if all good or an error string.
409      */
410     abstract function is_server_ready();
412     /**
413      * Adds a document to the search engine.
414      *
415      * @param document $document
416      * @param bool     $fileindexing True if file indexing is to be used
417      * @return bool    False if the file was skipped or failed, true on success
418      */
419     abstract function add_document($document, $fileindexing = false);
421     /**
422      * Executes the query on the engine.
423      *
424      * Implementations of this function should check user context array to limit the results to contexts where the
425      * user have access. They should also limit the owneruserid field to manger::NO_OWNER_ID or the current user's id.
426      * Engines must use area->check_access() to confirm user access.
427      *
428      * Engines should reasonably attempt to fill up to limit with valid results if they are available.
429      *
430      * @param  stdClass $filters Query and filters to apply.
431      * @param  array    $usercontexts Contexts where the user has access. True if the user can access all contexts.
432      * @param  int      $limit The maximum number of results to return. If empty, limit to manager::MAX_RESULTS.
433      * @return \core_search\document[] Results or false if no results
434      */
435     abstract function execute_query($filters, $usercontexts, $limit = 0);
437     /**
438      * Delete all documents.
439      *
440      * @param string $areaid To filter by area
441      * @return void
442      */
443     abstract function delete($areaid = null);