$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';
'indexed' => true
),
'title' => array(
- 'type' => 'string',
+ 'type' => 'text',
'stored' => true,
'indexed' => true
),
'content' => array(
- 'type' => 'string',
+ 'type' => 'text',
'stored' => true,
'indexed' => true
),
'indexed' => false
),
'description1' => array(
- 'type' => 'string',
+ 'type' => 'text',
'stored' => true,
'indexed' => true
),
'description2' => array(
- 'type' => 'string',
+ 'type' => 'text',
'stored' => true,
'indexed' => true
),
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.
*
// 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) {
// 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]);
}
}
* @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(),
* @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, '<span class="highlight">', $out, $startcount);
+ $out = str_replace(engine::HIGHLIGHT_END, '</span>', $out, $endcount);
+
+ // This makes sure any highlight tags are balanced, incase truncation or the highlight text contained our markers.
+ while ($startcount > $endcount) {
+ $out .= '</span>';
+ $endcount++;
+ }
+ while ($startcount < $endcount) {
+ $out = '<span class="highlight">' . $out;
+ $endcount++;
+ }
+
+ return parent::format_text($out);
}
}
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
/**
* @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.
$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);
$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']
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']) {
$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 = '|<span class="highlight">message</span>|';
+ $this->assertRegExp($regex, $exported['content']);
+ }
}
}}
<div class="result">
<h4 class="result-title">
- <a href="{{{docurl}}}">{{title}}</a>
+ <a href="{{{docurl}}}">{{{title}}}</a>
</h4>
{{#content}}
<div class="result-content">{{{content}}}</div>