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