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 * Solr earch engine base unit tests. 19 * 20 * Required params: 21 * - define('TEST_SEARCH_SOLR_HOSTNAME', '127.0.0.1'); 22 * - define('TEST_SEARCH_SOLR_PORT', '8983'); 23 * - define('TEST_SEARCH_SOLR_INDEXNAME', 'unittest'); 24 * 25 * Optional params: 26 * - define('TEST_SEARCH_SOLR_USERNAME', ''); 27 * - define('TEST_SEARCH_SOLR_PASSWORD', ''); 28 * - define('TEST_SEARCH_SOLR_SSLCERT', ''); 29 * - define('TEST_SEARCH_SOLR_SSLKEY', ''); 30 * - define('TEST_SEARCH_SOLR_KEYPASSWORD', ''); 31 * - define('TEST_SEARCH_SOLR_CAINFOCERT', ''); 32 * 33 * @package search_solr 34 * @category phpunit 35 * @copyright 2015 David Monllao {@link http://www.davidmonllao.com} 36 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 37 */ 38 39 defined('MOODLE_INTERNAL') || die(); 40 41 global $CFG; 42 require_once($CFG->dirroot . '/search/tests/fixtures/testable_core_search.php'); 43 require_once($CFG->dirroot . '/search/tests/fixtures/mock_search_area.php'); 44 require_once($CFG->dirroot . '/search/engine/solr/tests/fixtures/testable_engine.php'); 45 46 /** 47 * Solr search engine base unit tests. 48 * 49 * @package search_solr 50 * @category phpunit 51 * @copyright 2015 David Monllao {@link http://www.davidmonllao.com} 52 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 53 * 54 * @runTestsInSeparateProcesses 55 */ 56 class search_solr_engine_testcase extends advanced_testcase { 57 58 /** 59 * @var \core_search\manager 60 */ 61 protected $search = null; 62 63 /** 64 * @var Instace of core_search_generator. 65 */ 66 protected $generator = null; 67 68 /** 69 * @var Instace of testable_engine. 70 */ 71 protected $engine = null; 72 73 public function setUp(): void { 74 $this->resetAfterTest(); 75 set_config('enableglobalsearch', true); 76 set_config('searchengine', 'solr'); 77 78 if (!function_exists('solr_get_version')) { 79 $this->markTestSkipped('Solr extension is not loaded.'); 80 } 81 82 if (!defined('TEST_SEARCH_SOLR_HOSTNAME') || !defined('TEST_SEARCH_SOLR_INDEXNAME') || 83 !defined('TEST_SEARCH_SOLR_PORT')) { 84 $this->markTestSkipped('Solr extension test server not set.'); 85 } 86 87 set_config('server_hostname', TEST_SEARCH_SOLR_HOSTNAME, 'search_solr'); 88 set_config('server_port', TEST_SEARCH_SOLR_PORT, 'search_solr'); 89 set_config('indexname', TEST_SEARCH_SOLR_INDEXNAME, 'search_solr'); 90 91 if (defined('TEST_SEARCH_SOLR_USERNAME')) { 92 set_config('server_username', TEST_SEARCH_SOLR_USERNAME, 'search_solr'); 93 } 94 95 if (defined('TEST_SEARCH_SOLR_PASSWORD')) { 96 set_config('server_password', TEST_SEARCH_SOLR_PASSWORD, 'search_solr'); 97 } 98 99 if (defined('TEST_SEARCH_SOLR_SSLCERT')) { 100 set_config('secure', true, 'search_solr'); 101 set_config('ssl_cert', TEST_SEARCH_SOLR_SSLCERT, 'search_solr'); 102 } 103 104 if (defined('TEST_SEARCH_SOLR_SSLKEY')) { 105 set_config('ssl_key', TEST_SEARCH_SOLR_SSLKEY, 'search_solr'); 106 } 107 108 if (defined('TEST_SEARCH_SOLR_KEYPASSWORD')) { 109 set_config('ssl_keypassword', TEST_SEARCH_SOLR_KEYPASSWORD, 'search_solr'); 110 } 111 112 if (defined('TEST_SEARCH_SOLR_CAINFOCERT')) { 113 set_config('ssl_cainfo', TEST_SEARCH_SOLR_CAINFOCERT, 'search_solr'); 114 } 115 116 set_config('fileindexing', 1, 'search_solr'); 117 118 // We are only test indexing small string files, so setting this as low as we can. 119 set_config('maxindexfilekb', 1, 'search_solr'); 120 121 $this->generator = self::getDataGenerator()->get_plugin_generator('core_search'); 122 $this->generator->setup(); 123 124 // Inject search solr engine into the testable core search as we need to add the mock 125 // search component to it. 126 $this->engine = new \search_solr\testable_engine(); 127 $this->search = testable_core_search::instance($this->engine); 128 $areaid = \core_search\manager::generate_areaid('core_mocksearch', 'mock_search_area'); 129 $this->search->add_search_area($areaid, new core_mocksearch\search\mock_search_area()); 130 131 $this->setAdminUser(); 132 133 // Cleanup before doing anything on it as the index it is out of this test control. 134 $this->search->delete_index(); 135 136 // Add moodle fields if they don't exist. 137 $schema = new \search_solr\schema($this->engine); 138 $schema->setup(false); 139 } 140 141 public function tearDown(): void { 142 // For unit tests before PHP 7, teardown is called even on skip. So only do our teardown if we did setup. 143 if ($this->generator) { 144 // Moodle DML freaks out if we don't teardown the temp table after each run. 145 $this->generator->teardown(); 146 $this->generator = null; 147 } 148 } 149 150 /** 151 * Simple data provider to allow tests to be run with file indexing on and off. 152 */ 153 public function file_indexing_provider() { 154 return array( 155 'file-indexing-on' => array(1), 156 'file-indexing-off' => array(0) 157 ); 158 } 159 160 public function test_connection() { 161 $this->assertTrue($this->engine->is_server_ready()); 162 } 163 164 /** 165 * Tests that the alternate settings are used when configured. 166 */ 167 public function test_alternate_settings() { 168 // Index a couple of things. 169 $this->generator->create_record(); 170 $this->generator->create_record(); 171 $this->search->index(); 172 173 // By default settings, alternates are not set. 174 $this->assertFalse($this->engine->has_alternate_configuration()); 175 176 // Set up all the config the same as normal. 177 foreach (['server_hostname', 'indexname', 'secure', 'server_port', 178 'server_username', 'server_password'] as $setting) { 179 set_config('alternate' . $setting, get_config('search_solr', $setting), 'search_solr'); 180 } 181 // Also mess up the normal config. 182 set_config('indexname', 'not_the_right_index_name', 'search_solr'); 183 184 // Construct a new engine using normal settings. 185 $engine = new search_solr\engine(); 186 187 // Now alternates are available. 188 $this->assertTrue($engine->has_alternate_configuration()); 189 190 // But it won't actually work because of the bogus index name. 191 $this->assertFalse($engine->is_server_ready() === true); 192 $this->assertDebuggingCalled(); 193 194 // But if we construct one using alternate settings, it will work as normal. 195 $engine = new search_solr\engine(true); 196 $this->assertTrue($engine->is_server_ready()); 197 198 // Including finding the search results. 199 $this->assertCount(2, $engine->execute_query( 200 (object)['q' => 'message'], (object)['everything' => true])); 201 } 202 203 /** 204 * @dataProvider file_indexing_provider 205 */ 206 public function test_index($fileindexing) { 207 global $DB; 208 209 $this->engine->test_set_config('fileindexing', $fileindexing); 210 211 $record = new \stdClass(); 212 $record->timemodified = time() - 1; 213 $this->generator->create_record($record); 214 215 // Data gets into the search engine. 216 $this->assertTrue($this->search->index()); 217 218 // Not anymore as everything was already added. 219 sleep(1); 220 $this->assertFalse($this->search->index()); 221 222 $this->generator->create_record(); 223 224 // Indexing again once there is new data. 225 $this->assertTrue($this->search->index()); 226 } 227 228 /** 229 * Better keep this not very strict about which or how many results are returned as may depend on solr engine config. 230 * 231 * @dataProvider file_indexing_provider 232 * 233 * @return void 234 */ 235 public function test_search($fileindexing) { 236 global $USER, $DB; 237 238 $this->engine->test_set_config('fileindexing', $fileindexing); 239 240 $this->generator->create_record(); 241 $record = new \stdClass(); 242 $record->title = "Special title"; 243 $this->generator->create_record($record); 244 245 $this->search->index(); 246 247 $querydata = new stdClass(); 248 $querydata->q = 'message'; 249 $results = $this->search->search($querydata); 250 $this->assertCount(2, $results); 251 252 // Based on core_mocksearch\search\indexer. 253 $this->assertEquals($USER->id, $results[0]->get('userid')); 254 $this->assertEquals(\context_course::instance(SITEID)->id, $results[0]->get('contextid')); 255 256 // Do a test to make sure we aren't searching non-query fields, like areaid. 257 $querydata->q = \core_search\manager::generate_areaid('core_mocksearch', 'mock_search_area'); 258 $this->assertCount(0, $this->search->search($querydata)); 259 $querydata->q = 'message'; 260 261 sleep(1); 262 $beforeadding = time(); 263 sleep(1); 264 $this->generator->create_record(); 265 $this->search->index(); 266 267 // Timestart. 268 $querydata->timestart = $beforeadding; 269 $this->assertCount(1, $this->search->search($querydata)); 270 271 // Timeend. 272 unset($querydata->timestart); 273 $querydata->timeend = $beforeadding; 274 $this->assertCount(2, $this->search->search($querydata)); 275 276 // Title. 277 unset($querydata->timeend); 278 $querydata->title = 'Special title'; 279 $this->assertCount(1, $this->search->search($querydata)); 280 281 // Course IDs. 282 unset($querydata->title); 283 $querydata->courseids = array(SITEID + 1); 284 $this->assertCount(0, $this->search->search($querydata)); 285 286 $querydata->courseids = array(SITEID); 287 $this->assertCount(3, $this->search->search($querydata)); 288 289 // Now try some area-id combinations. 290 unset($querydata->courseids); 291 $forumpostareaid = \core_search\manager::generate_areaid('mod_forum', 'post'); 292 $mockareaid = \core_search\manager::generate_areaid('core_mocksearch', 'mock_search_area'); 293 294 $querydata->areaids = array($forumpostareaid); 295 $this->assertCount(0, $this->search->search($querydata)); 296 297 $querydata->areaids = array($forumpostareaid, $mockareaid); 298 $this->assertCount(3, $this->search->search($querydata)); 299 300 $querydata->areaids = array($mockareaid); 301 $this->assertCount(3, $this->search->search($querydata)); 302 303 $querydata->areaids = array(); 304 $this->assertCount(3, $this->search->search($querydata)); 305 306 // Check that index contents get updated. 307 $this->generator->delete_all(); 308 $this->search->index(true); 309 unset($querydata->title); 310 $querydata->q = '*'; 311 $this->assertCount(0, $this->search->search($querydata)); 312 } 313 314 /** 315 * @dataProvider file_indexing_provider 316 */ 317 public function test_delete($fileindexing) { 318 $this->engine->test_set_config('fileindexing', $fileindexing); 319 320 $this->generator->create_record(); 321 $this->generator->create_record(); 322 $this->search->index(); 323 324 $querydata = new stdClass(); 325 $querydata->q = 'message'; 326 327 $this->assertCount(2, $this->search->search($querydata)); 328 329 $areaid = \core_search\manager::generate_areaid('core_mocksearch', 'mock_search_area'); 330 $this->search->delete_index($areaid); 331 $this->assertCount(0, $this->search->search($querydata)); 332 } 333 334 /** 335 * @dataProvider file_indexing_provider 336 */ 337 public function test_alloweduserid($fileindexing) { 338 $this->engine->test_set_config('fileindexing', $fileindexing); 339 340 $area = new core_mocksearch\search\mock_search_area(); 341 342 $record = $this->generator->create_record(); 343 344 // Get the doc and insert the default doc. 345 $doc = $area->get_document($record); 346 $this->engine->add_document($doc); 347 348 $users = array(); 349 $users[] = $this->getDataGenerator()->create_user(); 350 $users[] = $this->getDataGenerator()->create_user(); 351 $users[] = $this->getDataGenerator()->create_user(); 352 353 // Add a record that only user 100 can see. 354 $originalid = $doc->get('id'); 355 356 // Now add a custom doc for each user. 357 foreach ($users as $user) { 358 $doc = $area->get_document($record); 359 $doc->set('id', $originalid.'-'.$user->id); 360 $doc->set('owneruserid', $user->id); 361 $this->engine->add_document($doc); 362 } 363 364 $this->engine->area_index_complete($area->get_area_id()); 365 366 $querydata = new stdClass(); 367 $querydata->q = 'message'; 368 $querydata->title = $doc->get('title'); 369 370 // We are going to go through each user and see if they get the original and the owned doc. 371 foreach ($users as $user) { 372 $this->setUser($user); 373 374 $results = $this->search->search($querydata); 375 $this->assertCount(2, $results); 376 377 $owned = 0; 378 $notowned = 0; 379 380 // We don't know what order we will get the results in, so we are doing this. 381 foreach ($results as $result) { 382 $owneruserid = $result->get('owneruserid'); 383 if (empty($owneruserid)) { 384 $notowned++; 385 $this->assertEquals(0, $owneruserid); 386 $this->assertEquals($originalid, $result->get('id')); 387 } else { 388 $owned++; 389 $this->assertEquals($user->id, $owneruserid); 390 $this->assertEquals($originalid.'-'.$user->id, $result->get('id')); 391 } 392 } 393 394 $this->assertEquals(1, $owned); 395 $this->assertEquals(1, $notowned); 396 } 397 398 // Now test a user with no owned results. 399 $otheruser = $this->getDataGenerator()->create_user(); 400 $this->setUser($otheruser); 401 402 $results = $this->search->search($querydata); 403 $this->assertCount(1, $results); 404 405 $this->assertEquals(0, $results[0]->get('owneruserid')); 406 $this->assertEquals($originalid, $results[0]->get('id')); 407 } 408 409 /** 410 * @dataProvider file_indexing_provider 411 */ 412 public function test_highlight($fileindexing) { 413 global $PAGE; 414 415 $this->engine->test_set_config('fileindexing', $fileindexing); 416 417 $this->generator->create_record(); 418 $this->search->index(); 419 420 $querydata = new stdClass(); 421 $querydata->q = 'message'; 422 423 $results = $this->search->search($querydata); 424 $this->assertCount(1, $results); 425 426 $result = reset($results); 427 428 $regex = '|'.\search_solr\engine::HIGHLIGHT_START.'message'.\search_solr\engine::HIGHLIGHT_END.'|'; 429 $this->assertRegExp($regex, $result->get('content')); 430 431 $searchrenderer = $PAGE->get_renderer('core_search'); 432 $exported = $result->export_for_template($searchrenderer); 433 434 $regex = '|<span class="highlight">message</span>|'; 435 $this->assertRegExp($regex, $exported['content']); 436 } 437 438 public function test_export_file_for_engine() { 439 // Get area to work with. 440 $areaid = \core_search\manager::generate_areaid('core_mocksearch', 'mock_search_area'); 441 $area = \core_search\manager::get_search_area($areaid); 442 443 $record = $this->generator->create_record(); 444 445 $doc = $area->get_document($record); 446 $filerecord = new stdClass(); 447 $filerecord->timemodified = 978310800; 448 $file = $this->generator->create_file($filerecord); 449 $doc->add_stored_file($file); 450 451 $filearray = $doc->export_file_for_engine($file); 452 453 $this->assertEquals(\core_search\manager::TYPE_FILE, $filearray['type']); 454 $this->assertEquals($file->get_id(), $filearray['solr_fileid']); 455 $this->assertEquals($file->get_contenthash(), $filearray['solr_filecontenthash']); 456 $this->assertEquals(\search_solr\document::INDEXED_FILE_TRUE, $filearray['solr_fileindexstatus']); 457 $this->assertEquals($file->get_filename(), $filearray['title']); 458 $this->assertEquals(978310800, \search_solr\document::import_time_from_engine($filearray['modified'])); 459 } 460 461 public function test_index_file() { 462 // Very simple test. 463 $file = $this->generator->create_file(); 464 465 $record = new \stdClass(); 466 $record->attachfileids = array($file->get_id()); 467 $this->generator->create_record($record); 468 469 $this->search->index(); 470 $querydata = new stdClass(); 471 $querydata->q = '"File contents"'; 472 473 $this->assertCount(1, $this->search->search($querydata)); 474 } 475 476 public function test_reindexing_files() { 477 // Get area to work with. 478 $areaid = \core_search\manager::generate_areaid('core_mocksearch', 'mock_search_area'); 479 $area = \core_search\manager::get_search_area($areaid); 480 481 $record = $this->generator->create_record(); 482 483 $doc = $area->get_document($record); 484 485 // Now we are going to make some files. 486 $fs = get_file_storage(); 487 $syscontext = \context_system::instance(); 488 489 $files = array(); 490 491 $filerecord = new \stdClass(); 492 // We make enough so that we pass the 500 files threashold. That is the boundary when getting files. 493 $boundary = 500; 494 $top = (int)($boundary * 1.1); 495 for ($i = 0; $i < $top; $i++) { 496 $filerecord->filename = 'searchfile'.$i; 497 $filerecord->content = 'Some FileContents'.$i; 498 $file = $this->generator->create_file($filerecord); 499 $doc->add_stored_file($file); 500 $files[] = $file; 501 } 502 503 // Add the doc with lots of files, then commit. 504 $this->engine->add_document($doc, true); 505 $this->engine->area_index_complete($area->get_area_id()); 506 507 // Indexes we are going to check. 0 means we will delete, 1 means we will keep. 508 $checkfiles = array( 509 0 => 0, // Check the begining of the set. 510 1 => 1, 511 2 => 0, 512 ($top - 3) => 0, // Check the end of the set. 513 ($top - 2) => 1, 514 ($top - 1) => 0, 515 ($boundary - 2) => 0, // Check at the boundary between fetch groups. 516 ($boundary - 1) => 0, 517 $boundary => 0, 518 ($boundary + 1) => 0, 519 ((int)($boundary * 0.5)) => 1, // Make sure we keep some middle ones. 520 ((int)($boundary * 1.05)) => 1 521 ); 522 523 $querydata = new stdClass(); 524 525 // First, check that all the files are currently there. 526 foreach ($checkfiles as $key => $unused) { 527 $querydata->q = 'FileContents'.$key; 528 $this->assertCount(1, $this->search->search($querydata)); 529 $querydata->q = 'searchfile'.$key; 530 $this->assertCount(1, $this->search->search($querydata)); 531 } 532 533 // Remove the files we want removed from the files array. 534 foreach ($checkfiles as $key => $keep) { 535 if (!$keep) { 536 unset($files[$key]); 537 } 538 } 539 540 // And make us a new file to add. 541 $filerecord->filename = 'searchfileNew'; 542 $filerecord->content = 'Some FileContentsNew'; 543 $files[] = $this->generator->create_file($filerecord); 544 $checkfiles['New'] = 1; 545 546 $doc = $area->get_document($record); 547 foreach($files as $file) { 548 $doc->add_stored_file($file); 549 } 550 551 // Reindex the document with the changed files. 552 $this->engine->add_document($doc, true); 553 $this->engine->area_index_complete($area->get_area_id()); 554 555 // Go through our check array, and see if the file is there or not. 556 foreach ($checkfiles as $key => $keep) { 557 $querydata->q = 'FileContents'.$key; 558 $this->assertCount($keep, $this->search->search($querydata)); 559 $querydata->q = 'searchfile'.$key; 560 $this->assertCount($keep, $this->search->search($querydata)); 561 } 562 563 // Now check that we get one result when we search from something in all of them. 564 $querydata->q = 'Some'; 565 $this->assertCount(1, $this->search->search($querydata)); 566 } 567 568 /** 569 * Test indexing a file we don't consider indexable. 570 */ 571 public function test_index_filtered_file() { 572 // Get area to work with. 573 $areaid = \core_search\manager::generate_areaid('core_mocksearch', 'mock_search_area'); 574 $area = \core_search\manager::get_search_area($areaid); 575 576 // Get a single record to make a doc from. 577 $record = $this->generator->create_record(); 578 579 $doc = $area->get_document($record); 580 581 // Now we are going to make some files. 582 $fs = get_file_storage(); 583 $syscontext = \context_system::instance(); 584 585 // We need to make a file greater than 1kB in size, which is the lowest filter size. 586 $filerecord = new \stdClass(); 587 $filerecord->filename = 'largefile'; 588 $filerecord->content = 'Some LargeFindContent to find.'; 589 for ($i = 0; $i < 200; $i++) { 590 $filerecord->content .= ' The quick brown fox jumps over the lazy dog.'; 591 } 592 593 $this->assertGreaterThan(1024, strlen($filerecord->content)); 594 595 $file = $this->generator->create_file($filerecord); 596 $doc->add_stored_file($file); 597 598 $filerecord->filename = 'smallfile'; 599 $filerecord->content = 'Some SmallFindContent to find.'; 600 $file = $this->generator->create_file($filerecord); 601 $doc->add_stored_file($file); 602 603 $this->engine->add_document($doc, true); 604 $this->engine->area_index_complete($area->get_area_id()); 605 606 $querydata = new stdClass(); 607 // We shouldn't be able to find the large file contents. 608 $querydata->q = 'LargeFindContent'; 609 $this->assertCount(0, $this->search->search($querydata)); 610 611 // But we should be able to find the filename. 612 $querydata->q = 'largefile'; 613 $this->assertCount(1, $this->search->search($querydata)); 614 615 // We should be able to find the small file contents. 616 $querydata->q = 'SmallFindContent'; 617 $this->assertCount(1, $this->search->search($querydata)); 618 619 // And we should be able to find the filename. 620 $querydata->q = 'smallfile'; 621 $this->assertCount(1, $this->search->search($querydata)); 622 } 623 624 public function test_delete_by_id() { 625 // First get files in the index. 626 $file = $this->generator->create_file(); 627 $record = new \stdClass(); 628 $record->attachfileids = array($file->get_id()); 629 $this->generator->create_record($record); 630 $this->generator->create_record($record); 631 $this->search->index(); 632 633 $querydata = new stdClass(); 634 635 // Then search to make sure they are there. 636 $querydata->q = '"File contents"'; 637 $results = $this->search->search($querydata); 638 $this->assertCount(2, $results); 639 640 $first = reset($results); 641 $deleteid = $first->get('id'); 642 643 $this->engine->delete_by_id($deleteid); 644 645 // Check that we don't get a result for it anymore. 646 $results = $this->search->search($querydata); 647 $this->assertCount(1, $results); 648 $result = reset($results); 649 $this->assertNotEquals($deleteid, $result->get('id')); 650 } 651 652 /** 653 * Test that expected results are returned, even with low check_access success rate. 654 * 655 * @dataProvider file_indexing_provider 656 */ 657 public function test_solr_filling($fileindexing) { 658 $this->engine->test_set_config('fileindexing', $fileindexing); 659 660 $user1 = self::getDataGenerator()->create_user(); 661 $user2 = self::getDataGenerator()->create_user(); 662 663 // We are going to create a bunch of records that user 1 can see with 2 keywords. 664 // Then we are going to create a bunch for user 2 with only 1 of the keywords. 665 // If user 2 searches for both keywords, solr will return all of the user 1 results, then the user 2 results. 666 // This is because the user 1 results will match 2 keywords, while the others will match only 1. 667 668 $record = new \stdClass(); 669 670 // First create a bunch of records for user 1 to see. 671 $record->denyuserids = array($user2->id); 672 $record->content = 'Something1 Something2'; 673 $maxresults = (int)(\core_search\manager::MAX_RESULTS * .75); 674 for ($i = 0; $i < $maxresults; $i++) { 675 $this->generator->create_record($record); 676 } 677 678 // Then create a bunch of records for user 2 to see. 679 $record->denyuserids = array($user1->id); 680 $record->content = 'Something1'; 681 for ($i = 0; $i < $maxresults; $i++) { 682 $this->generator->create_record($record); 683 } 684 685 $this->search->index(); 686 687 // Check that user 1 sees all their results. 688 $this->setUser($user1); 689 $querydata = new stdClass(); 690 $querydata->q = 'Something1 Something2'; 691 $results = $this->search->search($querydata); 692 $this->assertCount($maxresults, $results); 693 694 // Check that user 2 will see theirs, even though they may be crouded out. 695 $this->setUser($user2); 696 $results = $this->search->search($querydata); 697 $this->assertCount($maxresults, $results); 698 } 699 700 /** 701 * Create 40 docs, that will be return from Solr in 10 hidden, 10 visible, 10 hidden, 10 visible if you query for: 702 * Something1 Something2 Something3 Something4, with the specified user set. 703 */ 704 protected function setup_user_hidden_docs($user) { 705 // These results will come first, and will not be visible by the user. 706 $record = new \stdClass(); 707 $record->denyuserids = array($user->id); 708 $record->content = 'Something1 Something2 Something3 Something4'; 709 for ($i = 0; $i < 10; $i++) { 710 $this->generator->create_record($record); 711 } 712 713 // These results will come second, and will be visible by the user. 714 unset($record->denyuserids); 715 $record->content = 'Something1 Something2 Something3'; 716 for ($i = 0; $i < 10; $i++) { 717 $this->generator->create_record($record); 718 } 719 720 // These results will come third, and will not be visible by the user. 721 $record->denyuserids = array($user->id); 722 $record->content = 'Something1 Something2'; 723 for ($i = 0; $i < 10; $i++) { 724 $this->generator->create_record($record); 725 } 726 727 // These results will come fourth, and will be visible by the user. 728 unset($record->denyuserids); 729 $record->content = 'Something1 '; 730 for ($i = 0; $i < 10; $i++) { 731 $this->generator->create_record($record); 732 } 733 } 734 735 /** 736 * Test that counts are what we expect. 737 * 738 * @dataProvider file_indexing_provider 739 */ 740 public function test_get_query_total_count($fileindexing) { 741 $this->engine->test_set_config('fileindexing', $fileindexing); 742 743 $user = self::getDataGenerator()->create_user(); 744 $this->setup_user_hidden_docs($user); 745 $this->search->index(); 746 747 $this->setUser($user); 748 $querydata = new stdClass(); 749 $querydata->q = 'Something1 Something2 Something3 Something4'; 750 751 // In this first set, it should have determined the first 10 of 40 are bad, so there could be up to 30 left. 752 $results = $this->engine->execute_query($querydata, (object)['everything' => true], 5); 753 $this->assertEquals(30, $this->engine->get_query_total_count()); 754 $this->assertCount(5, $results); 755 756 // To get to 15, it has to process the first 10 that are bad, 10 that are good, 10 that are bad, then 5 that are good. 757 // So we now know 20 are bad out of 40. 758 $results = $this->engine->execute_query($querydata, (object)['everything' => true], 15); 759 $this->assertEquals(20, $this->engine->get_query_total_count()); 760 $this->assertCount(15, $results); 761 762 // Try to get more then all, make sure we still see 20 count and 20 returned. 763 $results = $this->engine->execute_query($querydata, (object)['everything' => true], 30); 764 $this->assertEquals(20, $this->engine->get_query_total_count()); 765 $this->assertCount(20, $results); 766 } 767 768 /** 769 * Test that paged results are what we expect. 770 * 771 * @dataProvider file_indexing_provider 772 */ 773 public function test_manager_paged_search($fileindexing) { 774 $this->engine->test_set_config('fileindexing', $fileindexing); 775 776 $user = self::getDataGenerator()->create_user(); 777 $this->setup_user_hidden_docs($user); 778 $this->search->index(); 779 780 // Check that user 1 sees all their results. 781 $this->setUser($user); 782 $querydata = new stdClass(); 783 $querydata->q = 'Something1 Something2 Something3 Something4'; 784 785 // On this first page, it should have determined the first 10 of 40 are bad, so there could be up to 30 left. 786 $results = $this->search->paged_search($querydata, 0); 787 $this->assertEquals(30, $results->totalcount); 788 $this->assertCount(10, $results->results); 789 $this->assertEquals(0, $results->actualpage); 790 791 // On the second page, it should have found the next 10 bad ones, so we no know there are only 20 total. 792 $results = $this->search->paged_search($querydata, 1); 793 $this->assertEquals(20, $results->totalcount); 794 $this->assertCount(10, $results->results); 795 $this->assertEquals(1, $results->actualpage); 796 797 // Try to get an additional page - we should get back page 1 results, since that is the last page with valid results. 798 $results = $this->search->paged_search($querydata, 2); 799 $this->assertEquals(20, $results->totalcount); 800 $this->assertCount(10, $results->results); 801 $this->assertEquals(1, $results->actualpage); 802 } 803 804 /** 805 * Tests searching for results restricted to context id. 806 */ 807 public function test_context_restriction() { 808 // Use real search areas. 809 $this->search->clear_static(); 810 $this->search->add_core_search_areas(); 811 812 // Create 2 courses and some forums. 813 $generator = $this->getDataGenerator(); 814 $course1 = $generator->create_course(['fullname' => 'Course 1', 'summary' => 'xyzzy']); 815 $contextc1 = \context_course::instance($course1->id); 816 $course1forum1 = $generator->create_module('forum', ['course' => $course1, 817 'name' => 'C1F1', 'intro' => 'xyzzy']); 818 $contextc1f1 = \context_module::instance($course1forum1->cmid); 819 $course1forum2 = $generator->create_module('forum', ['course' => $course1, 820 'name' => 'C1F2', 'intro' => 'xyzzy']); 821 $contextc1f2 = \context_module::instance($course1forum2->cmid); 822 $course2 = $generator->create_course(['fullname' => 'Course 2', 'summary' => 'xyzzy']); 823 $contextc2 = \context_course::instance($course1->id); 824 $course2forum = $generator->create_module('forum', ['course' => $course2, 825 'name' => 'C2F', 'intro' => 'xyzzy']); 826 $contextc2f = \context_module::instance($course2forum->cmid); 827 828 // Index the courses and forums. 829 $this->search->index(); 830 831 // Search as admin user should find everything. 832 $querydata = new stdClass(); 833 $querydata->q = 'xyzzy'; 834 $results = $this->search->search($querydata); 835 $this->assert_result_titles( 836 ['Course 1', 'Course 2', 'C1F1', 'C1F2', 'C2F'], $results); 837 838 // Admin user manually restricts results by context id to include one course and one forum. 839 $querydata->contextids = [$contextc2f->id, $contextc1->id]; 840 $results = $this->search->search($querydata); 841 $this->assert_result_titles(['Course 1', 'C2F'], $results); 842 843 // Student enrolled in only one course, same restriction, only has the available results. 844 $student2 = $generator->create_user(); 845 $generator->enrol_user($student2->id, $course2->id, 'student'); 846 $this->setUser($student2); 847 $results = $this->search->search($querydata); 848 $this->assert_result_titles(['C2F'], $results); 849 850 // Student enrolled in both courses, same restriction, same results as admin. 851 $student1 = $generator->create_user(); 852 $generator->enrol_user($student1->id, $course1->id, 'student'); 853 $generator->enrol_user($student1->id, $course2->id, 'student'); 854 $this->setUser($student1); 855 $results = $this->search->search($querydata); 856 $this->assert_result_titles(['Course 1', 'C2F'], $results); 857 858 // Restrict both course and context. 859 $querydata->courseids = [$course2->id]; 860 $results = $this->search->search($querydata); 861 $this->assert_result_titles(['C2F'], $results); 862 unset($querydata->courseids); 863 864 // Restrict both area and context. 865 $querydata->areaids = ['core_course-course']; 866 $results = $this->search->search($querydata); 867 $this->assert_result_titles(['Course 1'], $results); 868 869 // Restrict area and context, incompatibly - this has no results (and doesn't do a query). 870 $querydata->contextids = [$contextc2f->id]; 871 $results = $this->search->search($querydata); 872 $this->assert_result_titles([], $results); 873 } 874 875 /** 876 * Tests searching for results in groups, either by specified group ids or based on user 877 * access permissions. 878 */ 879 public function test_groups() { 880 global $USER; 881 882 // Use real search areas. 883 $this->search->clear_static(); 884 $this->search->add_core_search_areas(); 885 886 // Create 2 courses and a selection of forums with different group mode. 887 $generator = $this->getDataGenerator(); 888 $course1 = $generator->create_course(['fullname' => 'Course 1']); 889 $forum1nogroups = $generator->create_module('forum', ['course' => $course1, 'groupmode' => NOGROUPS]); 890 $forum1separategroups = $generator->create_module('forum', ['course' => $course1, 'groupmode' => SEPARATEGROUPS]); 891 $forum1visiblegroups = $generator->create_module('forum', ['course' => $course1, 'groupmode' => VISIBLEGROUPS]); 892 $course2 = $generator->create_course(['fullname' => 'Course 2']); 893 $forum2separategroups = $generator->create_module('forum', ['course' => $course2, 'groupmode' => SEPARATEGROUPS]); 894 895 // Create two groups on each course. 896 $group1a = $generator->create_group(['courseid' => $course1->id]); 897 $group1b = $generator->create_group(['courseid' => $course1->id]); 898 $group2a = $generator->create_group(['courseid' => $course2->id]); 899 $group2b = $generator->create_group(['courseid' => $course2->id]); 900 901 // Create search records in each activity and (where relevant) in each group. 902 $forumgenerator = $generator->get_plugin_generator('mod_forum'); 903 $forumgenerator->create_discussion(['course' => $course1->id, 'userid' => $USER->id, 904 'forum' => $forum1nogroups->id, 'name' => 'F1NG', 'message' => 'xyzzy']); 905 $forumgenerator->create_discussion(['course' => $course1->id, 'userid' => $USER->id, 906 'forum' => $forum1separategroups->id, 'name' => 'F1SG-A', 'message' => 'xyzzy', 907 'groupid' => $group1a->id]); 908 $forumgenerator->create_discussion(['course' => $course1->id, 'userid' => $USER->id, 909 'forum' => $forum1separategroups->id, 'name' => 'F1SG-B', 'message' => 'xyzzy', 910 'groupid' => $group1b->id]); 911 $forumgenerator->create_discussion(['course' => $course1->id, 'userid' => $USER->id, 912 'forum' => $forum1visiblegroups->id, 'name' => 'F1VG-A', 'message' => 'xyzzy', 913 'groupid' => $group1a->id]); 914 $forumgenerator->create_discussion(['course' => $course1->id, 'userid' => $USER->id, 915 'forum' => $forum1visiblegroups->id, 'name' => 'F1VG-B', 'message' => 'xyzzy', 916 'groupid' => $group1b->id]); 917 $forumgenerator->create_discussion(['course' => $course2->id, 'userid' => $USER->id, 918 'forum' => $forum2separategroups->id, 'name' => 'F2SG-A', 'message' => 'xyzzy', 919 'groupid' => $group2a->id]); 920 $forumgenerator->create_discussion(['course' => $course2->id, 'userid' => $USER->id, 921 'forum' => $forum2separategroups->id, 'name' => 'F2SG-B', 'message' => 'xyzzy', 922 'groupid' => $group2b->id]); 923 924 $this->search->index(); 925 926 // Search as admin user should find everything. 927 $querydata = new stdClass(); 928 $querydata->q = 'xyzzy'; 929 $results = $this->search->search($querydata); 930 $this->assert_result_titles( 931 ['F1NG', 'F1SG-A', 'F1SG-B', 'F1VG-A', 'F1VG-B', 'F2SG-A', 'F2SG-B'], $results); 932 933 // Admin user manually restricts results by groups. 934 $querydata->groupids = [$group1b->id, $group2a->id]; 935 $results = $this->search->search($querydata); 936 $this->assert_result_titles(['F1SG-B', 'F1VG-B', 'F2SG-A'], $results); 937 938 // Student enrolled in both courses but no groups. 939 $student1 = $generator->create_user(); 940 $generator->enrol_user($student1->id, $course1->id, 'student'); 941 $generator->enrol_user($student1->id, $course2->id, 'student'); 942 $this->setUser($student1); 943 944 unset($querydata->groupids); 945 $results = $this->search->search($querydata); 946 $this->assert_result_titles(['F1NG', 'F1VG-A', 'F1VG-B'], $results); 947 948 // Student enrolled in both courses and group A in both cases. 949 $student2 = $generator->create_user(); 950 $generator->enrol_user($student2->id, $course1->id, 'student'); 951 $generator->enrol_user($student2->id, $course2->id, 'student'); 952 groups_add_member($group1a, $student2); 953 groups_add_member($group2a, $student2); 954 $this->setUser($student2); 955 956 $results = $this->search->search($querydata); 957 $this->assert_result_titles(['F1NG', 'F1SG-A', 'F1VG-A', 'F1VG-B', 'F2SG-A'], $results); 958 959 // Manually restrict results to group B in course 1. 960 $querydata->groupids = [$group1b->id]; 961 $results = $this->search->search($querydata); 962 $this->assert_result_titles(['F1VG-B'], $results); 963 964 // Manually restrict results to group A in course 1. 965 $querydata->groupids = [$group1a->id]; 966 $results = $this->search->search($querydata); 967 $this->assert_result_titles(['F1SG-A', 'F1VG-A'], $results); 968 969 // Manager enrolled in both courses (has access all groups). 970 $manager = $generator->create_user(); 971 $generator->enrol_user($manager->id, $course1->id, 'manager'); 972 $generator->enrol_user($manager->id, $course2->id, 'manager'); 973 $this->setUser($manager); 974 unset($querydata->groupids); 975 $results = $this->search->search($querydata); 976 $this->assert_result_titles( 977 ['F1NG', 'F1SG-A', 'F1SG-B', 'F1VG-A', 'F1VG-B', 'F2SG-A', 'F2SG-B'], $results); 978 } 979 980 /** 981 * Tests searching for results restricted to specific user id(s). 982 */ 983 public function test_user_restriction() { 984 // Use real search areas. 985 $this->search->clear_static(); 986 $this->search->add_core_search_areas(); 987 988 // Create a course, a forum, and a glossary. 989 $generator = $this->getDataGenerator(); 990 $course = $generator->create_course(); 991 $forum = $generator->create_module('forum', ['course' => $course->id]); 992 $glossary = $generator->create_module('glossary', ['course' => $course->id]); 993 994 // Create 3 user accounts, all enrolled as students on the course. 995 $user1 = $generator->create_user(); 996 $user2 = $generator->create_user(); 997 $user3 = $generator->create_user(); 998 $generator->enrol_user($user1->id, $course->id, 'student'); 999 $generator->enrol_user($user2->id, $course->id, 'student'); 1000 $generator->enrol_user($user3->id, $course->id, 'student'); 1001 1002 // All users create a forum discussion. 1003 $forumgen = $generator->get_plugin_generator('mod_forum'); 1004 $forumgen->create_discussion(['course' => $course->id, 'forum' => $forum->id, 1005 'userid' => $user1->id, 'name' => 'Post1', 'message' => 'plugh']); 1006 $forumgen->create_discussion(['course' => $course->id, 'forum' => $forum->id, 1007 'userid' => $user2->id, 'name' => 'Post2', 'message' => 'plugh']); 1008 $forumgen->create_discussion(['course' => $course->id, 'forum' => $forum->id, 1009 'userid' => $user3->id, 'name' => 'Post3', 'message' => 'plugh']); 1010 1011 // Two of the users create entries in the glossary. 1012 $glossarygen = $generator->get_plugin_generator('mod_glossary'); 1013 $glossarygen->create_content($glossary, ['concept' => 'Entry1', 'definition' => 'plugh', 1014 'userid' => $user1->id]); 1015 $glossarygen->create_content($glossary, ['concept' => 'Entry3', 'definition' => 'plugh', 1016 'userid' => $user3->id]); 1017 1018 // Index the data. 1019 $this->search->index(); 1020 1021 // Search without user restriction should find everything. 1022 $querydata = new stdClass(); 1023 $querydata->q = 'plugh'; 1024 $results = $this->search->search($querydata); 1025 $this->assert_result_titles( 1026 ['Entry1', 'Entry3', 'Post1', 'Post2', 'Post3'], $results); 1027 1028 // Restriction to user 3 only. 1029 $querydata->userids = [$user3->id]; 1030 $results = $this->search->search($querydata); 1031 $this->assert_result_titles( 1032 ['Entry3', 'Post3'], $results); 1033 1034 // Restriction to users 1 and 2. 1035 $querydata->userids = [$user1->id, $user2->id]; 1036 $results = $this->search->search($querydata); 1037 $this->assert_result_titles( 1038 ['Entry1', 'Post1', 'Post2'], $results); 1039 1040 // Restriction to users 1 and 2 combined with context restriction. 1041 $querydata->contextids = [context_module::instance($glossary->cmid)->id]; 1042 $results = $this->search->search($querydata); 1043 $this->assert_result_titles( 1044 ['Entry1'], $results); 1045 1046 // Restriction to users 1 and 2 combined with area restriction. 1047 unset($querydata->contextids); 1048 $querydata->areaids = [\core_search\manager::generate_areaid('mod_forum', 'post')]; 1049 $results = $this->search->search($querydata); 1050 $this->assert_result_titles( 1051 ['Post1', 'Post2'], $results); 1052 } 1053 1054 /** 1055 * Tests searching for results containing words in italic text. (This used to fail.) 1056 */ 1057 public function test_italics() { 1058 global $USER; 1059 1060 // Use real search areas. 1061 $this->search->clear_static(); 1062 $this->search->add_core_search_areas(); 1063 1064 // Create a course and a forum. 1065 $generator = $this->getDataGenerator(); 1066 $course = $generator->create_course(); 1067 $forum = $generator->create_module('forum', ['course' => $course->id]); 1068 1069 // As admin user, create forum discussions with various words in italics or with underlines. 1070 $this->setAdminUser(); 1071 $forumgen = $generator->get_plugin_generator('mod_forum'); 1072 $forumgen->create_discussion(['course' => $course->id, 'forum' => $forum->id, 1073 'userid' => $USER->id, 'name' => 'Post1', 1074 'message' => '<p>This is a post about <i>frogs</i>.</p>']); 1075 $forumgen->create_discussion(['course' => $course->id, 'forum' => $forum->id, 1076 'userid' => $USER->id, 'name' => 'Post2', 1077 'message' => '<p>This is a post about <i>toads and zombies</i>.</p>']); 1078 $forumgen->create_discussion(['course' => $course->id, 'forum' => $forum->id, 1079 'userid' => $USER->id, 'name' => 'Post3', 1080 'message' => '<p>This is a post about toads_and_zombies.</p>']); 1081 $forumgen->create_discussion(['course' => $course->id, 'forum' => $forum->id, 1082 'userid' => $USER->id, 'name' => 'Post4', 1083 'message' => '<p>This is a post about _leading and trailing_ underlines.</p>']); 1084 1085 // Index the data. 1086 $this->search->index(); 1087 1088 // Search for 'frogs' should find the post. 1089 $querydata = new stdClass(); 1090 $querydata->q = 'frogs'; 1091 $results = $this->search->search($querydata); 1092 $this->assert_result_titles(['Post1'], $results); 1093 1094 // Search for 'toads' or 'zombies' should find post 2 (and not 3)... 1095 $querydata->q = 'toads'; 1096 $results = $this->search->search($querydata); 1097 $this->assert_result_titles(['Post2'], $results); 1098 $querydata->q = 'zombies'; 1099 $results = $this->search->search($querydata); 1100 $this->assert_result_titles(['Post2'], $results); 1101 1102 // Search for 'toads_and_zombies' should find post 3. 1103 $querydata->q = 'toads_and_zombies'; 1104 $results = $this->search->search($querydata); 1105 $this->assert_result_titles(['Post3'], $results); 1106 1107 // Search for '_leading' or 'trailing_' should find post 4. 1108 $querydata->q = '_leading'; 1109 $results = $this->search->search($querydata); 1110 $this->assert_result_titles(['Post4'], $results); 1111 $querydata->q = 'trailing_'; 1112 $results = $this->search->search($querydata); 1113 $this->assert_result_titles(['Post4'], $results); 1114 } 1115 1116 /** 1117 * Asserts that the returned documents have the expected titles (regardless of order). 1118 * 1119 * @param string[] $expected List of expected document titles 1120 * @param \core_search\document[] $results List of returned documents 1121 */ 1122 protected function assert_result_titles(array $expected, array $results) { 1123 $titles = []; 1124 foreach ($results as $result) { 1125 $titles[] = $result->get('title'); 1126 } 1127 sort($titles); 1128 sort($expected); 1129 $this->assertEquals($expected, $titles); 1130 } 1131 1132 /** 1133 * Tests the get_supported_orders function for contexts where we can only use relevance 1134 * (system, category). 1135 */ 1136 public function test_get_supported_orders_relevance_only() { 1137 global $DB; 1138 1139 // System or category context: relevance only. 1140 $orders = $this->engine->get_supported_orders(\context_system::instance()); 1141 $this->assertCount(1, $orders); 1142 $this->assertArrayHasKey('relevance', $orders); 1143 1144 $categoryid = $DB->get_field_sql('SELECT MIN(id) FROM {course_categories}'); 1145 $orders = $this->engine->get_supported_orders(\context_coursecat::instance($categoryid)); 1146 $this->assertCount(1, $orders); 1147 $this->assertArrayHasKey('relevance', $orders); 1148 } 1149 1150 /** 1151 * Tests the get_supported_orders function for contexts where we support location as well 1152 * (course, activity, block). 1153 */ 1154 public function test_get_supported_orders_relevance_and_location() { 1155 global $DB; 1156 1157 // Test with course context. 1158 $generator = $this->getDataGenerator(); 1159 $course = $generator->create_course(['fullname' => 'Frogs']); 1160 $coursecontext = \context_course::instance($course->id); 1161 1162 $orders = $this->engine->get_supported_orders($coursecontext); 1163 $this->assertCount(2, $orders); 1164 $this->assertArrayHasKey('relevance', $orders); 1165 $this->assertArrayHasKey('location', $orders); 1166 $this->assertStringContainsString('Course: Frogs', $orders['location']); 1167 1168 // Test with activity context. 1169 $page = $generator->create_module('page', ['course' => $course->id, 'name' => 'Toads']); 1170 1171 $orders = $this->engine->get_supported_orders(\context_module::instance($page->cmid)); 1172 $this->assertCount(2, $orders); 1173 $this->assertArrayHasKey('relevance', $orders); 1174 $this->assertArrayHasKey('location', $orders); 1175 $this->assertStringContainsString('Page: Toads', $orders['location']); 1176 1177 // Test with block context. 1178 $instance = (object)['blockname' => 'html', 'parentcontextid' => $coursecontext->id, 1179 'showinsubcontexts' => 0, 'pagetypepattern' => 'course-view-*', 1180 'defaultweight' => 0, 'timecreated' => 1, 'timemodified' => 1, 1181 'configdata' => '']; 1182 $blockid = $DB->insert_record('block_instances', $instance); 1183 $blockcontext = \context_block::instance($blockid); 1184 1185 $orders = $this->engine->get_supported_orders($blockcontext); 1186 $this->assertCount(2, $orders); 1187 $this->assertArrayHasKey('relevance', $orders); 1188 $this->assertArrayHasKey('location', $orders); 1189 $this->assertStringContainsString('Block: HTML', $orders['location']); 1190 } 1191 1192 /** 1193 * Tests ordering by relevance vs location. 1194 */ 1195 public function test_ordering() { 1196 // Create 2 courses and 2 activities. 1197 $generator = $this->getDataGenerator(); 1198 $course1 = $generator->create_course(['fullname' => 'Course 1']); 1199 $course1context = \context_course::instance($course1->id); 1200 $course1page = $generator->create_module('page', ['course' => $course1]); 1201 $course1pagecontext = \context_module::instance($course1page->cmid); 1202 $course2 = $generator->create_course(['fullname' => 'Course 2']); 1203 $course2context = \context_course::instance($course2->id); 1204 $course2page = $generator->create_module('page', ['course' => $course2]); 1205 $course2pagecontext = \context_module::instance($course2page->cmid); 1206 1207 // Create one search record in each activity and course. 1208 $this->create_search_record($course1->id, $course1context->id, 'C1', 'Xyzzy'); 1209 $this->create_search_record($course1->id, $course1pagecontext->id, 'C1P', 'Xyzzy'); 1210 $this->create_search_record($course2->id, $course2context->id, 'C2', 'Xyzzy'); 1211 $this->create_search_record($course2->id, $course2pagecontext->id, 'C2P', 'Xyzzy plugh'); 1212 $this->search->index(); 1213 1214 // Default search works by relevance so the one with both words should be top. 1215 $querydata = new stdClass(); 1216 $querydata->q = 'xyzzy plugh'; 1217 $results = $this->search->search($querydata); 1218 $this->assertCount(4, $results); 1219 $this->assertEquals('C2P', $results[0]->get('title')); 1220 1221 // Same if you explicitly specify relevance. 1222 $querydata->order = 'relevance'; 1223 $results = $this->search->search($querydata); 1224 $this->assertEquals('C2P', $results[0]->get('title')); 1225 1226 // If you specify order by location and you are in C2 or C2P then results are the same. 1227 $querydata->order = 'location'; 1228 $querydata->context = $course2context; 1229 $results = $this->search->search($querydata); 1230 $this->assertEquals('C2P', $results[0]->get('title')); 1231 $querydata->context = $course2pagecontext; 1232 $results = $this->search->search($querydata); 1233 $this->assertEquals('C2P', $results[0]->get('title')); 1234 1235 // But if you are in C1P then you get different results (C1P first). 1236 $querydata->context = $course1pagecontext; 1237 $results = $this->search->search($querydata); 1238 $this->assertEquals('C1P', $results[0]->get('title')); 1239 } 1240 1241 /** 1242 * Tests with bogus content (that can be entered into Moodle) to see if it crashes. 1243 */ 1244 public function test_bogus_content() { 1245 $generator = $this->getDataGenerator(); 1246 $course1 = $generator->create_course(['fullname' => 'Course 1']); 1247 $course1context = \context_course::instance($course1->id); 1248 1249 // It is possible to enter into a Moodle database content containing these characters, 1250 // which are Unicode non-characters / byte order marks. If sent to Solr, these cause 1251 // failures. 1252 $boguscontent = html_entity_decode('') . 'frog'; 1253 $this->create_search_record($course1->id, $course1context->id, 'C1', $boguscontent); 1254 $boguscontent = html_entity_decode('') . 'frog'; 1255 $this->create_search_record($course1->id, $course1context->id, 'C1', $boguscontent); 1256 1257 // Unicode Standard Version 9.0 - Core Specification, section 23.7, lists 66 non-characters 1258 // in total. Here are some of them - these work OK for me but it may depend on platform. 1259 $boguscontent = html_entity_decode('') . 'frog'; 1260 $this->create_search_record($course1->id, $course1context->id, 'C1', $boguscontent); 1261 $boguscontent = html_entity_decode('') . 'frog'; 1262 $this->create_search_record($course1->id, $course1context->id, 'C1', $boguscontent); 1263 $boguscontent = html_entity_decode('') . 'frog'; 1264 $this->create_search_record($course1->id, $course1context->id, 'C1', $boguscontent); 1265 $boguscontent = html_entity_decode('') . 'frog'; 1266 $this->create_search_record($course1->id, $course1context->id, 'C1', $boguscontent); 1267 1268 // Do the indexing (this will check it doesn't throw warnings). 1269 $this->search->index(); 1270 1271 // Confirm that all 6 documents are found in search. 1272 $querydata = new stdClass(); 1273 $querydata->q = 'frog'; 1274 $results = $this->search->search($querydata); 1275 $this->assertCount(6, $results); 1276 } 1277 1278 /** 1279 * Adds a record to the mock search area, so that the search engine can find it later. 1280 * 1281 * @param int $courseid Course id 1282 * @param int $contextid Context id 1283 * @param string $title Title for search index 1284 * @param string $content Content for search index 1285 */ 1286 protected function create_search_record($courseid, $contextid, $title, $content) { 1287 $record = new \stdClass(); 1288 $record->content = $content; 1289 $record->title = $title; 1290 $record->courseid = $courseid; 1291 $record->contextid = $contextid; 1292 $this->generator->create_record($record); 1293 } 1294 1295 /** 1296 * Tries out deleting data for a context or a course. 1297 * 1298 * @throws coding_exception 1299 * @throws moodle_exception 1300 */ 1301 public function test_deleted_contexts_and_courses() { 1302 // Create some courses and activities. 1303 $generator = $this->getDataGenerator(); 1304 $course1 = $generator->create_course(['fullname' => 'Course 1']); 1305 $course1context = \context_course::instance($course1->id); 1306 $course1page1 = $generator->create_module('page', ['course' => $course1]); 1307 $course1page1context = \context_module::instance($course1page1->cmid); 1308 $course1page2 = $generator->create_module('page', ['course' => $course1]); 1309 $course1page2context = \context_module::instance($course1page2->cmid); 1310 $course2 = $generator->create_course(['fullname' => 'Course 2']); 1311 $course2context = \context_course::instance($course2->id); 1312 $course2page = $generator->create_module('page', ['course' => $course2]); 1313 $course2pagecontext = \context_module::instance($course2page->cmid); 1314 1315 // Create one search record in each activity and course. 1316 $this->create_search_record($course1->id, $course1context->id, 'C1', 'Xyzzy'); 1317 $this->create_search_record($course1->id, $course1page1context->id, 'C1P1', 'Xyzzy'); 1318 $this->create_search_record($course1->id, $course1page2context->id, 'C1P2', 'Xyzzy'); 1319 $this->create_search_record($course2->id, $course2context->id, 'C2', 'Xyzzy'); 1320 $this->create_search_record($course2->id, $course2pagecontext->id, 'C2P', 'Xyzzy plugh'); 1321 $this->search->index(); 1322 1323 // By default we have all results. 1324 $this->assert_raw_solr_query_result('content:xyzzy', ['C1', 'C1P1', 'C1P2', 'C2', 'C2P']); 1325 1326 // Say we delete the course2pagecontext... 1327 $this->engine->delete_index_for_context($course2pagecontext->id); 1328 $this->assert_raw_solr_query_result('content:xyzzy', ['C1', 'C1P1', 'C1P2', 'C2']); 1329 1330 // Now delete the second course... 1331 $this->engine->delete_index_for_course($course2->id); 1332 $this->assert_raw_solr_query_result('content:xyzzy', ['C1', 'C1P1', 'C1P2']); 1333 1334 // Finally let's delete using Moodle functions to check that works. Single context first. 1335 course_delete_module($course1page1->cmid); 1336 $this->assert_raw_solr_query_result('content:xyzzy', ['C1', 'C1P2']); 1337 delete_course($course1, false); 1338 $this->assert_raw_solr_query_result('content:xyzzy', []); 1339 } 1340 1341 /** 1342 * Specific test of the add_document_batch function (also used in many other tests). 1343 */ 1344 public function test_add_document_batch() { 1345 // Get a default document. 1346 $area = new core_mocksearch\search\mock_search_area(); 1347 $record = $this->generator->create_record(); 1348 $doc = $area->get_document($record); 1349 $originalid = $doc->get('id'); 1350 1351 // Now create 5 similar documents. 1352 $docs = []; 1353 for ($i = 1; $i <= 5; $i++) { 1354 $doc = $area->get_document($record); 1355 $doc->set('id', $originalid . '-' . $i); 1356 $doc->set('title', 'Batch ' . $i); 1357 $docs[$i] = $doc; 1358 } 1359 1360 // Document 3 has a file attached. 1361 $fs = get_file_storage(); 1362 $filerecord = new \stdClass(); 1363 $filerecord->content = 'Some FileContents'; 1364 $file = $this->generator->create_file($filerecord); 1365 $docs[3]->add_stored_file($file); 1366 1367 // Add all these documents to the search engine. 1368 $this->assertEquals([5, 0, 1], $this->engine->add_document_batch($docs, true)); 1369 $this->engine->area_index_complete($area->get_area_id()); 1370 1371 // Check all documents were indexed. 1372 $querydata = new stdClass(); 1373 $querydata->q = 'Batch'; 1374 $results = $this->search->search($querydata); 1375 $this->assertCount(5, $results); 1376 1377 // Check it also finds based on the file. 1378 $querydata->q = 'FileContents'; 1379 $results = $this->search->search($querydata); 1380 $this->assertCount(1, $results); 1381 } 1382 1383 /** 1384 * Tests the batching logic, specifically the limit to 100 documents per 1385 * batch, and not batching very large documents. 1386 */ 1387 public function test_batching() { 1388 $area = new core_mocksearch\search\mock_search_area(); 1389 $record = $this->generator->create_record(); 1390 $doc = $area->get_document($record); 1391 $originalid = $doc->get('id'); 1392 1393 // Up to 100 documents in 1 batch. 1394 $docs = []; 1395 for ($i = 1; $i <= 100; $i++) { 1396 $doc = $area->get_document($record); 1397 $doc->set('id', $originalid . '-' . $i); 1398 $docs[$i] = $doc; 1399 } 1400 [, , , , , $batches] = $this->engine->add_documents( 1401 new ArrayIterator($docs), $area, ['indexfiles' => true]); 1402 $this->assertEquals(1, $batches); 1403 1404 // More than 100 needs 2 batches. 1405 $docs = []; 1406 for ($i = 1; $i <= 101; $i++) { 1407 $doc = $area->get_document($record); 1408 $doc->set('id', $originalid . '-' . $i); 1409 $docs[$i] = $doc; 1410 } 1411 [, , , , , $batches] = $this->engine->add_documents( 1412 new ArrayIterator($docs), $area, ['indexfiles' => true]); 1413 $this->assertEquals(2, $batches); 1414 1415 // Small number but with some large documents that aren't batched. 1416 $docs = []; 1417 for ($i = 1; $i <= 10; $i++) { 1418 $doc = $area->get_document($record); 1419 $doc->set('id', $originalid . '-' . $i); 1420 $docs[$i] = $doc; 1421 } 1422 // This one is just small enough to fit. 1423 $docs[3]->set('content', str_pad('xyzzy ', 1024 * 1024, 'x')); 1424 // These two don't fit. 1425 $docs[5]->set('content', str_pad('xyzzy ', 1024 * 1024 + 1, 'x')); 1426 $docs[6]->set('content', str_pad('xyzzy ', 1024 * 1024 + 1, 'x')); 1427 [, , , , , $batches] = $this->engine->add_documents( 1428 new ArrayIterator($docs), $area, ['indexfiles' => true]); 1429 $this->assertEquals(3, $batches); 1430 1431 // Check that all 3 of the large documents (added as batch or not) show up in results. 1432 $this->engine->area_index_complete($area->get_area_id()); 1433 $querydata = new stdClass(); 1434 $querydata->q = 'xyzzy'; 1435 $results = $this->search->search($querydata); 1436 $this->assertCount(3, $results); 1437 } 1438 1439 /** 1440 * Tests with large documents. The point of this test is that we stop batching 1441 * documents if they are bigger than 1MB, and the maximum batch count is 100, 1442 * so the maximum size batch will be about 100 1MB documents. 1443 */ 1444 public function test_add_document_batch_large() { 1445 // This test is a bit slow and not that important to run every time... 1446 if (!PHPUNIT_LONGTEST) { 1447 $this->markTestSkipped('PHPUNIT_LONGTEST is not defined'); 1448 } 1449 1450 // Get a default document. 1451 $area = new core_mocksearch\search\mock_search_area(); 1452 $record = $this->generator->create_record(); 1453 $doc = $area->get_document($record); 1454 $originalid = $doc->get('id'); 1455 1456 // Now create 100 large documents. 1457 $size = 1024 * 1024; 1458 $docs = []; 1459 for ($i = 1; $i <= 100; $i++) { 1460 $doc = $area->get_document($record); 1461 $doc->set('id', $originalid . '-' . $i); 1462 $doc->set('title', 'Batch ' . $i); 1463 $doc->set('content', str_pad('', $size, 'Long text ' . $i . '. ', STR_PAD_RIGHT) . ' xyzzy'); 1464 $docs[$i] = $doc; 1465 } 1466 1467 // Add all these documents to the search engine. 1468 $this->engine->add_document_batch($docs, true); 1469 $this->engine->area_index_complete($area->get_area_id()); 1470 1471 // Check all documents were indexed, searching for text at end. 1472 $querydata = new stdClass(); 1473 $querydata->q = 'xyzzy'; 1474 $results = $this->search->search($querydata); 1475 $this->assertCount(100, $results); 1476 1477 // Search for specific text that's only in one. 1478 $querydata->q = '42'; 1479 $results = $this->search->search($querydata); 1480 $this->assertCount(1, $results); 1481 } 1482 1483 /** 1484 * Carries out a raw Solr query using the Solr basic query syntax. 1485 * 1486 * This is used to test data contained in the index without going through Moodle processing. 1487 * 1488 * @param string $q Search query 1489 * @param string[] $expected Expected titles of results, in alphabetical order 1490 */ 1491 protected function assert_raw_solr_query_result(string $q, array $expected) { 1492 $solr = $this->engine->get_search_client_public(); 1493 $query = new SolrQuery($q); 1494 $results = $solr->query($query)->getResponse()->response->docs; 1495 if ($results) { 1496 $titles = array_map(function($x) { 1497 return $x->title; 1498 }, $results); 1499 sort($titles); 1500 } else { 1501 $titles = []; 1502 } 1503 $this->assertEquals($expected, $titles); 1504 } 1505 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body