updated search_api modules as indexation was not working localy

had to be tested in production
This commit is contained in:
Bachir Soussi Chiadmi
2017-05-24 18:57:04 +02:00
parent b5d76ec5e6
commit 28c7a0965a
53 changed files with 4166 additions and 187 deletions

View File

@@ -1,3 +1,41 @@
Search API 1.21 (2017-02-23):
-----------------------------
- #2780341 by Berdir: Fixed passing of custom ranges to date facets.
- #2765317 by JorgenSandstrom, NWOM, drunken monkey: Added a "Last" aggregation
type.
- #2842856 by drunken monkey: Fixed language filters for "Multiple types"
indexes.
- #2844990 by drunken monkey: Made the "Role filter" data alteration available
for multi-type indexes.
- #2837745 by drunken monkey, klausi: Fixed NULL tags on old serialized queries.
- #2833482 by drunken monkey: Fixed undefined constant when uninstalling facets
module.
- #2840261 by alan-ps: Fixed usage of outdated hash functions.
- #1670420 by kyletaylored, dorficus, drunken monkey: Fixed potential fatal
error in facet adapter's getSearchKeys() method.
- #2838075 by dsnopek: Fixed possible race condition in
hook_system_info_alter().
- #2836687 by sarthak drupal: Fixed one doc comment typo.
- #2632880 by drunken monkey, donquixote: Added possibility to change indexed
bundles on disabled indexes.
- #2828380 by jansete: Fixed taxonomy term access tag in Views filter.
- #2827717 by Fabien.Godineau, drunken monkey: Fixed disabling of search views
when reverting an index.
- #2822836 by prince_zyxware: Fixed some Drupal coding standards violations.
- #2822145 by drunken monkey: Fixed problem with phrase search in Views
fulltext filter.
- #2778261 by drunken monkey, BAHbKA: Fixed "Index items immediately"
functionality for unindexed items.
- #2358065 by Jelle_S, graper, drunken monkey: Added the option for
highlighting of partial matches to the processor.
- #2779159 by mark_fullmer, drunken monkey: Added a Stemmer processor.
- #2649412 by relaxnow, GoZ: Added support for minimum granularity to date
facets.
- #2769021 by Plazik, drunken monkey: Added the generated Search API query to
the Views preview.
- #2769877 by mfernea: Fixed database exception when filtering for anonymous
user.
Search API 1.20 (2016-07-21):
-----------------------------
- #2731103 by drunken monkey: Fixed the default value for the taxonomy term

View File

@@ -385,6 +385,10 @@ Included components
Enables the admin to specify a stopwords file, the words contained in which
will be filtered out of the text data indexed. This can be used to exclude
too common words from indexing, for servers not supporting this natively.
* Stem words
Uses the PorterStemmer method to reduce words to stems. A search for
"garden" will return results for "gardening" and "garden," as will a search
for "gardening."
- Additional modules

View File

