diff --git a/lang/en/search.php b/lang/en/search.php
index 49d741a27a1..84bab681e57 100644
--- a/lang/en/search.php
+++ b/lang/en/search.php
@@ -67,6 +67,7 @@ $string['ittook'] = 'It took';
$string['next'] = 'Next';
$string['noindexmessage'] = 'Admin: There appears to be no search index. Please';
$string['noresults'] = 'No results';
+$string['notitle'] = 'No title';
$string['normalsearch'] = 'Normal search';
$string['openedon'] = 'opened on';
$string['optimize'] = 'Optimize';
diff --git a/search/classes/document.php b/search/classes/document.php
index 9d47ab9f8b2..89f0d833901 100644
--- a/search/classes/document.php
+++ b/search/classes/document.php
@@ -91,12 +91,12 @@ class document implements \renderable, \templatable {
'indexed' => true
),
'title' => array(
- 'type' => 'string',
+ 'type' => 'text',
'stored' => true,
'indexed' => true
),
'content' => array(
- 'type' => 'string',
+ 'type' => 'text',
'stored' => true,
'indexed' => true
),
@@ -148,12 +148,12 @@ class document implements \renderable, \templatable {
'indexed' => false
),
'description1' => array(
- 'type' => 'string',
+ 'type' => 'text',
'stored' => true,
'indexed' => true
),
'description2' => array(
- 'type' => 'string',
+ 'type' => 'text',
'stored' => true,
'indexed' => true
),
@@ -305,6 +305,19 @@ class document implements \renderable, \templatable {
return $string;
}
+ /**
+ * Formats a text value for the search engine.
+ *
+ * Search engines may overwrite this method to apply restrictions, like limiting the size.
+ * The default behaviour is just returning the string.
+ *
+ * @param string $text
+ * @return string
+ */
+ public static function format_text_for_engine($text) {
+ return $text;
+ }
+
/**
* Returns a timestamp from the value stored in the search engine.
*
@@ -411,9 +424,13 @@ class document implements \renderable, \templatable {
// Overwrite the timestamp with the engine dependant format.
$data[$fieldname] = static::format_time_for_engine($data[$fieldname]);
} else if ($field['type'] === 'string') {
- // Overwrite the timestamp with the engine dependant format.
+ // Overwrite the string with the engine dependant format.
$data[$fieldname] = static::format_string_for_engine($data[$fieldname]);
+ } else if ($field['type'] === 'text') {
+ // Overwrite the text with the engine dependant format.
+ $data[$fieldname] = static::format_text_for_engine($data[$fieldname]);
}
+
}
foreach (static::$optionalfields as $fieldname => $field) {
@@ -424,8 +441,11 @@ class document implements \renderable, \templatable {
// Overwrite the timestamp with the engine dependant format.
$data[$fieldname] = static::format_time_for_engine($data[$fieldname]);
} else if ($field['type'] === 'string') {
- // Overwrite the timestamp with the engine dependant format.
+ // Overwrite the string with the engine dependant format.
$data[$fieldname] = static::format_string_for_engine($data[$fieldname]);
+ } else if ($field['type'] === 'text') {
+ // Overwrite the text with the engine dependant format.
+ $data[$fieldname] = static::format_text_for_engine($data[$fieldname]);
}
}
@@ -444,14 +464,14 @@ class document implements \renderable, \templatable {
* @return array
*/
public function export_for_template(\renderer_base $output) {
-
list($componentname, $areaname) = \core_search\manager::extract_areaid_parts($this->get('areaid'));
+ $title = $this->is_set('title') ? $this->format_text($this->get('title')) : '';
$data = [
'courseurl' => new \moodle_url('/course/view.php?id=' . $this->get('courseid')),
'coursefullname' => format_string($this->get('coursefullname'), true, array('context' => $this->get('contextid'))),
'modified' => userdate($this->get('modified')),
- 'title' => format_string($this->get('title'), true, array('context' => $this->get('contextid'))),
+ 'title' => ($title !== '') ? $title : get_string('notitle', 'search'),
'docurl' => $this->get_doc_url(),
'content' => $this->is_set('content') ? $this->format_text($this->get('content')) : null,
'contexturl' => $this->get_context_url(),
diff --git a/search/engine/solr/classes/document.php b/search/engine/solr/classes/document.php
index 20c1f3ecf9a..43137ff8c40 100644
--- a/search/engine/solr/classes/document.php
+++ b/search/engine/solr/classes/document.php
@@ -72,6 +72,41 @@ class document extends \core_search\document {
* @return int
*/
protected function get_text_format() {
- return FORMAT_MARKDOWN;
+ return FORMAT_HTML;
+ }
+
+ /**
+ * Formats a text string coming from the search engine.
+ *
+ * @param string $text Text to format
+ * @return string HTML text to be renderer
+ */
+ protected function format_text($text) {
+ // Since we allow output for highlighting, we need to encode html entities.
+ // This ensures plaintext html chars don't become valid html.
+ $out = s($text);
+
+ $startcount = 0;
+ $endcount = 0;
+
+ // Remove end/start pairs that span a few common seperation characters. Allows us to highlight phrases instead of words.
+ $regex = '|'.engine::HIGHLIGHT_END.'([ .,-]{0,3})'.engine::HIGHLIGHT_START.'|';
+ $out = preg_replace($regex, '$1', $out);
+
+ // Now replace our start and end highlight markers.
+ $out = str_replace(engine::HIGHLIGHT_START, '', $out, $startcount);
+ $out = str_replace(engine::HIGHLIGHT_END, '', $out, $endcount);
+
+ // This makes sure any highlight tags are balanced, incase truncation or the highlight text contained our markers.
+ while ($startcount > $endcount) {
+ $out .= '';
+ $endcount++;
+ }
+ while ($startcount < $endcount) {
+ $out = '' . $out;
+ $endcount++;
+ }
+
+ return parent::format_text($out);
}
}
diff --git a/search/engine/solr/classes/engine.php b/search/engine/solr/classes/engine.php
index f67c86ac388..3a07f6bef59 100644
--- a/search/engine/solr/classes/engine.php
+++ b/search/engine/solr/classes/engine.php
@@ -46,9 +46,19 @@ class engine extends \core_search\engine {
const AUTOCOMMIT_WITHIN = 15000;
/**
- * @var int Highlighting fragsize.
+ * Highlighting fragsize. Slightly larger than output size (500) to allow for ... appending.
*/
- const FRAG_SIZE = 500;
+ const FRAG_SIZE = 510;
+
+ /**
+ * Marker for the start of a highlight.
+ */
+ const HIGHLIGHT_START = '@@HI_S@@';
+
+ /**
+ * Marker for the end of a highlight.
+ */
+ const HIGHLIGHT_END = '@@HI_E@@';
/**
* @var \SolrClient
@@ -58,7 +68,7 @@ class engine extends \core_search\engine {
/**
* @var array Fields that can be highlighted.
*/
- protected $highlightfields = array('content', 'description1', 'description2');
+ protected $highlightfields = array('title', 'content', 'description1', 'description2');
/**
* Prepares a Solr query, applies filters and executes it returning its results.
@@ -161,8 +171,9 @@ class engine extends \core_search\engine {
$query->addHighlightField($field);
}
$query->setHighlightFragsize(static::FRAG_SIZE);
- $query->setHighlightSimplePre('__');
- $query->setHighlightSimplePost('__');
+ $query->setHighlightSimplePre(self::HIGHLIGHT_START);
+ $query->setHighlightSimplePost(self::HIGHLIGHT_END);
+ $query->setHighlightMergeContiguous(true);
$query->setQuery($q);
diff --git a/search/engine/solr/classes/schema.php b/search/engine/solr/classes/schema.php
index 5adff6df3f4..b023970fa79 100644
--- a/search/engine/solr/classes/schema.php
+++ b/search/engine/solr/classes/schema.php
@@ -177,7 +177,7 @@ class schema {
$params = array(
'add-field' => array(
'name' => $fieldname,
- 'type' => $data['type'],
+ 'type' => ($data['type'] === 'text' ? 'text_general' : $data['type']),
'stored' => $data['stored'],
'multiValued' => false,
'indexed' => $data['indexed']
@@ -242,7 +242,8 @@ class schema {
throw new \moodle_exception('errorcreatingschema', 'search_solr', '',
get_string('schemafieldautocreated', 'search_solr', $fieldname));
- } else if ($results->field->type !== $data['type'] ||
+ } else if (($results->field->type !== $data['type'] &&
+ ($data['type'] !== 'text' || $results->field->type !== 'text_general')) ||
$results->field->multiValued !== false ||
$results->field->indexed !== $data['indexed'] ||
$results->field->stored !== $data['stored']) {
diff --git a/search/engine/solr/tests/engine_test.php b/search/engine/solr/tests/engine_test.php
index e99b8293c43..5e91f82a9f1 100644
--- a/search/engine/solr/tests/engine_test.php
+++ b/search/engine/solr/tests/engine_test.php
@@ -252,4 +252,27 @@ class search_solr_engine_testcase extends advanced_testcase {
$this->assertEquals(0, $results[0]->get('owneruserid'));
$this->assertEquals($originalid, $results[0]->get('id'));
}
+
+ public function test_highlight() {
+ global $PAGE;
+
+ $this->search->index();
+
+ $querydata = new stdClass();
+ $querydata->q = 'message';
+
+ $results = $this->search->search($querydata);
+ $this->assertCount(2, $results);
+
+ $result = reset($results);
+
+ $regex = '|'.\search_solr\engine::HIGHLIGHT_START.'message'.\search_solr\engine::HIGHLIGHT_END.'|';
+ $this->assertRegExp($regex, $result->get('content'));
+
+ $searchrenderer = $PAGE->get_renderer('core_search');
+ $exported = $result->export_for_template($searchrenderer);
+
+ $regex = '|message|';
+ $this->assertRegExp($regex, $exported['content']);
+ }
}
diff --git a/search/templates/result.mustache b/search/templates/result.mustache
index 6d39ae8d97e..8840cfd1a1c 100644
--- a/search/templates/result.mustache
+++ b/search/templates/result.mustache
@@ -53,7 +53,7 @@
}}
{{#content}}
{{{content}}}