updated search_api, search_api_solr_override, imce

This commit is contained in:
2019-05-14 11:01:58 +02:00
parent dc39ddbbea
commit 2e69e3fd4c
41 changed files with 1383 additions and 140 deletions

View File

@@ -20,11 +20,23 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback {
*/
protected $reductionType;
/**
* A separator to use when the aggregation type is 'fulltext'.
*
* Used to temporarily store a string separator when the aggregation type is
* "fulltext", for use in SearchApiAlterAddAggregation::reduce() with
* array_reduce().
*
* @var string
*/
protected $fulltextReductionSeparator;
public function configurationForm() {
$form['#attached']['css'][] = drupal_get_path('module', 'search_api') . '/search_api.admin.css';
$fields = $this->index->getFields(FALSE);
$field_options = array();
$field_properties = array();
foreach ($fields as $name => $field) {
$field_options[$name] = check_plain($field['name']);
$field_properties[$name] = array(
@@ -79,9 +91,23 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback {
'#required' => TRUE,
);
$form['fields'][$name]['type_descriptions'] = $type_descriptions;
$type_selector = ':input[name="callbacks[search_api_alter_add_aggregation][settings][fields][' . $name . '][type]"]';
foreach (array_keys($types) as $type) {
$form['fields'][$name]['type_descriptions'][$type]['#states']['visible'][':input[name="callbacks[search_api_alter_add_aggregation][settings][fields][' . $name . '][type]"]']['value'] = $type;
$form['fields'][$name]['type_descriptions'][$type]['#states']['visible'][$type_selector]['value'] = $type;
}
$form['fields'][$name]['separator'] = array(
'#type' => 'textfield',
'#title' => t('Fulltext separator'),
'#description' => t('For aggregation type "Fulltext", set the text that should be used to separate the aggregated field values. Use "\t" for tabs and "\n" for newline characters.'),
'#default_value' => addcslashes(isset($field['separator']) ? $field['separator'] : "\n\n", "\0..\37\\"),
'#states' => array(
'visible' => array(
$type_selector => array(
'value' => 'fulltext',
),
),
),
);
$form['fields'][$name]['fields'] = array_merge($field_properties, array(
'#type' => 'checkboxes',
'#title' => t('Contained fields'),
@@ -125,11 +151,12 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback {
return;
}
foreach ($values['fields'] as $name => $field) {
$fields = $values['fields'][$name]['fields'] = array_values(array_filter($field['fields']));
unset($values['fields'][$name]['actions']);
$fields = $values['fields'][$name]['fields'] = array_values(array_filter($field['fields']));
if ($field['name'] && !$fields) {
form_error($form['fields'][$name]['fields'], t('You have to select at least one field to aggregate. If you want to remove an aggregated field, please delete its name.'));
}
$values['fields'][$name]['separator'] = stripcslashes($field['separator']);
}
}
@@ -176,6 +203,7 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback {
$values = $this->flattenArray($values);
$this->reductionType = $field['type'];
$this->fulltextReductionSeparator = isset($field['separator']) ? $field['separator'] : "\n\n";
$item->$name = array_reduce($values, array($this, 'reduce'), NULL);
if ($field['type'] == 'count' && !$item->$name) {
$item->$name = 0;
@@ -192,7 +220,7 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback {
public function reduce($a, $b) {
switch ($this->reductionType) {
case 'fulltext':
return isset($a) ? $a . "\n\n" . $b : $b;
return isset($a) ? $a . $this->fulltextReductionSeparator . $b : $b;
case 'sum':
return $a + $b;
case 'count':
@@ -300,10 +328,10 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback {
'count' => 'integer',
'max' => 'integer',
'min' => 'integer',
'first' => 'string',
'first_char' => 'string',
'last' => 'string',
'list' => 'list<string>',
'first' => 'token',
'first_char' => 'token',
'last' => 'token',
'list' => 'list<token>',
);
case 'description':
return array(

View File

@@ -108,7 +108,7 @@ class SearchApiAlterAddHierarchy extends SearchApiAbstractAlterCallback {
$this->extractHierarchy($child, $prop, $values[$key]);
}
foreach ($values as $key => $value) {
$item->$key = $value;
$item->$key = array_values($value);
}
}
}

View File

@@ -0,0 +1,57 @@
<?php
/**
* @file
* Contains SearchApiAlterAddUserContent.
*/
/**
* Adds the nodes created by the indexed user for indexing.
*/
class SearchApiAlterAddUserContent extends SearchApiAbstractAlterCallback {
/**
* {@inheritdoc}
*/
public function supportsIndex(SearchApiIndex $index) {
return $index->getEntityType() === 'user';
}
/**
* {@inheritdoc}
*/
public function propertyInfo() {
return array(
'search_api_user_content' => array(
'label' => t('User content'),
'description' => t('The nodes created by this user'),
'type' => 'list<node>',
),
);
}
/**
* {@inheritdoc}
*/
public function alterItems(array &$items) {
$uids = array();
foreach ($items as $item) {
$uids[] = $item->uid;
}
$sql = 'SELECT nid, uid FROM {node} WHERE uid IN (:uids)';
$nids = db_query($sql, array(':uids' => $uids));
$user_nodes = array();
foreach ($nids as $row) {
$user_nodes[$row->uid][] = $row->nid;
}
foreach ($items as $item) {
$item->search_api_user_content = array();
if (!empty($user_nodes[$item->uid])) {
$item->search_api_user_content = $user_nodes[$item->uid];
}
}
}
}

View File

@@ -315,99 +315,141 @@ class SearchApiHighlight extends SearchApiAbstractProcessor {
* @param array $keys
* Search keywords entered by the user.
*
* @return string
* A string containing HTML for the excerpt.
* @return string|null
* A string containing HTML for the excerpt, or NULL if none could be
* created.
*/
protected function createExcerpt($text, array $keys) {
// Prepare text by stripping HTML tags and decoding HTML entities.
$text = strip_tags(str_replace(array('<', '>'), array(' <', '> '), $text));
$text = ' ' . decode_entities($text);
$text = decode_entities($text);
$text = preg_replace('/\s+/', ' ', $text);
$text = trim($text, ' ');
$text_length = strlen($text);
// Extract fragments around keywords.
// First we collect ranges of text around each keyword, starting/ending
// at spaces, trying to get to the requested length.
// If the sum of all fragments is too short, we look for second occurrences.
// Try to reach the requested excerpt length with about two fragments (each
// with a keyword and some context).
$ranges = array();
$included = array();
$length = 0;
$work_keys = $keys;
while ($length < $this->options['excerpt_length'] && $work_keys) {
foreach ($work_keys as $k => $key) {
if ($length >= $this->options['excerpt_length']) {
$look_start = array();
$remaining_keys = $keys;
// Get the set excerpt length from the configuration. If the length is too
// small, only use one fragment.
$excerpt_length = $this->options['excerpt_length'];
$context_length = round($excerpt_length / 4) - 3;
if ($context_length < 32) {
$context_length = round($excerpt_length / 2) - 1;
}
while ($length < $excerpt_length && !empty($remaining_keys)) {
$found_keys = array();
foreach ($remaining_keys as $key) {
if ($length >= $excerpt_length) {
break;
}
// Remember occurrence of key so we can skip over it if more occurrences
// are desired.
if (!isset($included[$key])) {
$included[$key] = 0;
// Remember where we last found $key, in case we are coming through a
// second time.
if (!isset($look_start[$key])) {
$look_start[$key] = 0;
}
// Locate a keyword (position $p, always >0 because $text starts with a
// space).
$p = 0;
if (empty($this->options['highlight_partial'])) {
$regex = '/' . self::$boundary . preg_quote($key, '/') . self::$boundary . '/iu';
if (preg_match($regex, $text, $match, PREG_OFFSET_CAPTURE, $included[$key])) {
$p = $match[0][1];
// See if we can find $key after where we found it the last time. Since
// we are requiring a match on a word boundary, make sure $text starts
// and ends with a space.
$matches = array();
if (!$this->options['highlight_partial']) {
$found_position = FALSE;
$regex = '/' . static::$boundary . preg_quote($key, '/') . static::$boundary . '/iu';
if (preg_match($regex, ' ' . $text . ' ', $matches, PREG_OFFSET_CAPTURE, $look_start[$key])) {
$found_position = $matches[0][1];
}
}
else {
$p = stripos($text, $key, $included[$key]);
$found_position = stripos($text, $key, $look_start[$key]);
}
// Now locate a space in front (position $q) and behind it (position $s),
// leaving about 60 characters extra before and after for context.
// Note that a space was added to the front and end of $text above.
if ($p) {
if (($q = strpos(' ' . $text, ' ', max(0, $p - 61))) !== FALSE) {
$end = substr($text . ' ', $p, 80);
if (($s = strrpos($end, ' ')) !== FALSE) {
// Account for the added spaces.
$q = max($q - 1, 0);
$s = min($s, strlen($end) - 1);
$ranges[$q] = $p + $s;
$length += $p + $s - $q;
$included[$key] = $p + 1;
continue;
if ($found_position !== FALSE) {
$look_start[$key] = $found_position + 1;
// Keep track of which keys we found this time, in case we need to
// pass through again to find more text.
$found_keys[] = $key;
// Locate a space before and after this match, leaving some context on
// each end.
if ($found_position > $context_length) {
$before = strpos($text, ' ', $found_position - $context_length);
if ($before !== FALSE) {
++$before;
}
}
else {
$before = 0;
}
if ($before !== FALSE && $before <= $found_position) {
if ($text_length > $found_position + $context_length) {
$after = strrpos(substr($text, 0, $found_position + $context_length), ' ', $found_position);
}
else {
$after = $text_length;
}
if ($after !== FALSE && $after > $found_position) {
if ($before < $after) {
// Save this range.
$ranges[$before] = $after;
$length += $after - $before;
}
}
}
}
// Unless we got a match above, we don't need to look for this key any
// more.
unset($work_keys[$k]);
}
// Next time through this loop, only look for keys we found this time,
// if any.
$remaining_keys = $found_keys;
}
if (count($ranges) == 0) {
// We didn't find any keyword matches, so just return NULL.
if (!$ranges) {
// We didn't find any keyword matches, return NULL.
return NULL;
}
// Sort the text ranges by starting position.
ksort($ranges);
// Now we collapse overlapping text ranges into one. The sorting makes it O(n).
// Collapse overlapping text ranges into one. The sorting makes it O(n).
$newranges = array();
$from1 = $to1 = NULL;
foreach ($ranges as $from2 => $to2) {
if (!isset($from1)) {
if ($from1 === NULL) {
// This is the first time through this loop: initialize.
$from1 = $from2;
$to1 = $to2;
continue;
}
if ($from2 <= $to1) {
// The ranges overlap: combine them.
$to1 = max($to1, $to2);
}
else {
// The ranges do not overlap: save the working range and start a new
// one.
$newranges[$from1] = $to1;
$from1 = $from2;
$to1 = $to2;
}
}
// Save the remaining working range.
$newranges[$from1] = $to1;
// Fetch text
// Fetch text within the combined ranges we found.
$out = array();
foreach ($newranges as $from => $to) {
$out[] = substr($text, $from, $to - $from);
}
if (!$out) {
return NULL;
}
// Let translators have the ... separator text as one chunk.
$dots = explode('!excerpt', t('... !excerpt ... !excerpt ...'));

View File

@@ -24,12 +24,11 @@ class SearchApiPorterStemmer extends SearchApiAbstractProcessor {
$form = parent::configurationForm();
$args = array(
'!algorithm' => url('https://github.com/markfullmer/porter2'),
'!exclusions' => url('https://github.com/markfullmer/porter2#user-content-custom-exclusions'),
'@algorithm' => url('http://snowball.tartarus.org/algorithms/english/stemmer.html'),
);
$form += array(
'help' => array(
'#markup' => '<p>' . t('Optionally, provide an exclusion list to override the stemmer algorithm. Read about the <a href="@algorithm">algorithm</a> and <a href="@exclusions">exclusions</a>.', $args) . '</p>',
'#markup' => '<p>' . t('Optionally, provide an exclusion list to override the stemmer algorithm. (<a href="@algorithm">Read about the algorithm</a>.)', $args) . '</p>',
),
'exceptions' => array(
'#type' => 'textarea',
@@ -66,7 +65,7 @@ class SearchApiPorterStemmer extends SearchApiAbstractProcessor {
$stemmed[] = $word;
}
}
$value = implode('', $stemmed);
$value = implode(' ', $stemmed);
}
/**