@@ -192,6 +192,12 @@ class SearchApiFacetapiAdapter extends FacetapiAdapter {
*/
public function getSearchKeys() {
$search = $this->getCurrentSearch();
// If the search is empty then there's no reason to continue.
if (!$search) {
return NULL;
}
$keys = $search[0]->getOriginalKeys();
if (is_array($keys)) {
// This will happen nearly never when displaying the search keys to the
@@ -281,10 +287,24 @@ class SearchApiFacetapiAdapter extends FacetapiAdapter {
// Date facets don't support the "OR" operator (for now).
$form['global']['operator']['#access'] = FALSE;
$default_value = FACETAPI_DATE_YEAR;
if (isset($options['date_granularity_min'])) {
$default_value = $options['date_granularity_min'];
}
$form['global']['date_granularity_min'] = array(
'#type' => 'select',
'#title' => t('Minimum granularity'),
'#description' => t('Determine the minimum drill-down level to start at'),
'#prefix' => '<div class="facetapi-global-setting">',
'#suffix' => '</div>',
'#options' => $granularity_options,
'#default_value' => $default_value,
);
}
// Add an "Exclude" option for terms.
if(!empty($facet['query types']) && in_array('term', $facet['query types'])) {
if (!empty($facet['query types']) && in_array('term', $facet['query types'])) {
$form['global']['operator']['#weight'] = -2;
unset($form['global']['operator']['#suffix']);
$form['global']['exclude'] = array(

View File

@@ -76,7 +76,7 @@ class SearchApiFacetapiDate extends SearchApiFacetapiTerm implements FacetapiQue
*/
protected function createRangeFilter($value) {
// Ignore any filters passed directly from the server (range or missing).
if (!$value || $value == '!' || (!ctype_digit($value[0]) && preg_match('/^[\[(][^ ]+ [^ ]+[])]$/', $value))) {
if (!$value || $value == '!' || (!ctype_digit($value[0]) && preg_match('/^[\[(][^ ]+ TO [^ ]+[\])]$/', $value))) {
return $value ? $value : NULL;
}
@@ -245,9 +245,19 @@ class SearchApiFacetapiDate extends SearchApiFacetapiTerm implements FacetapiQue
}
}
// Get the finest level of detail we're allowed to drill down to.
$settings = $facet->getSettings()->settings;
$max_granularity = isset($settings['date_granularity']) ? $settings['date_granularity'] : FACETAPI_DATE_MINUTE;
// Get the finest level of detail we're allowed to drill down to.
$max_granularity = FACETAPI_DATE_MINUTE;
if (isset($settings['date_granularity'])) {
$max_granularity = $settings['date_granularity'];
}
// Get the coarsest level of detail we're allowed to start at.
$min_granularity = FACETAPI_DATE_YEAR;
if (isset($settings['date_granularity_min'])) {
$min_granularity = $settings['date_granularity_min'];
}
// Gets active facets, starts building hierarchy.
$parent = $granularity = NULL;
@@ -301,11 +311,14 @@ class SearchApiFacetapiDate extends SearchApiFacetapiTerm implements FacetapiQue
FACETAPI_DATE_MINUTE => 2,
FACETAPI_DATE_SECOND => 1,
);
// Gets gap numbers for both the gap and minimum gap, checks if the gap
// is within the limit set by the $granularity parameter.
// Gets gap numbers for both the gap, minimum and maximum gap, checks if
// the gap is within the limit set by the $granularity parameters.
if ($gap_numbers[$granularity] < $gap_numbers[$max_granularity]) {
$granularity = $max_granularity;
}
if ($gap_numbers[$granularity] > $gap_numbers[$min_granularity]) {
$granularity = $min_granularity;
}
}
else {
$granularity = $max_granularity;

View File

@@ -9,9 +9,9 @@ files[] = plugins/facetapi/adapter.inc
files[] = plugins/facetapi/query_type_term.inc
files[] = plugins/facetapi/query_type_date.inc
; Information added by Drupal.org packaging script on 2016-07-21
version = "7.x-1.20"
; Information added by Drupal.org packaging script on 2017-02-23
version = "7.x-1.21"
core = "7.x"
project = "search_api"
datestamp = "1469117342"
datestamp = "1487844493"

View File

@@ -22,12 +22,14 @@ function search_api_facetapi_install() {
*/
function search_api_facetapi_uninstall() {
variable_del('search_api_facets_search_ids');
variable_del('date_format_search_api_facetapi_' . FACETAPI_DATE_YEAR);
variable_del('date_format_search_api_facetapi_' . FACETAPI_DATE_MONTH);
variable_del('date_format_search_api_facetapi_' . FACETAPI_DATE_DAY);
variable_del('date_format_search_api_facetapi_' . FACETAPI_DATE_HOUR);
variable_del('date_format_search_api_facetapi_' . FACETAPI_DATE_MINUTE);
variable_del('date_format_search_api_facetapi_' . FACETAPI_DATE_SECOND);
// We have to use the literal values here, as the Facet API module could have
// already been disabled at this point.
variable_del('date_format_search_api_facetapi_YEAR');
variable_del('date_format_search_api_facetapi_MONTH');
variable_del('date_format_search_api_facetapi_DAY');
variable_del('date_format_search_api_facetapi_HOUR');
variable_del('date_format_search_api_facetapi_MINUTE');
variable_del('date_format_search_api_facetapi_SECOND');
}
/**

View File

@@ -151,7 +151,7 @@ class SearchApiViewsFacetsBlockDisplay extends views_plugin_display_block {
}
}
public function query(){
public function query() {
parent::query();
$facet_field = $this->get_option('facet_field');
@@ -291,7 +291,7 @@ class SearchApiViewsFacetsBlockDisplay extends views_plugin_display_block {
);
}
public function execute(){
public function execute() {
$info['content'] = $this->render();
$info['content']['more'] = $this->render_more_link();
$info['subject'] = filter_xss_admin($this->view->get_title());

View File

@@ -119,7 +119,17 @@ class SearchApiViewsHandlerFilterFulltext extends SearchApiViewsHandlerFilterTex
}
$words = preg_split('/\s+/', $input);
$quoted = FALSE;
foreach ($words as $i => $word) {
// Protect quoted strings.
if ($quoted && $word[strlen($word) - 1] === '"') {
$quoted = FALSE;
continue;
}
if ($quoted || $word[0] === '"') {
$quoted = TRUE;
continue;
}
if (drupal_strlen($word) < $this->options['min_length']) {
unset($words[$i]);
}

View File

@@ -93,7 +93,7 @@ class SearchApiViewsHandlerFilterTaxonomyTerm extends SearchApiViewsHandlerFilte
$query->orderby('tv.name');
$query->orderby('td.weight');
$query->orderby('td.name');
$query->addTag('term_access');
$query->addTag('taxonomy_term_access');
if ($vocabulary) {
$query->condition('tv.machine_name', $vocabulary->machine_name);
}
@@ -272,7 +272,7 @@ class SearchApiViewsHandlerFilterTaxonomyTerm extends SearchApiViewsHandlerFilte
if (!empty($this->definition['vocabulary'])) {
$query->condition('tv.machine_name', $this->definition['vocabulary']);
}
$query->addTag('term_access');
$query->addTag('taxonomy_term_access');
$result = $query->execute();
foreach ($result as $term) {
unset($missing[strtolower($term->name)]);

View File

@@ -29,8 +29,10 @@ class SearchApiViewsHandlerFilterUser extends SearchApiViewsHandlerFilterEntity
protected function ids_to_strings(array $ids) {
$names = array();
$args[':uids'] = array_filter($ids);
$result = db_query("SELECT uid, name FROM {users} u WHERE uid IN (:uids)", $args);
$result = $result->fetchAllKeyed();
if ($args[':uids']) {
$result = db_query('SELECT uid, name FROM {users} u WHERE uid IN (:uids)', $args);
$result = $result->fetchAllKeyed();
}
foreach ($ids as $uid) {
if (!$uid) {
$names[] = variable_get('anonymous', t('Anonymous'));

View File

@@ -103,7 +103,7 @@ class SearchApiViewsCache extends views_plugin_cache_time {
$key_data['exposed_info'] = $_GET['exposed_info'];
}
}
$key = md5(serialize($key_data));
$key = drupal_hash_base64(serialize($key_data));
return $key;
}

View File

@@ -310,6 +310,9 @@ class SearchApiViewsQuery extends views_plugin_query {
if (!empty($this->view->override_path) && strpos(current_path(), $this->view->override_path) !== 0) {
$this->query->setOption('search_api_base_path', $this->view->override_path);
}
// Save query information for Views UI.
$view->build_info['query'] = (string) $this->query;
}
/**

View File

@@ -27,9 +27,9 @@ files[] = includes/handler_sort.inc
files[] = includes/plugin_cache.inc
files[] = includes/query.inc
; Information added by Drupal.org packaging script on 2016-07-21
version = "7.x-1.20"
; Information added by Drupal.org packaging script on 2017-02-23
version = "7.x-1.21"
core = "7.x"
project = "search_api"
datestamp = "1469117342"
datestamp = "1487844493"

View File

@@ -45,7 +45,10 @@ function search_api_views_search_api_index_update(SearchApiIndex $index) {
* Implements hook_search_api_index_delete().
*/
function search_api_views_search_api_index_delete(SearchApiIndex $index) {
_search_api_views_index_unavailable($index);
// Only do this if this is a "real" deletion, no revert.
if (!$index->hasStatus(ENTITY_IN_CODE)) {
_search_api_views_index_unavailable($index);
}
}
/**

View File

@@ -182,4 +182,19 @@ abstract class SearchApiAbstractAlterCallback implements SearchApiAlterCallbackI
return array();
}
/**
* Determines whether the given index contains multiple types of entities.
*
* @param SearchApiIndex|null $index
* (optional) The index to examine. Defaults to the index set for this
* plugin.
*
* @return bool
* TRUE if the index is a multi-entity index, FALSE otherwise.
*/
protected function isMultiEntityIndex(SearchApiIndex $index = NULL) {
$index = $index ? $index : $this->index;
return $index->datasource() instanceof SearchApiCombinedEntityDataSourceController;
}
}

View File

@@ -209,6 +209,8 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback {
return $a;
}
return drupal_substr($b, 0, 1);
case 'last':
return isset($b) ? $b : $a;
case 'list':
if (!isset($a)) {
$a = array();
@@ -288,6 +290,7 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback {
'min' => t('Minimum'),
'first' => t('First'),
'first_char' => t('First letter'),
'last' => t('Last'),
'list' => t('List'),
);
case 'type':
@@ -299,6 +302,7 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback {
'min' => 'integer',
'first' => 'string',
'first_char' => 'string',
'last' => 'string',
'list' => 'list<string>',
);
case 'description':
@@ -310,6 +314,7 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback {
'min' => t('The Minimum aggregation computes the numerically smallest contained field value.'),
'first' => t('The First aggregation will simply keep the first encountered field value. This is helpful foremost when you know that a list field will only have a single value.'),
'first_char' => t('The "First letter" aggregation uses just the first letter of the first encountered field value as the aggregated value. This can, for example, be used to build a Glossary view.'),
'last' => t('The Last aggregation will simply keep the last encountered field value.'),
'list' => t('The List aggregation collects all field values into a multi-valued field containing all values.'),
);
}

View File

@@ -132,19 +132,4 @@ class SearchApiAlterBundleFilter extends SearchApiAbstractAlterCallback {
return !empty($entity_info['entity keys']['bundle']) && !empty($entity_info['bundles']);
}
/**
* Determines whether the given index contains multiple types of entities.
*
* @param SearchApiIndex|null $index
* (optional) The index to examine. Defaults to the index set for this
* plugin.
*
* @return bool
* TRUE if the index is a multi-entity index, FALSE otherwise.
*/
protected function isMultiEntityIndex(SearchApiIndex $index = NULL) {
$index = $index ? $index : $this->index;
return $index->datasource() instanceof SearchApiCombinedEntityDataSourceController;
}
}

View File

@@ -16,6 +16,9 @@ class SearchApiAlterRoleFilter extends SearchApiAbstractAlterCallback {
* This plugin only supports indexes containing users.
*/
public function supportsIndex(SearchApiIndex $index) {
if ($this->isMultiEntityIndex($index)) {
return in_array('user', $index->options['datasource']['types']);
}
return $index->getEntityType() == 'user';
}
@@ -23,10 +26,20 @@ class SearchApiAlterRoleFilter extends SearchApiAbstractAlterCallback {
* Implements SearchApiAlterCallbackInterface::alterItems().
*/
public function alterItems(array &$items) {
$roles = $this->options['roles'];
$selected_roles = $this->options['roles'];
$default = (bool) $this->options['default'];
foreach ($items as $id => $account) {
$role_match = (count(array_diff_key($account->roles, $roles)) !== count($account->roles));
$multi_types = $this->isMultiEntityIndex($this->index);
foreach ($items as $id => $item) {
if ($multi_types) {
if ($item->item_type !== 'user') {
continue;
}
$item_roles = $item->user->roles;
}
else {
$item_roles = $item->roles;
}
$role_match = (count(array_diff_key($item_roles, $selected_roles)) !== count($item_roles));
if ($role_match === $default) {
unset($items[$id]);
}

View File

@@ -626,8 +626,7 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou
return NULL;
}
$ret = array();
$indexes_by_id = array();
foreach ($indexes as $index) {
$this->checkIndex($index);
$update = db_update($this->table)
@@ -639,12 +638,26 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou
if ($item_ids !== FALSE) {
$update->condition($this->itemIdColumn, $item_ids, 'IN');
}
if ($update->execute()) {
$ret[] = $index;
}
$update->execute();
$indexes_by_id[$index->id] = $index;
}
return $ret;
// Determine and return the indexes with any changed items. If $item_ids is
// FALSE, all items are marked as changed and, thus, all indexes will be
// affected (unless they don't have any items, but no real point in treating
// that special case).
if ($item_ids !== FALSE) {
$indexes_with_items = db_select($this->table, 't')
->fields('t', array($this->indexIdColumn))
->distinct()
->condition($this->indexIdColumn, array_keys($indexes_by_id), 'IN')
->condition($this->itemIdColumn, $item_ids, 'IN')
->execute()
->fetchCol();
return array_intersect_key($indexes_by_id, array_flip($indexes_with_items));
}
return NULL;
}
/**
@@ -715,7 +728,7 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou
}
$this->checkIndex($index);
$select = db_select($this->table, 't');
$select->addField('t', 'item_id');
$select->addField('t', $this->itemIdColumn);
$select->condition($this->indexIdColumn, $index->id);
$select->condition($this->changedColumn, 0, '>');
$select->orderBy($this->changedColumn, 'ASC');

View File

@@ -278,10 +278,10 @@ class SearchApiEntityDataSourceController extends SearchApiAbstractDataSourceCon
$form['bundles'] = array(
'#type' => 'checkboxes',
'#title' => t('Bundles'),
'#description' => t('Restrict the entity bundles that will be included in this index. Leave blank to include all bundles. This setting cannot be changed for existing indexes.'),
'#description' => t('Restrict the entity bundles that will be included in this index. Leave blank to include all bundles. This setting cannot be changed for enabled indexes.'),
'#options' => array_map('check_plain', $options),
'#attributes' => array('class' => array('search-api-checkboxes-list')),
'#disabled' => !empty($form_state['index']),
'#disabled' => !empty($form_state['index']) && $form_state['index']->enabled,
);
if (!empty($form_state['index']->options['datasource'])) {
$form['bundles']['#default_value'] = drupal_map_assoc($form_state['index']->options['datasource']['bundles']);

View File

@@ -49,7 +49,7 @@ class SearchApiExternalDataSourceController extends SearchApiAbstractDataSourceC
* loadable, specify a function here.
*
* @param array $ids
* The IDs of the items to laod.
* The IDs of the items to load.
*
* @return array
* The loaded items, keyed by ID.

View File

@@ -44,6 +44,9 @@ class SearchApiCombinedEntityDataSourceController extends SearchApiAbstractDataS
$item->item_type = $type;
$item->item_entity_id = $entity_id;
$item->item_bundle = NULL;
// Add the item language so the "search_api_language" field will work
// correctly.
$item->language = isset($entity->language) ? $entity->language : NULL;
try {
list(, , $bundle) = entity_extract_ids($type, $entity);
$item->item_bundle = $bundle ? "$type:$bundle" : NULL;

View File

@@ -51,6 +51,7 @@ class SearchApiHighlight extends SearchApiAbstractProcessor {
'excerpt' => TRUE,
'excerpt_length' => 256,
'highlight' => 'always',
'highlight_partial' => FALSE,
'exclude_fields' => array(),
);
@@ -114,6 +115,13 @@ class SearchApiHighlight extends SearchApiAbstractProcessor {
'#default_value' => $this->options['highlight'],
);
$form['highlight_partial'] = array(
'#type' => 'checkbox',
'#title' => t('Highlight partial matches'),
'#description' => t('When enabled, matches in parts of words will be highlighted as well.'),
'#default_value' => $this->options['highlight_partial'],
);
return $form;
}
@@ -322,9 +330,9 @@ class SearchApiHighlight extends SearchApiAbstractProcessor {
$ranges = array();
$included = array();
$length = 0;
$workkeys = $keys;
while ($length < $this->options['excerpt_length'] && count($workkeys)) {
foreach ($workkeys as $k => $key) {
$work_keys = $keys;
while ($length < $this->options['excerpt_length'] && $work_keys) {
foreach ($work_keys as $k => $key) {
if ($length >= $this->options['excerpt_length']) {
break;
}
@@ -336,8 +344,14 @@ class SearchApiHighlight extends SearchApiAbstractProcessor {
// Locate a keyword (position $p, always >0 because $text starts with a
// space).
$p = 0;
if (preg_match('/' . self::$boundary . preg_quote($key, '/') . self::$boundary . '/iu', $text, $match, PREG_OFFSET_CAPTURE, $included[$key])) {
$p = $match[0][1];
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];
}
}
else {
$p = stripos($text, $key, $included[$key]);
}
// Now locate a space in front (position $q) and behind it (position $s),
// leaving about 60 characters extra before and after for context.
@@ -352,18 +366,13 @@ class SearchApiHighlight extends SearchApiAbstractProcessor {
$ranges[$q] = $p + $s;
$length += $p + $s - $q;
$included[$key] = $p + 1;
}
else {
unset($workkeys[$k]);
continue;
}
}
else {
unset($workkeys[$k]);
}
}
else {
unset($workkeys[$k]);
}
// Unless we got a match above, we don't need to look for this key any
// more.
unset($work_keys[$k]);
}
}
@@ -437,10 +446,14 @@ class SearchApiHighlight extends SearchApiAbstractProcessor {
}
return implode('', $texts);
}
$replace = $this->options['prefix'] . '\0' . $this->options['suffix'];
$keys = implode('|', array_map('preg_quote', $keys, array_fill(0, count($keys), '/')));
$text = preg_replace('/' . self::$boundary . '(' . $keys . ')' . self::$boundary . '/iu', $replace, ' ' . $text . ' ');
// If "Highlight partial matches" is disabled, we only want to highlight
// matches that are complete words. Otherwise, we want all of them.
$boundary = empty($this->options['highlight_partial']) ? self::$boundary : '';
$regex = '/' . $boundary . '(?:' . $keys . ')' . $boundary . '/iu';
$replace = $this->options['prefix'] . '\0' . $this->options['suffix'];
$text = preg_replace($regex, $replace, ' ' . $text . ' ');
return substr($text, 1, -1);
}

View File

@@ -0,0 +1,732 @@
<?php
/**
* @file
* Contains SearchApiPorterStemmer and SearchApiPorter2.
*/
/**
* Stems words to their roots.
*/
class SearchApiPorterStemmer extends SearchApiAbstractProcessor {
/**
* Static cache for already generated stems.
*
* @var array
*/
protected $stems = array();
/**
* {@inheritdoc}
*/
public function configurationForm() {
$form = parent::configurationForm();
$args = array(
'!algorithm' => url('https://github.com/markfullmer/porter2'),
'!exclusions' => url('https://github.com/markfullmer/porter2#user-content-custom-exclusions'),
);
$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>',
),
'exceptions' => array(
'#type' => 'textarea',
'#title' => t('Exceptions'),
'#description' => t('Enter exceptions in the form of WORD=STEM, where "WORD" is the term entered and "STEM" is the resulting stem. List each exception on a separate line.'),
'#default_value' => "texan=texa",
),
);
if (!empty($this->options['exceptions'])) {
$form['exceptions']['#default_value'] = $this->options['exceptions'];
}
return $form;
}
/**
* {@inheritdoc}
*/
protected function process(&$value) {
// Load custom exceptions.
$exceptions = $this->getExceptions();
$words = preg_split('/[^\p{L}\p{N}]+/u', $value, -1 , PREG_SPLIT_DELIM_CAPTURE);
$stemmed = array();
foreach ($words as $i => $word) {
if ($i % 2 == 0 && strlen($word)) {
if (!isset($this->stems[$word])) {
$stem = new SearchApiPorter2($word, $exceptions);
$this->stems[$word] = $stem->stem();
}
$stemmed[] = $this->stems[$word];
}
else {
$stemmed[] = $word;
}
}
$value = implode('', $stemmed);
}
/**
* Retrieves the processor's configured exceptions.
*
* @return string[]
* An associative array of exceptions, with words as keys and stems as their
* replacements.
*/
protected function getExceptions() {
if (!empty($this->options['exceptions'])) {
$exceptions = parse_ini_string($this->options['exceptions'], TRUE);
return is_array($exceptions) ? $exceptions : array();
}
return array();
}
}
/**
* Implements the Porter2 stemming algorithm.
*
* @see https://github.com/markfullmer/porter2
*/
class SearchApiPorter2 {
/**
* The word being stemmed.
*
* @var string
*/
protected $word;
/**
* The R1 of the word.
*
* @var int
*
* @see http://snowball.tartarus.org/texts/r1r2.html.
*/
protected $r1;
/**
* The R2 of the word.
*
* @var int
*
* @see http://snowball.tartarus.org/texts/r1r2.html.
*/
protected $r2;
/**
* List of exceptions to be used.
*
* @var string[]
*/
protected $exceptions = array();
/**
* Constructs a SearchApiPorter2 object.
*
* @param string $word
* The word to stem.
* @param string[] $custom_exceptions
* (optional) A custom list of exceptions.
*/
public function __construct($word, $custom_exceptions = array()) {
$this->word = $word;
$this->exceptions = $custom_exceptions + array(
'skis' => 'ski',
'skies' => 'sky',
'dying' => 'die',
'lying' => 'lie',
'tying' => 'tie',
'idly' => 'idl',
'gently' => 'gentl',
'ugly' => 'ugli',
'early' => 'earli',
'only' => 'onli',
'singly' => 'singl',
'sky' => 'sky',
'news' => 'news',
'howe' => 'howe',
'atlas' => 'atlas',
'cosmos' => 'cosmos',
'bias' => 'bias',
'andes' => 'andes',
);
// Set initial y, or y after a vowel, to Y.
$inc = 0;
while ($inc <= $this->length()) {
if (substr($this->word, $inc, 1) === 'y' && ($inc == 0 || $this->isVowel($inc - 1))) {
$this->word = substr_replace($this->word, 'Y', $inc, 1);
}
$inc++;
}
// Establish the regions R1 and R2. See function R().
$this->r1 = $this->R(1);
$this->r2 = $this->R(2);
}
/**
* Computes the stem of the word.
*
* @return string
* The word's stem.
*/
public function stem() {
// Ignore exceptions & words that are two letters or less.
if ($this->exceptions() || $this->length() <= 2) {
return strtolower($this->word);
}
else {
$this->step0();
$this->step1a();
$this->step1b();
$this->step1c();
$this->step2();
$this->step3();
$this->step4();
$this->step5();
}
return strtolower($this->word);
}
/**
* Determines whether the word is contained in our list of exceptions.
*
* If so, the $word property is changed to the stem listed in the exceptions.
*
* @return bool
* TRUE if the word was an exception, FALSE otherwise.
*/
protected function exceptions() {
if (isset($this->exceptions[$this->word])) {
$this->word = $this->exceptions[$this->word];
return TRUE;
}
return FALSE;
}
/**
* Searches for the longest among the "s" suffixes and removes it.
*
* Implements step 0 of the Porter2 algorithm.
*/
protected function step0() {
$found = FALSE;
$checks = array("'s'", "'s", "'");
foreach ($checks as $check) {
if (!$found && $this->hasEnding($check)) {
$this->removeEnding($check);
$found = TRUE;
}
}
}
/**
* Handles various suffixes, of which the longest is replaced.
*
* Implements step 1a of the Porter2 algorithm.
*/
protected function step1a() {
$found = FALSE;
if ($this->hasEnding('sses')) {
$this->removeEnding('sses');
$this->addEnding('ss');
$found = TRUE;
}
$checks = array('ied', 'ies');
foreach ($checks as $check) {
if (!$found && $this->hasEnding($check)) {
$length = $this->length();
$this->removeEnding($check);
if ($length > 4) {
$this->addEnding('i');
}
else {
$this->addEnding('ie');
}
$found = TRUE;
}
}
if ($this->hasEnding('us') || $this->hasEnding('ss')) {
$found = TRUE;
}
// Delete if preceding word part has a vowel not immediately before the s.
if (!$found && $this->hasEnding('s') && $this->containsVowel(substr($this->word, 0, -2))) {
$this->removeEnding('s');
}
}
/**
* Handles various suffixes, of which the longest is replaced.
*
* Implements step 1b of the Porter2 algorithm.
*/
protected function step1b() {
$exceptions = array(
'inning',
'outing',
'canning',
'herring',
'earring',
'proceed',
'exceed',
'succeed',
);
if (in_array($this->word, $exceptions)) {
return;
}
$checks = array('eedly', 'eed');
foreach ($checks as $check) {
if ($this->hasEnding($check)) {
if ($this->r1 !== $this->length()) {
$this->removeEnding($check);
$this->addEnding('ee');
}
return;
}
}
$checks = array('ingly', 'edly', 'ing', 'ed');
$second_endings = array('at', 'bl', 'iz');
foreach ($checks as $check) {
// If the ending is present and the previous part contains a vowel.
if ($this->hasEnding($check) && $this->containsVowel(substr($this->word, 0, -strlen($check)))) {
$this->removeEnding($check);
foreach ($second_endings as $ending) {
if ($this->hasEnding($ending)) {
$this->addEnding('e');
return;
}
}
// If the word ends with a double, remove the last letter.
$found = $this->removeDoubles();
// If the word is short, add e (so hop -> hope).
if (!$found && ($this->isShort())) {
$this->addEnding('e');
}
return;
}
}
}
/**
* Replaces suffix y or Y with i if after non-vowel not @ word begin.
*
* Implements step 1c of the Porter2 algorithm.
*/
protected function step1c() {
if (($this->hasEnding('y') || $this->hasEnding('Y')) && $this->length() > 2 && !($this->isVowel($this->length() - 2))) {
$this->removeEnding('y');
$this->addEnding('i');
}
}
/**
* Implements step 2 of the Porter2 algorithm.
*/
protected function step2() {
$checks = array(
"ization" => "ize",
"iveness" => "ive",
"fulness" => "ful",
"ational" => "ate",
"ousness" => "ous",
"biliti" => "ble",
"tional" => "tion",
"lessli" => "less",
"fulli" => "ful",
"entli" => "ent",
"ation" => "ate",
"aliti" => "al",
"iviti" => "ive",
"ousli" => "ous",
"alism" => "al",
"abli" => "able",
"anci" => "ance",
"alli" => "al",
"izer" => "ize",
"enci" => "ence",
"ator" => "ate",
"bli" => "ble",
"ogi" => "og",
);
foreach ($checks as $find => $replace) {
if ($this->hasEnding($find)) {
if ($this->inR1($find)) {
$this->removeEnding($find);
$this->addEnding($replace);
}
return;
}
}
if ($this->hasEnding('li')) {
if ($this->length() > 4 && $this->validLi($this->charAt(-3))) {
$this->removeEnding('li');
}
}
}
/**
* Implements step 3 of the Porter2 algorithm.
*/
protected function step3() {
$checks = array(
'ational' => 'ate',
'tional' => 'tion',
'alize' => 'al',
'icate' => 'ic',
'iciti' => 'ic',
'ical' => 'ic',
'ness' => '',
'ful' => '',
);
foreach ($checks as $find => $replace) {
if ($this->hasEnding($find)) {
if ($this->inR1($find)) {
$this->removeEnding($find);
$this->addEnding($replace);
}
return;
}
}
if ($this->hasEnding('ative')) {
if ($this->inR2('ative')) {
$this->removeEnding('ative');
}
}
}
/**
* Implements step 4 of the Porter2 algorithm.
*/
protected function step4() {
$checks = array(
'ement',
'ment',
'ance',
'ence',
'able',
'ible',
'ant',
'ent',
'ion',
'ism',
'ate',
'iti',
'ous',
'ive',
'ize',
'al',
'er',
'ic',
);
foreach ($checks as $check) {
// Among the suffixes, if found and in R2, delete.
if ($this->hasEnding($check)) {
if ($this->inR2($check)) {
if ($check !== 'ion' || in_array($this->charAt(-4), array('s', 't'))) {
$this->removeEnding($check);
}
}
return;
}
}
}
/**
* Implements step 5 of the Porter2 algorithm.
*/
protected function step5() {
if ($this->hasEnding('e')) {
// Delete if in R2, or in R1 and not preceded by a short syllable.
if ($this->inR2('e') || ($this->inR1('e') && !$this->isShortSyllable($this->length() - 3))) {
$this->removeEnding('e');
}
return;
}
if ($this->hasEnding('l')) {
// Delete if in R2 and preceded by l.
if ($this->inR2('l') && $this->charAt(-2) == 'l') {
$this->removeEnding('l');
}
}
}
/**
* Removes certain double consonants from the word's end.
*
* @return bool
* TRUE if a match was found and removed, FALSE otherwise.
*/
protected function removeDoubles() {
$found = FALSE;
$doubles = array('bb', 'dd', 'ff', 'gg', 'mm', 'nn', 'pp', 'rr', 'tt');
foreach ($doubles as $double) {
if (substr($this->word, -2) == $double) {
$this->word = substr($this->word, 0, -1);
$found = TRUE;
break;
}
}
return $found;
}
/**
* Checks whether a character is a vowel.
*
* @param int $position
* The character's position.
* @param string|null $word
* (optional) The word in which to check. Defaults to $this->word.
* @param string[] $additional
* (optional) Additional characters that should count as vowels.
*
* @return bool
* TRUE if the character is a vowel, FALSE otherwise.
*/
protected function isVowel($position, $word = NULL, $additional = array()) {
if ($word === NULL) {
$word = $this->word;
}
$vowels = array_merge(array('a', 'e', 'i', 'o', 'u', 'y'), $additional);
return in_array($this->charAt($position, $word), $vowels);
}
/**
* Retrieves the character at the given position.
*
* @param int $position
* The 0-based index of the character. If a negative number is given, the
* position is counted from the end of the string.
* @param string|null $word
* (optional) The word from which to retrieve the character. Defaults to
* $this->word.
*
* @return string
* The character at the given position, or an empty string if the given
* position was illegal.
*/
protected function charAt($position, $word = NULL) {
if ($word === NULL) {
$word = $this->word;
}
$length = strlen($word);
if (abs($position) >= $length) {
return '';
}
if ($position < 0) {
$position += $length;
}
return $word[$position];
}
/**
* Determines whether the word ends in a "vowel-consonant" suffix.
*
* Unless the word is only two characters long, it also checks that the
* third-last character is neither "w", "x" nor "Y".
*
* @param int|null $position
* (optional) If given, do not check the end of the word, but the character
* at the given position, and the next one.
*
* @return bool
* TRUE if the word has the described suffix, FALSE otherwise.
*/
protected function isShortSyllable($position = NULL) {
if ($position === NULL) {
$position = $this->length() - 2;
}
// A vowel at the beginning of the word followed by a non-vowel.
if ($position === 0) {
return $this->isVowel(0) && !$this->isVowel(1);
}
// Vowel followed by non-vowel other than w, x, Y and preceded by
// non-vowel.
$additional = array('w', 'x', 'Y');
return !$this->isVowel($position - 1) && $this->isVowel($position) && !$this->isVowel($position + 1, NULL, $additional);
}
/**
* Determines whether the word is short.
*
* A word is called short if it ends in a short syllable and if R1 is null.
*
* @return bool
* TRUE if the word is short, FALSE otherwise.
*/
protected function isShort() {
return $this->isShortSyllable() && $this->r1 == $this->length();
}
/**
* Determines the start of a certain "R" region.
*
* R is a region after the first non-vowel following a vowel, or end of word.
*
* @param int $type
* (optional) 1 or 2. If 2, then calculate the R after the R1.
*
* @return int
* The R position.
*/
protected function R($type = 1) {
$inc = 1;
if ($type === 2) {
$inc = $this->r1;
}
elseif ($this->length() > 5) {
$prefix_5 = substr($this->word, 0, 5);
if ($prefix_5 === 'gener' || $prefix_5 === 'arsen') {
return 5;
}
if ($this->length() > 6 && substr($this->word, 0, 6) === 'commun') {
return 6;
}
}
while ($inc <= $this->length()) {
if (!$this->isVowel($inc) && $this->isVowel($inc - 1)) {
$position = $inc;
break;
}
$inc++;
}
if (!isset($position)) {
$position = $this->length();
}
else {
// We add one, as this is the position AFTER the first non-vowel.
$position++;
}
return $position;
}
/**
* Checks whether the given string is contained in R1.
*
* @param string $string
* The string.
*
* @return bool
* TRUE if the string is in R1, FALSE otherwise.
*/
protected function inR1($string) {
$r1 = substr($this->word, $this->r1);
return strpos($r1, $string) !== FALSE;
}
/**
* Checks whether the given string is contained in R2.
*
* @param string $string
* The string.
*
* @return bool
* TRUE if the string is in R2, FALSE otherwise.
*/
protected function inR2($string) {
$r2 = substr($this->word, $this->r2);
return strpos($r2, $string) !== FALSE;
}
/**
* Determines the string length of the current word.
*
* @return int
* The string length of the current word.
*/
protected function length() {
return strlen($this->word);
}
/**
* Checks whether the word ends with the given string.
*
* @param string $string
* The string.
*
* @return bool
* TRUE if the word ends with the given string, FALSE otherwise.
*/
protected function hasEnding($string) {
$length = strlen($string);
if ($length > $this->length()) {
return FALSE;
}
return (substr_compare($this->word, $string, -1 * $length, $length) === 0);
}
/**
* Appends a given string to the current word.
*
* @param string $string
* The ending to append.
*/
protected function addEnding($string) {
$this->word = $this->word . $string;
}
/**
* Removes a given string from the end of the current word.
*
* Does not check whether the ending is actually there.
*
* @param string $string
* The ending to remove.
*/
protected function removeEnding($string) {
$this->word = substr($this->word, 0, -strlen($string));
}
/**
* Checks whether the given string contains a vowel.
*
* @param string $string
* The string to check.
*
* @return bool
* TRUE if the string contains a vowel, FALSE otherwise.
*/
protected function containsVowel($string) {
$inc = 0;
$return = FALSE;
while ($inc < strlen($string)) {
if ($this->isVowel($inc, $string)) {
$return = TRUE;
break;
}
$inc++;
}
return $return;
}
/**
* Checks whether the given string is a valid -li prefix.
*
* @param string $string
* The string to check.
*
* @return bool
* TRUE if the given string is a valid -li prefix, FALSE otherwise.
*/
protected function validLi($string) {
return in_array($string, array(
'c',
'd',
'e',
'g',
'h',
'k',
'm',
'n',
'r',
't',
));
}
}

View File

@@ -856,10 +856,33 @@ class SearchApiQuery implements SearchApiQueryInterface {
}
$ret .= 'Sorting: ' . implode(', ', $sort) . "\n";
}
$ret .= 'Options: ' . str_replace("\n", "\n ", var_export($this->options, TRUE)) . "\n";
$options = $this->sanitizeOptions($this->options);
$options = str_replace("\n", "\n ", var_export($options, TRUE));
$ret .= 'Options: ' . $options . "\n";
return $ret;
}
/**
* Sanitizes an array of options in a way that plays nice with var_export().
*
* @param array $options
* An array of options.
*
* @return array
* The sanitized options.
*/
protected function sanitizeOptions(array $options) {
foreach ($options as $key => $value) {
if (is_object($value)) {
$options[$key] = 'object (' . get_class($value) . ')';
}
elseif (is_array($value)) {
$options[$key] = $this->sanitizeOptions($value);
}
}
return $options;
}
}
/**
@@ -1048,6 +1071,10 @@ class SearchApiQueryFilter implements SearchApiQueryFilterInterface {
* {@inheritdoc}
*/
public function &getTags() {
// Tags can sometimes be NULL for old serialized query filter objects.
if (!isset($this->tags)) {
$this->tags = array();
}
return $this->tags;
}

View File

@@ -27,6 +27,7 @@ files[] = includes/processor.inc
files[] = includes/processor_highlight.inc
files[] = includes/processor_html_filter.inc
files[] = includes/processor_ignore_case.inc
files[] = includes/processor_stemmer.inc
files[] = includes/processor_stopwords.inc
files[] = includes/processor_tokenizer.inc
files[] = includes/processor_transliteration.inc
@@ -36,9 +37,9 @@ files[] = includes/service.inc
configure = admin/config/search/search_api
; Information added by Drupal.org packaging script on 2016-07-21
version = "7.x-1.20"
; Information added by Drupal.org packaging script on 2017-02-23
version = "7.x-1.21"
core = "7.x"
project = "search_api"
datestamp = "1469117342"
datestamp = "1487844493"

View File

@@ -362,7 +362,7 @@ function search_api_install() {
),
);
search_api_index_insert($values);
drupal_set_message('The Search API module was installed. A new default node index was created.');
drupal_set_message(t('The Search API module was installed. A new default node index was created.'));
}
/**

View File

@@ -784,7 +784,7 @@ function search_api_features_export_alter(&$export) {
* @see hook_search_api_item_type_info()
*/
function search_api_system_info_alter(&$info, $file, $type) {
if ($type != 'module' || $file->name == 'search_api') {
if ($type != 'module' || $file->name == 'search_api' || !module_exists($file->name)) {
return;
}
// Check for defined item types.
@@ -1152,11 +1152,17 @@ function search_api_search_api_processor_info() {
'class' => 'SearchApiStopWords',
'weight' => 30,
);
$processors['search_api_porter_stemmer'] = array(
'name' => t('Stem words'),
'description' => t('This processor reduces words to a stem (e.g., "talking" to "talk"). For best results, it should only be executed after tokenizing.'),
'class' => 'SearchApiPorterStemmer',
'weight' => 35,
);
$processors['search_api_highlighting'] = array(
'name' => t('Highlighting'),
'description' => t('Adds highlighting for search results.'),
'class' => 'SearchApiHighlight',
'weight' => 35,
'weight' => 40,
);
return $processors;

View File

@@ -10,7 +10,7 @@
* Implements hook_rules_action_info().
*/
function search_api_rules_action_info() {
$items['search_api_index'] = array (
$items['search_api_index'] = array(
'parameter' => array(
'entity' => array(
'type' => 'entity',

View File

@@ -10,9 +10,9 @@ files[] = search_api_test.module
hidden = TRUE
; Information added by Drupal.org packaging script on 2016-07-21
version = "7.x-1.20"
; Information added by Drupal.org packaging script on 2017-02-23
version = "7.x-1.21"
core = "7.x"
project = "search_api"
datestamp = "1469117342"
datestamp = "1487844493"