updated and repatched search_api module

please check this thread : Decide on strategy for language aware search https://www.drupal.org/node/1393058
This commit is contained in:
Bachir Soussi Chiadmi
2015-04-20 19:18:35 +02:00
parent d3dcf44a1e
commit ca9413af4d
34 changed files with 1272 additions and 377 deletions

View File

@@ -193,6 +193,12 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback {
return isset($a) ? min($a, $b) : $b;
case 'first':
return isset($a) ? $a : $b;
case 'list':
if (!isset($a)) {
$a = array();
}
$a[] = $b;
return $a;
}
}
@@ -261,6 +267,7 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback {
'max' => t('Maximum'),
'min' => t('Minimum'),
'first' => t('First'),
'list' => t('List'),
);
case 'type':
return array(
@@ -270,6 +277,7 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback {
'max' => 'integer',
'min' => 'integer',
'first' => 'string',
'list' => 'list<string>',
);
case 'description':
return array(
@@ -279,6 +287,7 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback {
'max' => t('The Maximum aggregation computes the numerically largest contained field value.'),
'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.'),
'list' => t('The List aggregation collects all field values into a multi-valued field containing all values.'),
);
}
}
@@ -289,6 +298,8 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback {
public function formButtonSubmit(array $form, array &$form_state) {
$button_name = $form_state['triggering_element']['#name'];
if ($button_name == 'op') {
// Increment $i until the corresponding field is not set, then create the
// field with that number as suffix.
for ($i = 1; isset($this->options['fields']['search_api_aggregation_' . $i]); ++$i) {
}
$this->options['fields']['search_api_aggregation_' . $i] = array(

View File

@@ -172,20 +172,25 @@ class SearchApiIndex extends Entity {
/**
* Constructor as a helper to the parent constructor.
*/
public function __construct(array $values = array()) {
parent::__construct($values, 'search_api_index');
public function __construct(array $values = array(), $entity_type = 'search_api_index') {
parent::__construct($values, $entity_type);
}
/**
* Execute necessary tasks for a newly created index.
*/
public function postCreate() {
if ($this->enabled) {
$this->queueItems();
try {
if ($server = $this->server()) {
// Tell the server about the new index.
$server->addIndex($this);
if ($this->enabled) {
$this->queueItems();
}
}
}
if ($server = $this->server()) {
// Tell the server about the new index.
$server->addIndex($this);
catch (SearchApiException $e) {
watchdog_exception('search_api', $e);
}
}
@@ -193,8 +198,13 @@ class SearchApiIndex extends Entity {
* Execute necessary tasks when the index is removed from the database.
*/
public function postDelete() {
if ($server = $this->server()) {
$server->removeIndex($this);
try {
if ($server = $this->server()) {
$server->removeIndex($this);
}
}
catch (SearchApiException $e) {
watchdog_exception('search_api', $e);
}
// Stop tracking entities for indexing.
@@ -206,7 +216,12 @@ class SearchApiIndex extends Entity {
*/
public function queueItems() {
if (!$this->read_only) {
$this->datasource()->startTracking(array($this));
try {
$this->datasource()->startTracking(array($this));
}
catch (SearchApiException $e) {
watchdog_exception('search_api', $e);
}
}
}
@@ -214,7 +229,12 @@ class SearchApiIndex extends Entity {
* Remove all records of entities to index.
*/
public function dequeueItems() {
$this->datasource()->stopTracking(array($this));
try {
$this->datasource()->stopTracking(array($this));
}
catch (SearchApiException $e) {
watchdog_exception('search_api', $e);
}
}
/**
@@ -231,16 +251,25 @@ class SearchApiIndex extends Entity {
if (empty($this->description)) {
$this->description = NULL;
}
if (empty($this->server)) {
$server = FALSE;
if (!empty($this->server)) {
$server = search_api_server_load($this->server);
if (!$server) {
$vars['%server'] = $this->server;
$vars['%index'] = $this->name;
watchdog('search_api', 'Unknown server %server specified for index %index.', $vars, WATCHDOG_ERROR);
}
}
if (!$server) {
$this->server = NULL;
$this->enabled = FALSE;
}
// This will also throw an exception if the server doesn't exist which is good.
elseif (!$this->server(TRUE)->enabled) {
$this->enabled = FALSE;
$this->server = NULL;
if (!empty($this->options['fields'])) {
ksort($this->options['fields']);
}
$this->resetCaches();
return parent::save();
}
@@ -305,7 +334,12 @@ class SearchApiIndex extends Entity {
return TRUE;
}
$this->server()->deleteItems('all', $this);
try {
$this->server()->deleteItems('all', $this);
}
catch (SearchApiException $e) {
watchdog_exception('search_api', $e);
}
_search_api_index_reindex($this);
module_invoke_all('search_api_index_reindex', $this, TRUE);
@@ -350,7 +384,12 @@ class SearchApiIndex extends Entity {
* otherwise.
*/
public function getEntityType() {
return $this->datasource()->getEntityType();
try {
return $this->datasource()->getEntityType();
}
catch (SearchApiException $e) {
return NULL;
}
}
/**
@@ -385,7 +424,7 @@ class SearchApiIndex extends Entity {
* SearchApiQueryInterface::__construct().
*
* @throws SearchApiException
* If the index is currently disabled.
* If the index is currently disabled or its server doesn't exist.
*
* @return SearchApiQueryInterface
* A query object for searching this index.
@@ -399,15 +438,20 @@ class SearchApiIndex extends Entity {
/**
* Indexes items on this index. Will return an array of IDs of items that
* should be marked as indexed i.e., items that were either rejected by a
* data-alter callback or were successfully indexed.
* Indexes items on this index.
*
* Will return an array of IDs of items that should be marked as indexed
* i.e., items that were either rejected by a data-alter callback or were
* successfully indexed.
*
* @param array $items
* An array of items to index.
* An array of items to index, of this index's item type.
*
* @return array
* An array of the IDs of all items that should be marked as indexed.
*
* @throws SearchApiException
* If an error occurred during indexing.
*/
public function index(array $items) {
if ($this->read_only) {
@@ -925,12 +969,18 @@ class SearchApiIndex extends Entity {
* @return EntityMetadataWrapper
* A wrapper for the item type of this index, optionally loaded with the
* given data and having additional fields according to the data alterations
* of this index.
* of this index (if $alter wasn't set to FALSE).
*/
public function entityWrapper($item = NULL, $alter = TRUE) {
$info['property info alter'] = $alter ? array($this, 'propertyInfoAlter') : '_search_api_wrapper_add_all_properties';
$info['property defaults']['property info alter'] = '_search_api_wrapper_add_all_properties';
return $this->datasource()->getMetadataWrapper($item, $info);
try {
$info['property info alter'] = $alter ? array($this, 'propertyInfoAlter') : '_search_api_wrapper_add_all_properties';
$info['property defaults']['property info alter'] = '_search_api_wrapper_add_all_properties';
return $this->datasource()->getMetadataWrapper($item, $info);
}
catch (SearchApiException $e) {
watchdog_exception('search_api', $e);
return entity_metadata_wrapper($this->item_type);
}
}
/**
@@ -945,16 +995,24 @@ class SearchApiIndex extends Entity {
* @see SearchApiDataSourceControllerInterface::loadItems()
*/
public function loadItems(array $ids) {
return $this->datasource()->loadItems($ids);
try {
return $this->datasource()->loadItems($ids);
}
catch (SearchApiException $e) {
watchdog_exception('search_api', $e);
return array();
}
}
/**
* Reset internal static caches.
* Reset internal caches.
*
* Should be used when things like fields or data alterations change to avoid
* using stale data.
*/
public function resetCaches() {
cache_clear_all($this->getCacheId(''), 'cache', TRUE);
$this->datasource = NULL;
$this->server_object = NULL;
$this->callbacks = NULL;

View File

@@ -22,8 +22,6 @@ class SearchApiHighlight extends SearchApiAbstractProcessor {
/**
* PREG regular expression for splitting words.
*
* We highlight around non-indexable or CJK characters.
*
* @var string
*/
protected static $split;
@@ -40,7 +38,7 @@ class SearchApiHighlight extends SearchApiAbstractProcessor {
'\x{F900}-\x{FAFF}\x{FF21}-\x{FF3A}\x{FF41}-\x{FF5A}\x{FF66}-\x{FFDC}' .
'\x{20000}-\x{2FFFD}\x{30000}-\x{3FFFD}';
self::$boundary = '(?:(?<=[' . PREG_CLASS_UNICODE_WORD_BOUNDARY . $cjk . '])|(?=[' . PREG_CLASS_UNICODE_WORD_BOUNDARY . $cjk . ']))';
self::$split = '/[' . PREG_CLASS_UNICODE_WORD_BOUNDARY . $cjk . ']+/iu';
self::$split = '/[' . PREG_CLASS_UNICODE_WORD_BOUNDARY . ']+/iu';
}
/**
@@ -53,6 +51,7 @@ class SearchApiHighlight extends SearchApiAbstractProcessor {
'excerpt' => TRUE,
'excerpt_length' => 256,
'highlight' => 'always',
'exclude_fields' => array(),
);
$form['prefix'] = array(
@@ -87,6 +86,22 @@ class SearchApiHighlight extends SearchApiAbstractProcessor {
),
),
);
// Exclude certain fulltextfields
$fields = $this->index->getFields();
$fulltext_fields = array();
foreach ($this->index->getFulltextFields() as $field) {
if (isset($fields[$field])) {
$fulltext_fields[$field] = $fields[$field]['name'] . ' (' . $field . ')';
}
}
$form['exclude_fields'] = array(
'#type' => 'checkboxes',
'#title' => t('Exclude fields from excerpt'),
'#description' => t('Exclude certain fulltext fields from being displayed in the excerpt.'),
'#options' => $fulltext_fields,
'#default_value' => $this->options['exclude_fields'],
'#attributes' => array('class' => array('search-api-checkboxes-list')),
);
$form['highlight'] = array(
'#type' => 'select',
'#title' => t('Highlight returned field data'),
@@ -106,21 +121,29 @@ class SearchApiHighlight extends SearchApiAbstractProcessor {
* {@inheritdoc}
*/
public function configurationFormValidate(array $form, array &$values, array &$form_state) {
// Overridden so $form['fields'] is not checked.
$values['exclude_fields'] = array_filter($values['exclude_fields']);
}
/**
* {@inheritdoc}
*/
public function postprocessSearchResults(array &$response, SearchApiQuery $query) {
if (!$response['result count'] || !($keys = $this->getKeywords($query))) {
if (empty($response['results']) || !($keys = $this->getKeywords($query))) {
return;
}
$fulltext_fields = $this->index->getFulltextFields();
if (!empty($this->options['exclude_fields'])) {
$fulltext_fields = drupal_map_assoc($fulltext_fields);
foreach ($this->options['exclude_fields'] as $field) {
unset($fulltext_fields[$field]);
}
}
foreach ($response['results'] as $id => &$result) {
if ($this->options['excerpt']) {
$text = array();
$fields = $this->getFulltextFields($response['results'], $id);
$fields = $this->getFulltextFields($response['results'], $id, $fulltext_fields);
foreach ($fields as $data) {
if (is_array($data)) {
$text = array_merge($text, $data);
@@ -129,10 +152,11 @@ class SearchApiHighlight extends SearchApiAbstractProcessor {
$text[] = $data;
}
}
$result['excerpt'] = $this->createExcerpt(implode("\n\n", $text), $keys);
$result['excerpt'] = $this->createExcerpt($this->flattenArrayValues($text), $keys);
}
if ($this->options['highlight'] != 'never') {
$fields = $this->getFulltextFields($response['results'], $id, $this->options['highlight'] == 'always');
$fields = $this->getFulltextFields($response['results'], $id, $fulltext_fields, $this->options['highlight'] == 'always');
foreach ($fields as $field => $data) {
if (is_array($data)) {
foreach ($data as $i => $text) {
@@ -155,6 +179,8 @@ class SearchApiHighlight extends SearchApiAbstractProcessor {
* @param int|string $i
* The index in the results array of the result whose data should be
* returned.
* @param array $fulltext_fields
* The fulltext fields from which the excerpt should be created.
* @param bool $load
* TRUE if the item should be loaded if necessary, FALSE if only fields
* already returned in the results should be used.
@@ -163,7 +189,7 @@ class SearchApiHighlight extends SearchApiAbstractProcessor {
* An array containing fulltext field names mapped to the text data
* contained in them for the given result.
*/
protected function getFulltextFields(array &$results, $i, $load = TRUE) {
protected function getFulltextFields(array &$results, $i, array $fulltext_fields, $load = TRUE) {
global $language;
$data = array();
@@ -171,7 +197,6 @@ class SearchApiHighlight extends SearchApiAbstractProcessor {
// Act as if $load is TRUE if we have a loaded item.
$load |= !empty($result['entity']);
$result += array('fields' => array());
$fulltext_fields = $this->index->getFulltextFields();
// We only need detailed fields data if $load is TRUE.
$fields = $load ? $this->index->getFields() : array();
$needs_extraction = array();
@@ -309,7 +334,7 @@ class SearchApiHighlight extends SearchApiAbstractProcessor {
// Locate a keyword (position $p, always >0 because $text starts with a
// space).
$p = 0;
if (preg_match('/' . self::$boundary . $key . self::$boundary . '/iu', $text, $match, PREG_OFFSET_CAPTURE, $included[$key])) {
if (preg_match('/' . self::$boundary . preg_quote($key, '/') . self::$boundary . '/iu', $text, $match, PREG_OFFSET_CAPTURE, $included[$key])) {
$p = $match[0][1];
}
// Now locate a space in front (position $q) and behind it (position $s),
@@ -379,7 +404,9 @@ class SearchApiHighlight extends SearchApiAbstractProcessor {
$text = (isset($newranges[0]) ? '' : $dots[0]) . implode($dots[1], $out) . $dots[2];
$text = check_plain($text);
return $this->highlightField($text, $keys);
// Since we stripped the tags at the beginning, highlighting doesn't need to
// handle HTML anymore.
return $this->highlightField($text, $keys, FALSE);
}
/**
@@ -389,15 +416,55 @@ class SearchApiHighlight extends SearchApiAbstractProcessor {
* The text of the field.
* @param array $keys
* Search keywords entered by the user.
* @param bool $html
* Whether the text can contain HTML tags or not. In the former case, text
* inside tags (i.e., tag names and attributes) won't be highlighted.
*
* @return string
* The field's text with all occurrences of search keywords highlighted.
*/
protected function highlightField($text, array $keys) {
protected function highlightField($text, array $keys, $html = TRUE) {
if (is_array($text)) {
$text = $this->flattenArrayValues($text);
}
if ($html) {
$texts = preg_split('#((?:</?[[:alpha:]](?:[^>"\']*|"[^"]*"|\'[^\']\')*>)+)#i', $text, -1, PREG_SPLIT_DELIM_CAPTURE);
for ($i = 0; $i < count($texts); $i += 2) {
$texts[$i] = $this->highlightField($texts[$i], $keys, FALSE);
}
return implode('', $texts);
}
$replace = $this->options['prefix'] . '\0' . $this->options['suffix'];
$keys = implode('|', array_map('preg_quote', $keys));
$keys = implode('|', array_map('preg_quote', $keys, array_fill(0, count($keys), '/')));
$text = preg_replace('/' . self::$boundary . '(' . $keys . ')' . self::$boundary . '/iu', $replace, ' ' . $text . ' ');
return substr($text, 1, -1);
}
/**
* Flattens a (possibly multidimensional) array into a string.
*
* @param array $array
* The array to flatten.
* @param string $glue
* The separator to insert between individual array items.
*
* @return string
* The glued string.
*/
protected function flattenArrayValues(array $array, $glue = "\n\n") {
$ret = array();
foreach ($array as $item) {
if (is_array($item)) {
$ret[] = $this->flattenArrayValues($item, $glue);
}
else {
$ret[] = $item;
}
}
return implode($glue, $ret);
}
}

View File

@@ -102,6 +102,8 @@ class SearchApiHtmlFilter extends SearchApiAbstractProcessor {
}
else {
$value = strip_tags($text);
// Remove any multiple or leading/trailing spaces we might have introduced.
$value = preg_replace('/\s\s+/', ' ', trim($value));
}
}
@@ -109,8 +111,11 @@ class SearchApiHtmlFilter extends SearchApiAbstractProcessor {
$ret = array();
while (($pos = strpos($text, '<')) !== FALSE) {
if ($boost && $pos > 0) {
$token = html_entity_decode(substr($text, 0, $pos), ENT_QUOTES, 'UTF-8');
// Remove any multiple or leading/trailing spaces we might have introduced.
$token = preg_replace('/\s\s+/', ' ', trim($token));
$ret[] = array(
'value' => html_entity_decode(substr($text, 0, $pos), ENT_QUOTES, 'UTF-8'),
'value' => $token,
'score' => $boost,
);
}
@@ -130,8 +135,11 @@ class SearchApiHtmlFilter extends SearchApiAbstractProcessor {
}
}
if ($text) {
$token = html_entity_decode($text, ENT_QUOTES, 'UTF-8');
// Remove any multiple or leading/trailing spaces we might have introduced.
$token = preg_replace('/\s\s+/', ' ', trim($token));
$ret[] = array(
'value' => html_entity_decode($text, ENT_QUOTES, 'UTF-8'),
'value' => $token,
'score' => $boost,
);
$text = '';

View File

@@ -226,6 +226,9 @@ interface SearchApiQueryInterface {
*
* This method should always be called by execute() and contain all necessary
* operations before the query is passed to the server's search() method.
*
* @throws SearchApiException
* If any error occurred during the preparation of the query.
*/
public function preExecute();
@@ -366,7 +369,7 @@ class SearchApiQuery implements SearchApiQueryInterface {
/**
* The index's machine name.
*
* used during serialization to avoid serializing the whole index object.
* Used during serialization to avoid serializing the whole index object.
*
* @var string
*/
@@ -811,6 +814,31 @@ class SearchApiQuery implements SearchApiQueryInterface {
$this->filter = clone $this->filter;
}
/**
* Implements the magic __toString() method to simplify debugging.
*/
public function __toString() {
$ret = 'Index: ' . $this->index->machine_name . "\n";
$ret .= 'Keys: ' . str_replace("\n", "\n ", var_export($this->orig_keys, TRUE)) . "\n";
if (isset($this->keys)) {
$ret .= 'Parsed keys: ' . str_replace("\n", "\n ", var_export($this->keys, TRUE)) . "\n";
$ret .= 'Searched fields: ' . (isset($this->fields) ? implode(', ', $this->fields) : '[ALL]') . "\n";
}
if ($filter = (string) $this->filter) {
$filter = str_replace("\n", "\n ", $filter);
$ret .= "Filters:\n $filter\n";
}
if ($this->sort) {
$sort = array();
foreach ($this->sort as $field => $order) {
$sort[] = "$field $order";
}
$ret .= 'Sorting: ' . implode(', ', $sort) . "\n";
}
$ret .= 'Options: ' . str_replace("\n", "\n ", var_export($this->options, TRUE)) . "\n";
return $ret;
}
}
/**
@@ -890,8 +918,11 @@ interface SearchApiQueryFilterInterface {
* Return all conditions and nested filters contained in this filter.
*
* @return array
* An array containing this filter's subfilters. Each of these is either an
* array (field, value, operator), or another SearchApiFilter object.
* An array containing this filter's subfilters. Each of these is either a
* condition, represented as a numerically indexed array with the arguments
* of a previous SearchApiQueryFilterInterface::condition() call (field,
* value, operator); or a nested filter, represented by a
* SearchApiQueryFilterInterface filter object.
*/
public function &getFilters();
@@ -1010,4 +1041,24 @@ class SearchApiQueryFilter implements SearchApiQueryFilterInterface {
}
}
/**
* Implements the magic __toString() method to simplify debugging.
*/
public function __toString() {
// Special case for a single, nested filter:
if (count($this->filters) == 1 && is_object($this->filters[0])) {
return (string) $this->filters[0];
}
$ret = array();
foreach ($this->filters as $filter) {
if (is_object($filter)) {
$ret[] = "[\n " . str_replace("\n", "\n ", (string) $filter) . "\n ]";
}
else {
$ret[] = "$filter[0] $filter[2] " . str_replace("\n", "\n ", var_export($filter[1], TRUE));
}
}
return $ret ? ' ' . implode("\n{$this->conjunction}\n ", $ret) : '';
}
}

View File

@@ -74,8 +74,8 @@ class SearchApiServer extends Entity {
/**
* Constructor as a helper to the parent constructor.
*/
public function __construct(array $values = array()) {
parent::__construct($values, 'search_api_server');
public function __construct(array $values = array(), $entity_type = 'search_api_server') {
parent::__construct($values, $entity_type);
}
/**