Differences Between: [Versions 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403] [Versions 39 and 310]
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/>. 16 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 */ 26 27 namespace core_search; 28 29 defined('MOODLE_INTERNAL') || die(); 30 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 { 41 42 /** 43 * The search engine configuration. 44 * 45 * @var \stdClass 46 */ 47 protected $config = null; 48 49 /** 50 * Last executed query error, if there was any. 51 * @var string 52 */ 53 protected $queryerror = null; 54 55 /** 56 * @var array Internal cache. 57 */ 58 protected $cachedareas = array(); 59 60 /** 61 * @var array Internal cache. 62 */ 63 protected $cachedcourses = array(); 64 65 /** 66 * User data required to show their fullnames. Indexed by userid. 67 * 68 * @var \stdClass[] 69 */ 70 protected static $cachedusers = array(); 71 72 /** 73 * @var string Frankenstyle plugin name. 74 */ 75 protected $pluginname = null; 76 77 /** 78 * @var bool If true, should skip schema validity check when checking the search engine is ready 79 */ 80 protected $skipschemacheck = false; 81 82 /** 83 * Initialises the search engine configuration. 84 * 85 * Search engine availability should be checked separately. 86 * 87 * The alternate configuration option is only used to construct a special second copy of the 88 * search engine object, as described in {@see has_alternate_configuration}. 89 * 90 * @param bool $alternateconfiguration If true, use alternate configuration settings 91 * @return void 92 */ 93 public function __construct(bool $alternateconfiguration = false) { 94 95 $classname = get_class($this); 96 if (strpos($classname, '\\') === false) { 97 throw new \coding_exception('"' . $classname . '" class should specify its component namespace and it should be named engine.'); 98 } else if (strpos($classname, '_') === false) { 99 throw new \coding_exception('"' . $classname . '" class namespace should be its frankenstyle name'); 100 } 101 102 // This is search_xxxx config. 103 $this->pluginname = substr($classname, 0, strpos($classname, '\\')); 104 if ($config = get_config($this->pluginname)) { 105 $this->config = $config; 106 } else { 107 $this->config = new stdClass(); 108 } 109 110 // For alternate configuration, automatically replace normal configuration values with 111 // those beginning with 'alternate'. 112 if ($alternateconfiguration) { 113 foreach ((array)$this->config as $key => $value) { 114 if (preg_match('~^alternate(.*)$~', $key, $matches)) { 115 $this->config->{$matches[1]} = $value; 116 } 117 } 118 } 119 120 // Flag just in case engine needs to know it is using the alternate configuration. 121 $this->config->alternateconfiguration = $alternateconfiguration; 122 } 123 124 /** 125 * Returns a course instance checking internal caching. 126 * 127 * @param int $courseid 128 * @return stdClass 129 */ 130 protected function get_course($courseid) { 131 if (!empty($this->cachedcourses[$courseid])) { 132 return $this->cachedcourses[$courseid]; 133 } 134 135 // No need to clone, only read. 136 $this->cachedcourses[$courseid] = get_course($courseid, false); 137 138 return $this->cachedcourses[$courseid]; 139 } 140 141 /** 142 * Returns user data checking the internal static cache. 143 * 144 * Including here the minimum required user information as this may grow big. 145 * 146 * @param int $userid 147 * @return stdClass 148 */ 149 public function get_user($userid) { 150 global $DB; 151 152 if (empty(self::$cachedusers[$userid])) { 153 $fields = get_all_user_name_fields(true); 154 self::$cachedusers[$userid] = $DB->get_record('user', array('id' => $userid), 'id, ' . $fields); 155 } 156 return self::$cachedusers[$userid]; 157 } 158 159 /** 160 * Clears the users cache. 161 * 162 * @return null 163 */ 164 public static function clear_users_cache() { 165 self::$cachedusers = []; 166 } 167 168 /** 169 * Returns a search instance of the specified area checking internal caching. 170 * 171 * @param string $areaid Area id 172 * @return \core_search\base 173 */ 174 protected function get_search_area($areaid) { 175 176 if (isset($this->cachedareas[$areaid]) && $this->cachedareas[$areaid] === false) { 177 // We already checked that area and it is not available. 178 return false; 179 } 180 181 if (!isset($this->cachedareas[$areaid])) { 182 // First result that matches this area. 183 184 $this->cachedareas[$areaid] = \core_search\manager::get_search_area($areaid); 185 if ($this->cachedareas[$areaid] === false) { 186 // The area does not exist or it is not available any more. 187 188 $this->cachedareas[$areaid] = false; 189 return false; 190 } 191 192 if (!$this->cachedareas[$areaid]->is_enabled()) { 193 // We skip the area if it is not enabled. 194 195 // Marking it as false so next time we don' need to check it again. 196 $this->cachedareas[$areaid] = false; 197 198 return false; 199 } 200 } 201 202 return $this->cachedareas[$areaid]; 203 } 204 205 /** 206 * Returns a document instance prepared to be rendered. 207 * 208 * @param \core_search\base $searcharea 209 * @param array $docdata 210 * @return \core_search\document 211 */ 212 protected function to_document(\core_search\base $searcharea, $docdata) { 213 214 list($componentname, $areaname) = \core_search\manager::extract_areaid_parts($docdata['areaid']); 215 $doc = \core_search\document_factory::instance($docdata['itemid'], $componentname, $areaname, $this); 216 $doc->set_data_from_engine($docdata); 217 $doc->set_doc_url($searcharea->get_doc_url($doc)); 218 $doc->set_context_url($searcharea->get_context_url($doc)); 219 $doc->set_doc_icon($searcharea->get_doc_icon($doc)); 220 221 // Uses the internal caches to get required data needed to render the document later. 222 $course = $this->get_course($doc->get('courseid')); 223 $doc->set_extra('coursefullname', $course->fullname); 224 225 if ($doc->is_set('userid')) { 226 $user = $this->get_user($doc->get('userid')); 227 $doc->set_extra('userfullname', fullname($user)); 228 } 229 230 return $doc; 231 } 232 233 /** 234 * Loop through given iterator of search documents 235 * and and have the search engine back end add them 236 * to the index. 237 * 238 * @param \iterator $iterator the iterator of documents to index 239 * @param base $searcharea the area for the documents to index 240 * @param array $options document indexing options 241 * @return array Processed document counts 242 */ 243 public function add_documents($iterator, $searcharea, $options) { 244 $numrecords = 0; 245 $numdocs = 0; 246 $numdocsignored = 0; 247 $numbatches = 0; 248 $lastindexeddoc = 0; 249 $firstindexeddoc = 0; 250 $partial = false; 251 $lastprogress = manager::get_current_time(); 252 253 $batchmode = $this->supports_add_document_batch(); 254 $currentbatch = []; 255 256 foreach ($iterator as $document) { 257 // Stop if we have exceeded the time limit (and there are still more items). Always 258 // do at least one second's worth of documents otherwise it will never make progress. 259 if ($lastindexeddoc !== $firstindexeddoc && 260 !empty($options['stopat']) && manager::get_current_time() >= $options['stopat']) { 261 $partial = true; 262 break; 263 } 264 265 if (!$document instanceof \core_search\document) { 266 continue; 267 } 268 269 if (isset($options['lastindexedtime']) && $options['lastindexedtime'] == 0) { 270 // If we have never indexed this area before, it must be new. 271 $document->set_is_new(true); 272 } 273 274 if ($options['indexfiles']) { 275 // Attach files if we are indexing. 276 $searcharea->attach_files($document); 277 } 278 279 if ($batchmode && strlen($document->get('content')) <= $this->get_batch_max_content()) { 280 $currentbatch[] = $document; 281 if (count($currentbatch) >= $this->get_batch_max_documents()) { 282 [$processed, $failed, $batches] = $this->add_document_batch($currentbatch, $options['indexfiles']); 283 $numdocs += $processed; 284 $numdocsignored += $failed; 285 $numbatches += $batches; 286 $currentbatch = []; 287 } 288 } else { 289 if ($this->add_document($document, $options['indexfiles'])) { 290 $numdocs++; 291 } else { 292 $numdocsignored++; 293 } 294 $numbatches++; 295 } 296 297 $lastindexeddoc = $document->get('modified'); 298 if (!$firstindexeddoc) { 299 $firstindexeddoc = $lastindexeddoc; 300 } 301 $numrecords++; 302 303 // If indexing the area takes a long time, periodically output progress information. 304 if (isset($options['progress'])) { 305 $now = manager::get_current_time(); 306 if ($now - $lastprogress >= manager::DISPLAY_INDEXING_PROGRESS_EVERY) { 307 $lastprogress = $now; 308 // The first date format is the same used in cron_trace_time_and_memory(). 309 $options['progress']->output(date('H:i:s', $now) . ': Done to ' . userdate( 310 $lastindexeddoc, get_string('strftimedatetimeshort', 'langconfig')), 1); 311 } 312 } 313 } 314 315 // Add remaining documents from batch. 316 if ($batchmode && $currentbatch) { 317 [$processed, $failed, $batches] = $this->add_document_batch($currentbatch, $options['indexfiles']); 318 $numdocs += $processed; 319 $numdocsignored += $failed; 320 $numbatches += $batches; 321 } 322 323 return [$numrecords, $numdocs, $numdocsignored, $lastindexeddoc, $partial, $numbatches]; 324 } 325 326 /** 327 * Returns the plugin name. 328 * 329 * @return string Frankenstyle plugin name. 330 */ 331 public function get_plugin_name() { 332 return $this->pluginname; 333 } 334 335 /** 336 * Gets the document class used by this search engine. 337 * 338 * Search engines can overwrite \core_search\document with \search_ENGINENAME\document class. 339 * 340 * Looks for a document class in the current search engine namespace, falling back to \core_search\document. 341 342 * Publicly available because search areas do not have access to the engine details, 343 * \core_search\document_factory accesses this function. 344 * 345 * @return string 346 */ 347 public function get_document_classname() { 348 $classname = $this->pluginname . '\\document'; 349 if (!class_exists($classname)) { 350 $classname = '\\core_search\\document'; 351 } 352 return $classname; 353 } 354 355 /** 356 * Run any pre-indexing operations. 357 * 358 * Should be overwritten if the search engine needs to do any pre index preparation. 359 * 360 * @param bool $fullindex True if a full index will be performed 361 * @return void 362 */ 363 public function index_starting($fullindex = false) { 364 // Nothing by default. 365 } 366 367 /** 368 * Run any post indexing operations. 369 * 370 * Should be overwritten if the search engine needs to do any post index cleanup. 371 * 372 * @param int $numdocs The number of documents that were added to the index 373 * @param bool $fullindex True if a full index was performed 374 * @return void 375 */ 376 public function index_complete($numdocs = 0, $fullindex = false) { 377 // Nothing by default. 378 } 379 380 /** 381 * Do anything that may need to be done before an area is indexed. 382 * 383 * @param \core_search\base $searcharea The search area that was complete 384 * @param bool $fullindex True if a full index is being performed 385 * @return void 386 */ 387 public function area_index_starting($searcharea, $fullindex = false) { 388 // Nothing by default. 389 } 390 391 /** 392 * Do any area cleanup needed, and do anything to confirm contents. 393 * 394 * Return false to prevent the search area completed time and stats from being updated. 395 * 396 * @param \core_search\base $searcharea The search area that was complete 397 * @param int $numdocs The number of documents that were added to the index 398 * @param bool $fullindex True if a full index is being performed 399 * @return bool True means that data is considered indexed 400 */ 401 public function area_index_complete($searcharea, $numdocs = 0, $fullindex = false) { 402 return true; 403 } 404 405 /** 406 * Optimizes the search engine. 407 * 408 * Should be overwritten if the search engine can optimize its contents. 409 * 410 * @return void 411 */ 412 public function optimize() { 413 // Nothing by default. 414 mtrace('The ' . get_string('pluginname', $this->get_plugin_name()) . 415 ' search engine does not require automatic optimization.'); 416 } 417 418 /** 419 * Does the system satisfy all the requirements. 420 * 421 * Should be overwritten if the search engine has any system dependencies 422 * that needs to be checked. 423 * 424 * @return bool 425 */ 426 public function is_installed() { 427 return true; 428 } 429 430 /** 431 * Returns any error reported by the search engine when executing the provided query. 432 * 433 * It should be called from static::execute_query when an exception is triggered. 434 * 435 * @return string 436 */ 437 public function get_query_error() { 438 return $this->queryerror; 439 } 440 441 /** 442 * Returns the total number of documents available for the most recent call to execute_query. 443 * 444 * This can be an estimate, but should get more accurate the higher the limited passed to execute_query is. 445 * To do that, the engine can use (actual result returned count + count of unchecked documents), or 446 * (total possible docs - docs that have been checked and rejected). 447 * 448 * Engine can limit to manager::MAX_RESULTS if there is cost to determining more. 449 * If this cannot be computed in a reasonable way, manager::MAX_RESULTS may be returned. 450 * 451 * @return int 452 */ 453 abstract public function get_query_total_count(); 454 455 /** 456 * Return true if file indexing is supported and enabled. False otherwise. 457 * 458 * @return bool 459 */ 460 public function file_indexing_enabled() { 461 return false; 462 } 463 464 /** 465 * Clears the current query error value. 466 * 467 * @return void 468 */ 469 public function clear_query_error() { 470 $this->queryerror = null; 471 } 472 473 /** 474 * Is the server ready to use? 475 * 476 * This should also check that the search engine configuration is ok. 477 * 478 * If the function $this->should_skip_schema_check() returns true, then this function may leave 479 * out time-consuming checks that the schema is valid. (This allows for improved performance on 480 * critical pages such as the main search form.) 481 * 482 * @return true|string Returns true if all good or an error string. 483 */ 484 abstract function is_server_ready(); 485 486 /** 487 * Tells the search engine to skip any time-consuming checks that it might do as part of the 488 * is_server_ready function, and only carry out a basic check that it can contact the server. 489 * 490 * This setting is not remembered and applies only to the current request. 491 * 492 * @since Moodle 3.5 493 * @param bool $skip True to skip the checks, false to start checking again 494 */ 495 public function skip_schema_check($skip = true) { 496 $this->skipschemacheck = $skip; 497 } 498 499 /** 500 * For use by subclasses. The engine can call this inside is_server_ready to check whether it 501 * should skip time-consuming schema checks. 502 * 503 * @since Moodle 3.5 504 * @return bool True if schema checks should be skipped 505 */ 506 protected function should_skip_schema_check() { 507 return $this->skipschemacheck; 508 } 509 510 /** 511 * Adds a document to the search engine. 512 * 513 * @param document $document 514 * @param bool $fileindexing True if file indexing is to be used 515 * @return bool False if the file was skipped or failed, true on success 516 */ 517 abstract function add_document($document, $fileindexing = false); 518 519 /** 520 * Adds multiple documents to the search engine. 521 * 522 * It should return the number successfully processed, and the number of batches they were 523 * processed in (for example if you add 100 documents and there is an error processing one of 524 * those documents, and it took 4 batches, it would return [99, 1, 4]). 525 * 526 * If the engine implements this, it should return true to {@see supports_add_document_batch}. 527 * 528 * The system will only call this function with up to {@see get_batch_max_documents} documents, 529 * and each document in the batch will have content no larger than specified by 530 * {@see get_batch_max_content}. 531 * 532 * @param document[] $documents Documents to add 533 * @param bool $fileindexing True if file indexing is to be used 534 * @return int[] Array of three elements, successfully processed, failed processed, batch count 535 */ 536 public function add_document_batch(array $documents, bool $fileindexing = false): array { 537 throw new \coding_exception('add_document_batch not supported by this engine'); 538 } 539 540 /** 541 * Executes the query on the engine. 542 * 543 * Implementations of this function should check user context array to limit the results to contexts where the 544 * user have access. They should also limit the owneruserid field to manger::NO_OWNER_ID or the current user's id. 545 * Engines must use area->check_access() to confirm user access. 546 * 547 * Engines should reasonably attempt to fill up to limit with valid results if they are available. 548 * 549 * The $filters object may include the following fields (optional except q): 550 * - q: value of main search field; results should include this text 551 * - title: if included, title must match this search 552 * - areaids: array of search area id strings (only these areas will be searched) 553 * - courseids: array of course ids (only these courses will be searched) 554 * - groupids: array of group ids (only results specifically from these groupids will be 555 * searched) - this option will be ignored if the search engine doesn't support groups 556 * 557 * The $accessinfo parameter has two different values (for historical compatibility). If the 558 * engine returns false to supports_group_filtering then it is an array of user contexts, or 559 * true if the user can access all contexts. (This parameter used to be called $usercontexts.) 560 * If the engine returns true to supports_group_filtering then it will be an object containing 561 * these fields: 562 * - everything (true if admin is searching with no restrictions) 563 * - usercontexts (same as above) 564 * - separategroupscontexts (array of context ids where separate groups are used) 565 * - visiblegroupscontextsareas (array of subset of those where some areas use visible groups) 566 * - usergroups (array of relevant group ids that user belongs to) 567 * 568 * The engine should apply group restrictions to those contexts listed in the 569 * 'separategroupscontexts' array. In these contexts, it shouled only include results if the 570 * groupid is not set, or if the groupid matches one of the values in USER_GROUPS array, or 571 * if the search area is one of those listed in 'visiblegroupscontextsareas' for that context. 572 * 573 * @param \stdClass $filters Query and filters to apply. 574 * @param \stdClass $accessinfo Information about the contexts the user can access 575 * @param int $limit The maximum number of results to return. If empty, limit to manager::MAX_RESULTS. 576 * @return \core_search\document[] Results or false if no results 577 */ 578 public abstract function execute_query($filters, $accessinfo, $limit = 0); 579 580 /** 581 * Delete all documents. 582 * 583 * @param string $areaid To filter by area 584 * @return void 585 */ 586 abstract function delete($areaid = null); 587 588 /** 589 * Deletes information related to a specific context id. This should be used when the context 590 * itself is deleted from Moodle. 591 * 592 * This only deletes information for the specified context - not for any child contexts. 593 * 594 * This function is optional; if not supported it will return false and the information will 595 * not be deleted from the search index. 596 * 597 * If an engine implements this function it should also implement delete_index_for_course; 598 * otherwise, nothing will be deleted when users delete an entire course at once. 599 * 600 * @param int $oldcontextid ID of context that has been deleted 601 * @return bool True if implemented 602 * @throws \core_search\engine_exception Engines may throw this exception for any problem 603 */ 604 public function delete_index_for_context(int $oldcontextid) { 605 return false; 606 } 607 608 /** 609 * Deletes information related to a specific course id. This should be used when the course 610 * itself is deleted from Moodle. 611 * 612 * This deletes all information relating to that course from the index, including all child 613 * contexts. 614 * 615 * This function is optional; if not supported it will return false and the information will 616 * not be deleted from the search index. 617 * 618 * If an engine implements this function then, ideally, it should also implement 619 * delete_index_for_context so that deletion of single activities/blocks also works. 620 * 621 * @param int $oldcourseid ID of course that has been deleted 622 * @return bool True if implemented 623 * @throws \core_search\engine_exception Engines may throw this exception for any problem 624 */ 625 public function delete_index_for_course(int $oldcourseid) { 626 return false; 627 } 628 629 /** 630 * Checks that the schema is the latest version. If the version stored in config does not match 631 * the current, this function will attempt to upgrade the schema. 632 * 633 * @return bool|string True if schema is OK, a string if user needs to take action 634 */ 635 public function check_latest_schema() { 636 if (empty($this->config->schemaversion)) { 637 $currentversion = 0; 638 } else { 639 $currentversion = $this->config->schemaversion; 640 } 641 if ($currentversion < document::SCHEMA_VERSION) { 642 return $this->update_schema((int)$currentversion, (int)document::SCHEMA_VERSION); 643 } else { 644 return true; 645 } 646 } 647 648 /** 649 * Usually called by the engine; marks that the schema has been updated. 650 * 651 * @param int $version Records the schema version now applied 652 */ 653 public function record_applied_schema_version($version) { 654 set_config('schemaversion', $version, $this->pluginname); 655 } 656 657 /** 658 * Requests the search engine to upgrade the schema. The engine should update the schema if 659 * possible/necessary, and should ensure that record_applied_schema_version is called as a 660 * result. 661 * 662 * If it is not possible to upgrade the schema at the moment, it can do nothing and return; the 663 * function will be called again next time search is initialised. 664 * 665 * The default implementation just returns, with a DEBUG_DEVELOPER warning. 666 * 667 * @param int $oldversion Old schema version 668 * @param int $newversion New schema version 669 * @return bool|string True if schema is updated successfully, a string if it needs updating manually 670 */ 671 protected function update_schema($oldversion, $newversion) { 672 debugging('Unable to update search engine schema: ' . $this->pluginname, DEBUG_DEVELOPER); 673 return get_string('schemanotupdated', 'search'); 674 } 675 676 /** 677 * Checks if this search engine supports groups. 678 * 679 * Note that returning true to this function causes the parameters to execute_query to be 680 * passed differently! 681 * 682 * In order to implement groups and return true to this function, the search engine should: 683 * 684 * 1. Handle the fields ->separategroupscontexts and ->usergroups in the $accessinfo parameter 685 * to execute_query (ideally, using these to automatically restrict search results). 686 * 2. Support the optional groupids parameter in the $filter parameter for execute_query to 687 * restrict results to only those where the stored groupid matches the given value. 688 * 689 * @return bool True if this engine supports searching by group id field 690 */ 691 public function supports_group_filtering() { 692 return false; 693 } 694 695 /** 696 * Obtain a list of results orders (and names for them) that are supported by this 697 * search engine in the given context. 698 * 699 * By default, engines sort by relevance only. 700 * 701 * @param \context $context Context that the user requested search from 702 * @return array Array from order name => display text 703 */ 704 public function get_supported_orders(\context $context) { 705 return ['relevance' => get_string('order_relevance', 'search')]; 706 } 707 708 /** 709 * Checks if the search engine supports searching by user. 710 * 711 * If it returns true to this function, the search engine should support the 'userids' option 712 * in the $filters value passed to execute_query(), returning only items where the userid in 713 * the search document matches one of those user ids. 714 * 715 * @return bool True if the search engine supports searching by user 716 */ 717 public function supports_users() { 718 return false; 719 } 720 721 /** 722 * Checks if the search engine supports adding documents in a batch. 723 * 724 * If it returns true to this function, the search engine must implement the add_document_batch 725 * function. 726 * 727 * @return bool True if the search engine supports adding documents in a batch 728 */ 729 public function supports_add_document_batch(): bool { 730 return false; 731 } 732 733 /** 734 * Gets the maximum number of documents to send together in batch mode. 735 * 736 * Only relevant if the engine returns true to {@see supports_add_document_batch}. 737 * 738 * Can be overridden by search engine if required. 739 * 740 * @var int Number of documents to send together in batch mode, default 100. 741 */ 742 public function get_batch_max_documents(): int { 743 return 100; 744 } 745 746 /** 747 * Gets the maximum size of document content to be included in a shared batch (if the 748 * document is bigger then it will be sent on its own; batching does not provide a performance 749 * improvement for big documents anyway). 750 * 751 * Only relevant if the engine returns true to {@see supports_add_document_batch}. 752 * 753 * Can be overridden by search engine if required. 754 * 755 * @return int Max size in bytes, default 1MB 756 */ 757 public function get_batch_max_content(): int { 758 return 1024 * 1024; 759 } 760 761 /** 762 * Checks if the search engine has an alternate configuration. 763 * 764 * This is used where the same search engine class supports two different configurations, 765 * which are both shown on the settings screen. The alternate configuration is selected by 766 * passing 'true' parameter to the constructor. 767 * 768 * The feature is used when a different connection is in use for indexing vs. querying 769 * the search engine. 770 * 771 * This function should only return true if the engine supports an alternate configuration 772 * and the user has filled in the settings. (We do not need to test they are valid, that will 773 * happen as normal.) 774 * 775 * @return bool True if an alternate configuration is defined 776 */ 777 public function has_alternate_configuration(): bool { 778 return false; 779 } 780 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body