merged search_api submodule
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains base definitions for data alterations.
|
||||
*
|
||||
* Contains the SearchApiAlterCallbackInterface interface and the
|
||||
* SearchApiAbstractAlterCallback class.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Interface representing a Search API data-alter callback.
|
||||
*/
|
||||
interface SearchApiAlterCallbackInterface {
|
||||
|
||||
/**
|
||||
* Construct a data-alter callback.
|
||||
*
|
||||
* @param SearchApiIndex $index
|
||||
* The index whose items will be altered.
|
||||
* @param array $options
|
||||
* The callback options set for this index.
|
||||
*/
|
||||
public function __construct(SearchApiIndex $index, array $options = array());
|
||||
|
||||
/**
|
||||
* Check whether this data-alter callback is applicable for a certain index.
|
||||
*
|
||||
* This can be used for hiding the callback on the index's "Filters" tab. To
|
||||
* avoid confusion, you should only use criteria that are immutable, such as
|
||||
* the index's entity type. Also, since this is only used for UI purposes, you
|
||||
* should not completely rely on this to ensure certain index configurations
|
||||
* and at least throw an exception with a descriptive error message if this is
|
||||
* violated on runtime.
|
||||
*
|
||||
* @param SearchApiIndex $index
|
||||
* The index to check for.
|
||||
*
|
||||
* @return boolean
|
||||
* TRUE if the callback can run on the given index; FALSE otherwise.
|
||||
*/
|
||||
public function supportsIndex(SearchApiIndex $index);
|
||||
|
||||
/**
|
||||
* Display a form for configuring this callback.
|
||||
*
|
||||
* @return array
|
||||
* A form array for configuring this callback, or FALSE if no configuration
|
||||
* is possible.
|
||||
*/
|
||||
public function configurationForm();
|
||||
|
||||
/**
|
||||
* Validation callback for the form returned by configurationForm().
|
||||
*
|
||||
* @param array $form
|
||||
* The form returned by configurationForm().
|
||||
* @param array $values
|
||||
* The part of the $form_state['values'] array corresponding to this form.
|
||||
* @param array $form_state
|
||||
* The complete form state.
|
||||
*/
|
||||
public function configurationFormValidate(array $form, array &$values, array &$form_state);
|
||||
|
||||
/**
|
||||
* Submit callback for the form returned by configurationForm().
|
||||
*
|
||||
* This method should both return the new options and set them internally.
|
||||
*
|
||||
* @param array $form
|
||||
* The form returned by configurationForm().
|
||||
* @param array $values
|
||||
* The part of the $form_state['values'] array corresponding to this form.
|
||||
* @param array $form_state
|
||||
* The complete form state.
|
||||
*
|
||||
* @return array
|
||||
* The new options array for this callback.
|
||||
*/
|
||||
public function configurationFormSubmit(array $form, array &$values, array &$form_state);
|
||||
|
||||
/**
|
||||
* Alter items before indexing.
|
||||
*
|
||||
* Items which are removed from the array won't be indexed, but will be marked
|
||||
* as clean for future indexing. This could for instance be used to implement
|
||||
* some sort of access filter for security purposes (e.g., don't index
|
||||
* unpublished nodes or comments).
|
||||
*
|
||||
* @param array $items
|
||||
* An array of items to be altered, keyed by item IDs.
|
||||
*/
|
||||
public function alterItems(array &$items);
|
||||
|
||||
/**
|
||||
* Declare the properties that are added to items by this callback.
|
||||
*
|
||||
* If one of the specified properties already exists for an entity it will be
|
||||
* overridden, so keep a clear namespace by prefixing the properties with the
|
||||
* module name if this is not desired.
|
||||
*
|
||||
* CAUTION: Since this method is used when calling
|
||||
* SearchApiIndex::getFields(), calling that method from inside propertyInfo()
|
||||
* will lead to a recursion and should therefore be avoided.
|
||||
*
|
||||
* @see hook_entity_property_info()
|
||||
*
|
||||
* @return array
|
||||
* Information about all additional properties, as specified by
|
||||
* hook_entity_property_info() (only the inner "properties" array).
|
||||
*/
|
||||
public function propertyInfo();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract base class for data-alter callbacks.
|
||||
*
|
||||
* This class implements most methods with sensible defaults.
|
||||
*
|
||||
* Extending classes will at least have to implement the alterItems() method to
|
||||
* make this work. If that method adds additional fields to the items,
|
||||
* propertyInfo() has to be overridden, too.
|
||||
*/
|
||||
abstract class SearchApiAbstractAlterCallback implements SearchApiAlterCallbackInterface {
|
||||
|
||||
/**
|
||||
* The index whose items will be altered.
|
||||
*
|
||||
* @var SearchApiIndex
|
||||
*/
|
||||
protected $index;
|
||||
|
||||
/**
|
||||
* The configuration options for this callback, if it has any.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $options;
|
||||
|
||||
/**
|
||||
* Implements SearchApiAlterCallbackInterface::__construct().
|
||||
*/
|
||||
public function __construct(SearchApiIndex $index, array $options = array()) {
|
||||
$this->index = $index;
|
||||
$this->options = $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements SearchApiAlterCallbackInterface::supportsIndex().
|
||||
*
|
||||
* The default implementation always returns TRUE.
|
||||
*/
|
||||
public function supportsIndex(SearchApiIndex $index) {
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements SearchApiAlterCallbackInterface::configurationForm().
|
||||
*/
|
||||
public function configurationForm() {
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements SearchApiAlterCallbackInterface::configurationFormValidate().
|
||||
*/
|
||||
public function configurationFormValidate(array $form, array &$values, array &$form_state) { }
|
||||
|
||||
/**
|
||||
* Implements SearchApiAlterCallbackInterface::configurationFormSubmit().
|
||||
*/
|
||||
public function configurationFormSubmit(array $form, array &$values, array &$form_state) {
|
||||
$this->options = $values;
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements SearchApiAlterCallbackInterface::propertyInfo().
|
||||
*/
|
||||
public function propertyInfo() {
|
||||
return array();
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,322 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains SearchApiAlterAddAggregation.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Search API data alteration callback that adds an URL field for all items.
|
||||
*/
|
||||
class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback {
|
||||
|
||||
public function configurationForm() {
|
||||
$form['#attached']['css'][] = drupal_get_path('module', 'search_api') . '/search_api.admin.css';
|
||||
|
||||
$fields = $this->index->getFields(FALSE);
|
||||
$field_options = array();
|
||||
foreach ($fields as $name => $field) {
|
||||
$field_options[$name] = check_plain($field['name']);
|
||||
$field_properties[$name] = array(
|
||||
'#attributes' => array('title' => $name),
|
||||
'#description' => check_plain($field['description']),
|
||||
);
|
||||
}
|
||||
$additional = empty($this->options['fields']) ? array() : $this->options['fields'];
|
||||
|
||||
$types = $this->getTypes();
|
||||
$type_descriptions = $this->getTypes('description');
|
||||
$tmp = array();
|
||||
foreach ($types as $type => $name) {
|
||||
$tmp[$type] = array(
|
||||
'#type' => 'item',
|
||||
'#description' => $type_descriptions[$type],
|
||||
);
|
||||
}
|
||||
$type_descriptions = $tmp;
|
||||
|
||||
$form['#id'] = 'edit-callbacks-search-api-alter-add-aggregation-settings';
|
||||
$form['description'] = array(
|
||||
'#markup' => t('<p>This data alteration lets you define additional fields that will be added to this index. ' .
|
||||
'Each of these new fields will be an aggregation of one or more existing fields.</p>' .
|
||||
'<p>To add a new aggregated field, click the "Add new field" button and then fill out the form.</p>' .
|
||||
'<p>To remove a previously defined field, click the "Remove field" button.</p>' .
|
||||
'<p>You can also change the names or contained fields of existing aggregated fields.</p>'),
|
||||
);
|
||||
$form['fields']['#prefix'] = '<div id="search-api-alter-add-aggregation-field-settings">';
|
||||
$form['fields']['#suffix'] = '</div>';
|
||||
if (isset($this->changes)) {
|
||||
$form['fields']['#prefix'] .= '<div class="messages warning">All changes in the form will not be saved until the <em>Save configuration</em> button at the form bottom is clicked.</div>';
|
||||
}
|
||||
foreach ($additional as $name => $field) {
|
||||
$form['fields'][$name] = array(
|
||||
'#type' => 'fieldset',
|
||||
'#title' => $field['name'] ? $field['name'] : t('New field'),
|
||||
'#collapsible' => TRUE,
|
||||
'#collapsed' => (boolean) $field['name'],
|
||||
);
|
||||
$form['fields'][$name]['name'] = array(
|
||||
'#type' => 'textfield',
|
||||
'#title' => t('New field name'),
|
||||
'#default_value' => $field['name'],
|
||||
'#required' => TRUE,
|
||||
);
|
||||
$form['fields'][$name]['type'] = array(
|
||||
'#type' => 'select',
|
||||
'#title' => t('Aggregation type'),
|
||||
'#options' => $types,
|
||||
'#default_value' => $field['type'],
|
||||
'#required' => TRUE,
|
||||
);
|
||||
$form['fields'][$name]['type_descriptions'] = $type_descriptions;
|
||||
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]['fields'] = array_merge($field_properties, array(
|
||||
'#type' => 'checkboxes',
|
||||
'#title' => t('Contained fields'),
|
||||
'#options' => $field_options,
|
||||
'#default_value' => drupal_map_assoc($field['fields']),
|
||||
'#attributes' => array('class' => array('search-api-alter-add-aggregation-fields')),
|
||||
'#required' => TRUE,
|
||||
));
|
||||
$form['fields'][$name]['actions'] = array(
|
||||
'#type' => 'actions',
|
||||
'remove' => array(
|
||||
'#type' => 'submit',
|
||||
'#value' => t('Remove field'),
|
||||
'#submit' => array('_search_api_add_aggregation_field_submit'),
|
||||
'#limit_validation_errors' => array(),
|
||||
'#name' => 'search_api_add_aggregation_remove_' . $name,
|
||||
'#ajax' => array(
|
||||
'callback' => '_search_api_add_aggregation_field_ajax',
|
||||
'wrapper' => 'search-api-alter-add-aggregation-field-settings',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
$form['actions']['#type'] = 'actions';
|
||||
$form['actions']['add_field'] = array(
|
||||
'#type' => 'submit',
|
||||
'#value' => t('Add new field'),
|
||||
'#submit' => array('_search_api_add_aggregation_field_submit'),
|
||||
'#limit_validation_errors' => array(),
|
||||
'#ajax' => array(
|
||||
'callback' => '_search_api_add_aggregation_field_ajax',
|
||||
'wrapper' => 'search-api-alter-add-aggregation-field-settings',
|
||||
),
|
||||
);
|
||||
return $form;
|
||||
}
|
||||
|
||||
public function configurationFormValidate(array $form, array &$values, array &$form_state) {
|
||||
unset($values['actions']);
|
||||
if (empty($values['fields'])) {
|
||||
return;
|
||||
}
|
||||
foreach ($values['fields'] as $name => $field) {
|
||||
$fields = $values['fields'][$name]['fields'] = array_values(array_filter($field['fields']));
|
||||
unset($values['fields'][$name]['actions']);
|
||||
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.'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function configurationFormSubmit(array $form, array &$values, array &$form_state) {
|
||||
if (empty($values['fields'])) {
|
||||
return array();
|
||||
}
|
||||
$index_fields = $this->index->getFields(FALSE);
|
||||
foreach ($values['fields'] as $name => $field) {
|
||||
if (!$field['name']) {
|
||||
unset($values['fields'][$name]);
|
||||
}
|
||||
else {
|
||||
$values['fields'][$name]['description'] = $this->fieldDescription($field, $index_fields);
|
||||
}
|
||||
}
|
||||
$this->options = $values;
|
||||
return $values;
|
||||
}
|
||||
|
||||
public function alterItems(array &$items) {
|
||||
if (!$items) {
|
||||
return;
|
||||
}
|
||||
if (isset($this->options['fields'])) {
|
||||
$types = $this->getTypes('type');
|
||||
foreach ($items as $item) {
|
||||
$wrapper = $this->index->entityWrapper($item);
|
||||
foreach ($this->options['fields'] as $name => $field) {
|
||||
if ($field['name']) {
|
||||
$required_fields = array();
|
||||
foreach ($field['fields'] as $f) {
|
||||
if (!isset($required_fields[$f])) {
|
||||
$required_fields[$f]['type'] = $types[$field['type']];
|
||||
}
|
||||
}
|
||||
$fields = search_api_extract_fields($wrapper, $required_fields);
|
||||
$values = array();
|
||||
foreach ($fields as $f) {
|
||||
if (isset($f['value'])) {
|
||||
$values[] = $f['value'];
|
||||
}
|
||||
}
|
||||
$values = $this->flattenArray($values);
|
||||
|
||||
$this->reductionType = $field['type'];
|
||||
$item->$name = array_reduce($values, array($this, 'reduce'), NULL);
|
||||
if ($field['type'] == 'count' && !$item->$name) {
|
||||
$item->$name = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for reducing an array to a single value.
|
||||
*/
|
||||
public function reduce($a, $b) {
|
||||
switch ($this->reductionType) {
|
||||
case 'fulltext':
|
||||
return isset($a) ? $a . "\n\n" . $b : $b;
|
||||
case 'sum':
|
||||
return $a + $b;
|
||||
case 'count':
|
||||
return $a + 1;
|
||||
case 'max':
|
||||
return isset($a) ? max($a, $b) : $b;
|
||||
case 'min':
|
||||
return isset($a) ? min($a, $b) : $b;
|
||||
case 'first':
|
||||
return isset($a) ? $a : $b;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for flattening a multi-dimensional array.
|
||||
*/
|
||||
protected function flattenArray(array $data) {
|
||||
$ret = array();
|
||||
foreach ($data as $item) {
|
||||
if (!isset($item)) {
|
||||
continue;
|
||||
}
|
||||
if (is_scalar($item)) {
|
||||
$ret[] = $item;
|
||||
}
|
||||
else {
|
||||
$ret = array_merge($ret, $this->flattenArray($item));
|
||||
}
|
||||
}
|
||||
return $ret;
|
||||
}
|
||||
|
||||
public function propertyInfo() {
|
||||
$types = $this->getTypes('type');
|
||||
$ret = array();
|
||||
if (isset($this->options['fields'])) {
|
||||
foreach ($this->options['fields'] as $name => $field) {
|
||||
$ret[$name] = array(
|
||||
'label' => $field['name'],
|
||||
'description' => empty($field['description']) ? '' : $field['description'],
|
||||
'type' => $types[$field['type']],
|
||||
);
|
||||
}
|
||||
}
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for creating a field description.
|
||||
*/
|
||||
protected function fieldDescription(array $field, array $index_fields) {
|
||||
$fields = array();
|
||||
foreach ($field['fields'] as $f) {
|
||||
$fields[] = isset($index_fields[$f]) ? $index_fields[$f]['name'] : $f;
|
||||
}
|
||||
$type = $this->getTypes();
|
||||
$type = $type[$field['type']];
|
||||
return t('A @type aggregation of the following fields: @fields.', array('@type' => $type, '@fields' => implode(', ', $fields)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for getting all available aggregation types.
|
||||
*
|
||||
* @param $info (optional)
|
||||
* One of "name", "type" or "description", to indicate what values should be
|
||||
* returned for the types. Defaults to "name".
|
||||
*
|
||||
*/
|
||||
protected function getTypes($info = 'name') {
|
||||
switch ($info) {
|
||||
case 'name':
|
||||
return array(
|
||||
'fulltext' => t('Fulltext'),
|
||||
'sum' => t('Sum'),
|
||||
'count' => t('Count'),
|
||||
'max' => t('Maximum'),
|
||||
'min' => t('Minimum'),
|
||||
'first' => t('First'),
|
||||
);
|
||||
case 'type':
|
||||
return array(
|
||||
'fulltext' => 'text',
|
||||
'sum' => 'integer',
|
||||
'count' => 'integer',
|
||||
'max' => 'integer',
|
||||
'min' => 'integer',
|
||||
'first' => 'string',
|
||||
);
|
||||
case 'description':
|
||||
return array(
|
||||
'fulltext' => t('The Fulltext aggregation concatenates the text data of all contained fields.'),
|
||||
'sum' => t('The Sum aggregation adds the values of all contained fields numerically.'),
|
||||
'count' => t('The Count aggregation takes the total number of contained field values as the aggregated field value.'),
|
||||
'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.'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit helper callback for buttons in the callback's configuration form.
|
||||
*/
|
||||
public function formButtonSubmit(array $form, array &$form_state) {
|
||||
$button_name = $form_state['triggering_element']['#name'];
|
||||
if ($button_name == 'op') {
|
||||
for ($i = 1; isset($this->options['fields']['search_api_aggregation_' . $i]); ++$i) {
|
||||
}
|
||||
$this->options['fields']['search_api_aggregation_' . $i] = array(
|
||||
'name' => '',
|
||||
'type' => 'fulltext',
|
||||
'fields' => array(),
|
||||
);
|
||||
}
|
||||
else {
|
||||
$field = substr($button_name, 34);
|
||||
unset($this->options['fields'][$field]);
|
||||
}
|
||||
$form_state['rebuild'] = TRUE;
|
||||
$this->changes = TRUE;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit function for buttons in the callback's configuration form.
|
||||
*/
|
||||
function _search_api_add_aggregation_field_submit(array $form, array &$form_state) {
|
||||
$form_state['callbacks']['search_api_alter_add_aggregation']->formButtonSubmit($form, $form_state);
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX submit function for buttons in the callback's configuration form.
|
||||
*/
|
||||
function _search_api_add_aggregation_field_ajax(array $form, array &$form_state) {
|
||||
return $form['callbacks']['settings']['search_api_alter_add_aggregation']['fields'];
|
||||
}
|
@@ -0,0 +1,217 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains SearchApiAlterAddHierarchy.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Adds all ancestors for hierarchical fields.
|
||||
*/
|
||||
class SearchApiAlterAddHierarchy extends SearchApiAbstractAlterCallback {
|
||||
|
||||
/**
|
||||
* Cached value for the hierarchical field options.
|
||||
*
|
||||
* @var array
|
||||
*
|
||||
* @see getHierarchicalFields()
|
||||
*/
|
||||
protected $field_options;
|
||||
|
||||
/**
|
||||
* Overrides SearchApiAbstractAlterCallback::supportsIndex().
|
||||
*
|
||||
* Returns TRUE only if any hierarchical fields are available.
|
||||
*/
|
||||
public function supportsIndex(SearchApiIndex $index) {
|
||||
return (bool) $this->getHierarchicalFields();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configurationForm() {
|
||||
$options = $this->getHierarchicalFields();
|
||||
$this->options += array('fields' => array());
|
||||
$form['fields'] = array(
|
||||
'#title' => t('Hierarchical fields'),
|
||||
'#description' => t('Select the fields which should be supplemented with their ancestors. ' .
|
||||
'Each field is listed along with its children of the same type. ' .
|
||||
'When selecting several child properties of a field, all those properties will be recursively added to that field. ' .
|
||||
'Please note that you should de-select all fields before disabling this data alteration.'),
|
||||
'#type' => 'select',
|
||||
'#multiple' => TRUE,
|
||||
'#size' => min(6, count($options, COUNT_RECURSIVE)),
|
||||
'#options' => $options,
|
||||
'#default_value' => $this->options['fields'],
|
||||
);
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configurationFormSubmit(array $form, array &$values, array &$form_state) {
|
||||
// Change the saved type of fields in the index, if necessary.
|
||||
if (!empty($this->index->options['fields'])) {
|
||||
$fields = &$this->index->options['fields'];
|
||||
$previous = drupal_map_assoc($this->options['fields']);
|
||||
foreach ($values['fields'] as $field) {
|
||||
list($key) = explode(':', $field);
|
||||
if (empty($previous[$field]) && isset($fields[$key]['type'])) {
|
||||
$fields[$key]['type'] = 'list<' . search_api_extract_inner_type($fields[$key]['type']) . '>';
|
||||
$change = TRUE;
|
||||
}
|
||||
}
|
||||
$new = drupal_map_assoc($values['fields']);
|
||||
foreach ($previous as $field) {
|
||||
list($key) = explode(':', $field);
|
||||
if (empty($new[$field]) && isset($fields[$key]['type'])) {
|
||||
$w = $this->index->entityWrapper(NULL, FALSE);
|
||||
if (isset($w->$key)) {
|
||||
$type = $w->$key->type();
|
||||
$inner = search_api_extract_inner_type($fields[$key]['type']);
|
||||
$fields[$key]['type'] = search_api_nest_type($inner, $type);
|
||||
$change = TRUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isset($change)) {
|
||||
$this->index->save();
|
||||
}
|
||||
}
|
||||
|
||||
return parent::configurationFormSubmit($form, $values, $form_state);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function alterItems(array &$items) {
|
||||
if (empty($this->options['fields'])) {
|
||||
return;
|
||||
}
|
||||
foreach ($items as $item) {
|
||||
$wrapper = $this->index->entityWrapper($item, FALSE);
|
||||
|
||||
$values = array();
|
||||
foreach ($this->options['fields'] as $field) {
|
||||
list($key, $prop) = explode(':', $field);
|
||||
if (!isset($wrapper->$key)) {
|
||||
continue;
|
||||
}
|
||||
$child = $wrapper->$key;
|
||||
|
||||
$values += array($key => array());
|
||||
$this->extractHierarchy($child, $prop, $values[$key]);
|
||||
}
|
||||
foreach ($values as $key => $value) {
|
||||
$item->$key = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function propertyInfo() {
|
||||
if (empty($this->options['fields'])) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$ret = array();
|
||||
$wrapper = $this->index->entityWrapper(NULL, FALSE);
|
||||
foreach ($this->options['fields'] as $field) {
|
||||
list($key, $prop) = explode(':', $field);
|
||||
if (!isset($wrapper->$key)) {
|
||||
continue;
|
||||
}
|
||||
$child = $wrapper->$key;
|
||||
while (search_api_is_list_type($child->type())) {
|
||||
$child = $child[0];
|
||||
}
|
||||
if (!isset($child->$prop)) {
|
||||
continue;
|
||||
}
|
||||
if (!isset($ret[$key])) {
|
||||
$ret[$key] = $child->info();
|
||||
$type = search_api_extract_inner_type($ret[$key]['type']);
|
||||
$ret[$key]['type'] = "list<$type>";
|
||||
$ret[$key]['getter callback'] = 'entity_property_verbatim_get';
|
||||
// The return value of info() has some additional internal values set,
|
||||
// which we have to unset for the use here.
|
||||
unset($ret[$key]['name'], $ret[$key]['parent'], $ret[$key]['langcode'], $ret[$key]['clear'],
|
||||
$ret[$key]['property info alter'], $ret[$key]['property defaults']);
|
||||
}
|
||||
if (isset($ret[$key]['bundle'])) {
|
||||
$info = $child->$prop->info();
|
||||
if (empty($info['bundle']) || $ret[$key]['bundle'] != $info['bundle']) {
|
||||
unset($ret[$key]['bundle']);
|
||||
}
|
||||
}
|
||||
}
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all hierarchical fields for the current index.
|
||||
*
|
||||
* @return array
|
||||
* An array containing all hierarchical fields of the index, structured as
|
||||
* an options array grouped by primary field.
|
||||
*/
|
||||
protected function getHierarchicalFields() {
|
||||
if (!isset($this->field_options)) {
|
||||
$this->field_options = array();
|
||||
$wrapper = $this->index->entityWrapper(NULL, FALSE);
|
||||
// Only entities can be indexed in hierarchies, as other properties don't
|
||||
// have IDs that we can extract and store.
|
||||
$entity_info = entity_get_info();
|
||||
foreach ($wrapper as $key1 => $child) {
|
||||
while (search_api_is_list_type($child->type())) {
|
||||
$child = $child[0];
|
||||
}
|
||||
$info = $child->info();
|
||||
$type = $child->type();
|
||||
if (empty($entity_info[$type])) {
|
||||
continue;
|
||||
}
|
||||
foreach ($child as $key2 => $prop) {
|
||||
if (search_api_extract_inner_type($prop->type()) == $type) {
|
||||
$prop_info = $prop->info();
|
||||
$this->field_options[$info['label']]["$key1:$key2"] = $prop_info['label'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $this->field_options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts a hierarchy from a metadata wrapper by modifying $values.
|
||||
*/
|
||||
public function extractHierarchy(EntityMetadataWrapper $wrapper, $property, array &$values) {
|
||||
if (search_api_is_list_type($wrapper->type())) {
|
||||
foreach ($wrapper as $w) {
|
||||
$this->extractHierarchy($w, $property, $values);
|
||||
}
|
||||
return;
|
||||
}
|
||||
try {
|
||||
$v = $wrapper->value(array('identifier' => TRUE));
|
||||
if ($v && !isset($values[$v])) {
|
||||
$values[$v] = $v;
|
||||
if (isset($wrapper->$property) && $wrapper->value() && $wrapper->$property->value()) {
|
||||
$this->extractHierarchy($wrapper->$property, $property, $values);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (EntityMetadataWrapperException $e) {
|
||||
// Some properties like entity_metadata_book_get_properties() throw
|
||||
// exceptions, so we catch them here and ignore the property.
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains SearchApiAlterAddUrl.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Search API data alteration callback that adds an URL field for all items.
|
||||
*/
|
||||
class SearchApiAlterAddUrl extends SearchApiAbstractAlterCallback {
|
||||
|
||||
public function alterItems(array &$items) {
|
||||
foreach ($items as &$item) {
|
||||
$url = $this->index->datasource()->getItemUrl($item);
|
||||
if (!$url) {
|
||||
$item->search_api_url = NULL;
|
||||
continue;
|
||||
}
|
||||
$item->search_api_url = url($url['path'], array('absolute' => TRUE) + $url['options']);
|
||||
}
|
||||
}
|
||||
|
||||
public function propertyInfo() {
|
||||
return array(
|
||||
'search_api_url' => array(
|
||||
'label' => t('URI'),
|
||||
'description' => t('An URI where the item can be accessed.'),
|
||||
'type' => 'uri',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains SearchApiAlterAddViewedEntity.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Search API data alteration callback that adds an URL field for all items.
|
||||
*/
|
||||
class SearchApiAlterAddViewedEntity extends SearchApiAbstractAlterCallback {
|
||||
|
||||
/**
|
||||
* Only support indexes containing entities.
|
||||
*
|
||||
* @see SearchApiAlterCallbackInterface::supportsIndex()
|
||||
*/
|
||||
public function supportsIndex(SearchApiIndex $index) {
|
||||
return (bool) $index->getEntityType();
|
||||
}
|
||||
|
||||
public function configurationForm() {
|
||||
$view_modes = array();
|
||||
if ($entity_type = $this->index->getEntityType()) {
|
||||
$info = entity_get_info($entity_type);
|
||||
foreach ($info['view modes'] as $key => $mode) {
|
||||
$view_modes[$key] = $mode['label'];
|
||||
}
|
||||
}
|
||||
$this->options += array('mode' => reset($view_modes));
|
||||
if (count($view_modes) > 1) {
|
||||
$form['mode'] = array(
|
||||
'#type' => 'select',
|
||||
'#title' => t('View mode'),
|
||||
'#options' => $view_modes,
|
||||
'#default_value' => $this->options['mode'],
|
||||
);
|
||||
}
|
||||
else {
|
||||
$form['mode'] = array(
|
||||
'#type' => 'value',
|
||||
'#value' => $this->options['mode'],
|
||||
);
|
||||
if ($view_modes) {
|
||||
$form['note'] = array(
|
||||
'#markup' => '<p>' . t('Entities of type %type have only a single view mode. ' .
|
||||
'Therefore, no selection needs to be made.', array('%type' => $info['label'])) . '</p>',
|
||||
);
|
||||
}
|
||||
else {
|
||||
$form['note'] = array(
|
||||
'#markup' => '<p>' . t('Entities of type %type have no defined view modes. ' .
|
||||
'This might either mean that they are always displayed the same way, or that they cannot be processed by this alteration at all. ' .
|
||||
'Please consider this when using this alteration.', array('%type' => $info['label'])) . '</p>',
|
||||
);
|
||||
}
|
||||
}
|
||||
return $form;
|
||||
}
|
||||
|
||||
public function alterItems(array &$items) {
|
||||
// Prevent session information from being saved while indexing.
|
||||
drupal_save_session(FALSE);
|
||||
|
||||
// Force the current user to anonymous to prevent access bypass in search
|
||||
// indexes.
|
||||
$original_user = $GLOBALS['user'];
|
||||
$GLOBALS['user'] = drupal_anonymous_user();
|
||||
|
||||
$type = $this->index->getEntityType();
|
||||
$mode = empty($this->options['mode']) ? 'full' : $this->options['mode'];
|
||||
foreach ($items as &$item) {
|
||||
// Since we can't really know what happens in entity_view() and render(),
|
||||
// we use try/catch. This will at least prevent some errors, even though
|
||||
// it's no protection against fatal errors and the like.
|
||||
try {
|
||||
$render = entity_view($type, array(entity_id($type, $item) => $item), $mode);
|
||||
$text = render($render);
|
||||
if (!$text) {
|
||||
$item->search_api_viewed = NULL;
|
||||
continue;
|
||||
}
|
||||
$item->search_api_viewed = $text;
|
||||
}
|
||||
catch (Exception $e) {
|
||||
$item->search_api_viewed = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
// Restore the user.
|
||||
$GLOBALS['user'] = $original_user;
|
||||
drupal_save_session(TRUE);
|
||||
}
|
||||
|
||||
public function propertyInfo() {
|
||||
return array(
|
||||
'search_api_viewed' => array(
|
||||
'label' => t('Entity HTML output'),
|
||||
'description' => t('The whole HTML content of the entity when viewed.'),
|
||||
'type' => 'text',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains SearchApiAlterBundleFilter.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents a data alteration that restricts entity indexes to some bundles.
|
||||
*/
|
||||
class SearchApiAlterBundleFilter extends SearchApiAbstractAlterCallback {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function supportsIndex(SearchApiIndex $index) {
|
||||
return $index->getEntityType() && ($info = entity_get_info($index->getEntityType())) && self::hasBundles($info);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function alterItems(array &$items) {
|
||||
$info = entity_get_info($this->index->getEntityType());
|
||||
if (self::hasBundles($info) && isset($this->options['bundles'])) {
|
||||
$bundles = array_flip($this->options['bundles']);
|
||||
$default = (bool) $this->options['default'];
|
||||
$bundle_prop = $info['entity keys']['bundle'];
|
||||
foreach ($items as $id => $item) {
|
||||
if (isset($bundles[$item->$bundle_prop]) == $default) {
|
||||
unset($items[$id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configurationForm() {
|
||||
$info = entity_get_info($this->index->getEntityType());
|
||||
if (self::hasBundles($info)) {
|
||||
$options = array();
|
||||
foreach ($info['bundles'] as $bundle => $bundle_info) {
|
||||
$options[$bundle] = isset($bundle_info['label']) ? $bundle_info['label'] : $bundle;
|
||||
}
|
||||
$form = array(
|
||||
'default' => array(
|
||||
'#type' => 'radios',
|
||||
'#title' => t('Which items should be indexed?'),
|
||||
'#default_value' => isset($this->options['default']) ? $this->options['default'] : 1,
|
||||
'#options' => array(
|
||||
1 => t('All but those from one of the selected bundles'),
|
||||
0 => t('Only those from the selected bundles'),
|
||||
),
|
||||
),
|
||||
'bundles' => array(
|
||||
'#type' => 'select',
|
||||
'#title' => t('Bundles'),
|
||||
'#default_value' => isset($this->options['bundles']) ? $this->options['bundles'] : array(),
|
||||
'#options' => $options,
|
||||
'#size' => min(4, count($options)),
|
||||
'#multiple' => TRUE,
|
||||
),
|
||||
);
|
||||
}
|
||||
else {
|
||||
$form = array(
|
||||
'forbidden' => array(
|
||||
'#markup' => '<p>' . t("Items indexed by this index don't have bundles and therefore cannot be filtered here.") . '</p>',
|
||||
),
|
||||
);
|
||||
}
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a certain entity type has any bundles.
|
||||
*
|
||||
* @param array $entity_info
|
||||
* The entity type's entity_get_info() array.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if the entity type has bundles, FASLE otherwise.
|
||||
*/
|
||||
protected static function hasBundles(array $entity_info) {
|
||||
return !empty($entity_info['entity keys']['bundle']) && !empty($entity_info['bundles']);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
/**
|
||||
* @file
|
||||
* Contains the SearchApiAlterCommentAccess class.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Adds node access information to comment indexes.
|
||||
*/
|
||||
class SearchApiAlterCommentAccess extends SearchApiAlterNodeAccess {
|
||||
|
||||
/**
|
||||
* Overrides SearchApiAlterNodeAccess::supportsIndex().
|
||||
*
|
||||
* Returns TRUE only for indexes on comments.
|
||||
*/
|
||||
public function supportsIndex(SearchApiIndex $index) {
|
||||
return $index->getEntityType() === 'comment';
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides SearchApiAlterNodeAccess::getNode().
|
||||
*
|
||||
* Returns the comment's node, instead of the item (i.e., the comment) itself.
|
||||
*/
|
||||
protected function getNode($item) {
|
||||
return node_load($item->nid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides SearchApiAlterNodeAccess::configurationFormSubmit().
|
||||
*
|
||||
* Doesn't index the comment's "Author".
|
||||
*/
|
||||
public function configurationFormSubmit(array $form, array &$values, array &$form_state) {
|
||||
$old_status = !empty($form_state['index']->options['data_alter_callbacks']['search_api_alter_comment_access']['status']);
|
||||
$new_status = !empty($form_state['values']['callbacks']['search_api_alter_comment_access']['status']);
|
||||
|
||||
if (!$old_status && $new_status) {
|
||||
$form_state['index']->options['fields']['status']['type'] = 'boolean';
|
||||
}
|
||||
|
||||
return parent::configurationFormSubmit($form, $values, $form_state);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains SearchApiAlterLanguageControl.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Search API data alteration callback that filters out items based on their
|
||||
* bundle.
|
||||
*/
|
||||
class SearchApiAlterLanguageControl extends SearchApiAbstractAlterCallback {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __construct(SearchApiIndex $index, array $options = array()) {
|
||||
$options += array(
|
||||
'lang_field' => '',
|
||||
'languages' => array(),
|
||||
);
|
||||
parent::__construct($index, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides SearchApiAbstractAlterCallback::supportsIndex().
|
||||
*
|
||||
* Only returns TRUE if the system is multilingual.
|
||||
*
|
||||
* @see drupal_multilingual()
|
||||
*/
|
||||
public function supportsIndex(SearchApiIndex $index) {
|
||||
return drupal_multilingual();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configurationForm() {
|
||||
$form = array();
|
||||
|
||||
$wrapper = $this->index->entityWrapper();
|
||||
$fields[''] = t('- Use default -');
|
||||
foreach ($wrapper as $key => $property) {
|
||||
if ($key == 'search_api_language') {
|
||||
continue;
|
||||
}
|
||||
$type = $property->type();
|
||||
// Only single-valued string properties make sense here. Also, nested
|
||||
// properties probably don't make sense.
|
||||
if ($type == 'text' || $type == 'token') {
|
||||
$info = $property->info();
|
||||
$fields[$key] = $info['label'];
|
||||
}
|
||||
}
|
||||
|
||||
if (count($fields) > 1) {
|
||||
$form['lang_field'] = array(
|
||||
'#type' => 'select',
|
||||
'#title' => t('Language field'),
|
||||
'#description' => t("Select the field which should be used to determine an item's language."),
|
||||
'#options' => $fields,
|
||||
'#default_value' => $this->options['lang_field'],
|
||||
);
|
||||
}
|
||||
|
||||
$languages[LANGUAGE_NONE] = t('Language neutral');
|
||||
$list = language_list('enabled') + array(array(), array());
|
||||
foreach (array($list[1], $list[0]) as $list) {
|
||||
foreach ($list as $lang) {
|
||||
$name = t($lang->name);
|
||||
$native = $lang->native;
|
||||
$languages[$lang->language] = ($name == $native) ? $name : "$name ($native)";
|
||||
if (!$lang->enabled) {
|
||||
$languages[$lang->language] .= ' [' . t('disabled') . ']';
|
||||
}
|
||||
}
|
||||
}
|
||||
$form['languages'] = array(
|
||||
'#type' => 'checkboxes',
|
||||
'#title' => t('Indexed languages'),
|
||||
'#description' => t('Index only items in the selected languages. ' .
|
||||
'When no language is selected, there will be no language-related restrictions.'),
|
||||
'#options' => $languages,
|
||||
'#default_value' => $this->options['languages'],
|
||||
);
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configurationFormSubmit(array $form, array &$values, array &$form_state) {
|
||||
$values['languages'] = array_filter($values['languages']);
|
||||
return parent::configurationFormSubmit($form, $values, $form_state);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function alterItems(array &$items) {
|
||||
foreach ($items as $i => &$item) {
|
||||
// Set item language, if a custom field was selected.
|
||||
if ($field = $this->options['lang_field']) {
|
||||
$wrapper = $this->index->entityWrapper($item);
|
||||
if (isset($wrapper->$field)) {
|
||||
try {
|
||||
$item->search_api_language = $wrapper->$field->value();
|
||||
}
|
||||
catch (EntityMetadataWrapperException $e) {
|
||||
// Something went wrong while accessing the language field. Probably
|
||||
// doesn't really matter.
|
||||
}
|
||||
}
|
||||
}
|
||||
// Filter out items according to language, if any were selected.
|
||||
if ($languages = $this->options['languages']) {
|
||||
if (empty($languages[$item->search_api_language])) {
|
||||
unset($items[$i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
/**
|
||||
* @file
|
||||
* Contains the SearchApiAlterNodeAccess class.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Adds node access information to node indexes.
|
||||
*/
|
||||
class SearchApiAlterNodeAccess extends SearchApiAbstractAlterCallback {
|
||||
|
||||
/**
|
||||
* Overrides SearchApiAbstractAlterCallback::supportsIndex().
|
||||
*
|
||||
* Returns TRUE only for indexes on nodes.
|
||||
*/
|
||||
public function supportsIndex(SearchApiIndex $index) {
|
||||
// Currently only node access is supported.
|
||||
return $index->getEntityType() === 'node';
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides SearchApiAbstractAlterCallback::propertyInfo().
|
||||
*
|
||||
* Adds the "search_api_access_node" property.
|
||||
*/
|
||||
public function propertyInfo() {
|
||||
return array(
|
||||
'search_api_access_node' => array(
|
||||
'label' => t('Node access information'),
|
||||
'description' => t('Data needed to apply node access.'),
|
||||
'type' => 'list<token>',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function alterItems(array &$items) {
|
||||
static $account;
|
||||
|
||||
if (!isset($account)) {
|
||||
// Load the anonymous user.
|
||||
$account = drupal_anonymous_user();
|
||||
}
|
||||
|
||||
foreach ($items as $id => $item) {
|
||||
$node = $this->getNode($item);
|
||||
// Check whether all users have access to the node.
|
||||
if (!node_access('view', $node, $account)) {
|
||||
// Get node access grants.
|
||||
$result = db_query('SELECT * FROM {node_access} WHERE (nid = 0 OR nid = :nid) AND grant_view = 1', array(':nid' => $node->nid));
|
||||
|
||||
// Store all grants together with their realms in the item.
|
||||
foreach ($result as $grant) {
|
||||
$items[$id]->search_api_access_node[] = "node_access_{$grant->realm}:{$grant->gid}";
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Add the generic view grant if we are not using node access or the
|
||||
// node is viewable by anonymous users.
|
||||
$items[$id]->search_api_access_node = array('node_access__all');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the node related to a search item.
|
||||
*
|
||||
* In the default implementation for nodes, the item is already the node.
|
||||
* Subclasses may override this to easily provide node access checks for
|
||||
* items related to nodes.
|
||||
*/
|
||||
protected function getNode($item) {
|
||||
return $item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides SearchApiAbstractAlterCallback::configurationFormSubmit().
|
||||
*
|
||||
* If the data alteration is being enabled, set "Published" and "Author" to
|
||||
* "indexed", because both are needed for the node access filter.
|
||||
*/
|
||||
public function configurationFormSubmit(array $form, array &$values, array &$form_state) {
|
||||
$old_status = !empty($form_state['index']->options['data_alter_callbacks']['search_api_alter_node_access']['status']);
|
||||
$new_status = !empty($form_state['values']['callbacks']['search_api_alter_node_access']['status']);
|
||||
|
||||
if (!$old_status && $new_status) {
|
||||
$form_state['index']->options['fields']['status']['type'] = 'boolean';
|
||||
$form_state['index']->options['fields']['author']['type'] = 'integer';
|
||||
$form_state['index']->options['fields']['author']['entity_type'] = 'user';
|
||||
}
|
||||
|
||||
return parent::configurationFormSubmit($form, $values, $form_state);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains the SearchApiAlterNodeStatus class.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Exclude unpublished nodes from node indexes.
|
||||
*/
|
||||
class SearchApiAlterNodeStatus extends SearchApiAbstractAlterCallback {
|
||||
|
||||
/**
|
||||
* Check whether this data-alter callback is applicable for a certain index.
|
||||
*
|
||||
* Returns TRUE only for indexes on nodes.
|
||||
*
|
||||
* @param SearchApiIndex $index
|
||||
* The index to check for.
|
||||
*
|
||||
* @return boolean
|
||||
* TRUE if the callback can run on the given index; FALSE otherwise.
|
||||
*/
|
||||
public function supportsIndex(SearchApiIndex $index) {
|
||||
return $index->getEntityType() === 'node';
|
||||
}
|
||||
|
||||
/**
|
||||
* Alter items before indexing.
|
||||
*
|
||||
* Items which are removed from the array won't be indexed, but will be marked
|
||||
* as clean for future indexing.
|
||||
*
|
||||
* @param array $items
|
||||
* An array of items to be altered, keyed by item IDs.
|
||||
*/
|
||||
public function alterItems(array &$items) {
|
||||
foreach ($items as $nid => &$item) {
|
||||
if (empty($item->status)) {
|
||||
unset($items[$nid]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains the SearchApiAlterRoleFilter class.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Data alteration that filters out users based on their role.
|
||||
*/
|
||||
class SearchApiAlterRoleFilter extends SearchApiAbstractAlterCallback {
|
||||
|
||||
/**
|
||||
* Overrides SearchApiAbstractAlterCallback::supportsIndex().
|
||||
*
|
||||
* This plugin only supports indexes containing users.
|
||||
*/
|
||||
public function supportsIndex(SearchApiIndex $index) {
|
||||
return $index->getEntityType() == 'user';
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements SearchApiAlterCallbackInterface::alterItems().
|
||||
*/
|
||||
public function alterItems(array &$items) {
|
||||
$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));
|
||||
if ($role_match === $default) {
|
||||
unset($items[$id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides SearchApiAbstractAlterCallback::configurationForm().
|
||||
*
|
||||
* Add option for the roles to include/exclude.
|
||||
*/
|
||||
public function configurationForm() {
|
||||
$options = array_map('check_plain', user_roles());
|
||||
$form = array(
|
||||
'default' => array(
|
||||
'#type' => 'radios',
|
||||
'#title' => t('Which users should be indexed?'),
|
||||
'#default_value' => isset($this->options['default']) ? $this->options['default'] : 1,
|
||||
'#options' => array(
|
||||
1 => t('All but those from one of the selected roles'),
|
||||
0 => t('Only those from the selected roles'),
|
||||
),
|
||||
),
|
||||
'roles' => array(
|
||||
'#type' => 'select',
|
||||
'#title' => t('Roles'),
|
||||
'#default_value' => isset($this->options['roles']) ? $this->options['roles'] : array(),
|
||||
'#options' => $options,
|
||||
'#size' => min(4, count($options)),
|
||||
'#multiple' => TRUE,
|
||||
),
|
||||
);
|
||||
return $form;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,686 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains the SearchApiDataSourceControllerInterface as well as a default base class.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Interface for all data source controllers for Search API indexes.
|
||||
*
|
||||
* Data source controllers encapsulate all operations specific to an item type.
|
||||
* They are used for loading items, extracting item data, keeping track of the
|
||||
* item status, etc.
|
||||
*
|
||||
* Modules providing implementations of this interface that use a different way
|
||||
* (either different table or different method altogether) of keeping track of
|
||||
* indexed/dirty items than SearchApiAbstractDataSourceController should be
|
||||
* aware that indexes' numerical IDs can change due to feature reverts. It is
|
||||
* therefore recommended to use search_api_index_update_datasource(), or similar
|
||||
* code, in a hook_search_api_index_update() implementation.
|
||||
*/
|
||||
interface SearchApiDataSourceControllerInterface {
|
||||
|
||||
/**
|
||||
* Constructs a new data source controller.
|
||||
*
|
||||
* @param string $type
|
||||
* The item type for which this controller is created.
|
||||
*/
|
||||
public function __construct($type);
|
||||
|
||||
/**
|
||||
* Returns information on the ID field for this controller's type.
|
||||
*
|
||||
* @return array
|
||||
* An associative array containing the following keys:
|
||||
* - key: The property key for the ID field, as used in the item wrapper.
|
||||
* - type: The type of the ID field. Has to be one of the types from
|
||||
* search_api_field_types(). List types ("list<*>") are not allowed.
|
||||
*
|
||||
* @throws SearchApiDataSourceException
|
||||
* If any error state was encountered.
|
||||
*/
|
||||
public function getIdFieldInfo();
|
||||
|
||||
/**
|
||||
* Loads items of the type of this data source controller.
|
||||
*
|
||||
* @param array $ids
|
||||
* The IDs of the items to laod.
|
||||
*
|
||||
* @return array
|
||||
* The loaded items, keyed by ID.
|
||||
*
|
||||
* @throws SearchApiDataSourceException
|
||||
* If any error state was encountered.
|
||||
*/
|
||||
public function loadItems(array $ids);
|
||||
|
||||
/**
|
||||
* Creates a metadata wrapper for this datasource controller's type.
|
||||
*
|
||||
* @param mixed $item
|
||||
* Unless NULL, an item of the item type for this controller to be wrapped.
|
||||
* @param array $info
|
||||
* Optionally, additional information that should be used for creating the
|
||||
* wrapper. Uses the same format as entity_metadata_wrapper().
|
||||
*
|
||||
* @return EntityMetadataWrapper
|
||||
* A wrapper for the item type of this data source controller, according to
|
||||
* the info array, and optionally loaded with the given data.
|
||||
*
|
||||
* @throws SearchApiDataSourceException
|
||||
* If any error state was encountered.
|
||||
*
|
||||
* @see entity_metadata_wrapper()
|
||||
*/
|
||||
public function getMetadataWrapper($item = NULL, array $info = array());
|
||||
|
||||
/**
|
||||
* Retrieves the unique ID of an item.
|
||||
*
|
||||
* @param mixed $item
|
||||
* An item of this controller's type.
|
||||
*
|
||||
* @return mixed
|
||||
* Either the unique ID of the item, or NULL if none is available.
|
||||
*
|
||||
* @throws SearchApiDataSourceException
|
||||
* If any error state was encountered.
|
||||
*/
|
||||
public function getItemId($item);
|
||||
|
||||
/**
|
||||
* Retrieves a human-readable label for an item.
|
||||
*
|
||||
* @param mixed $item
|
||||
* An item of this controller's type.
|
||||
*
|
||||
* @return string|null
|
||||
* Either a human-readable label for the item, or NULL if none is available.
|
||||
*
|
||||
* @throws SearchApiDataSourceException
|
||||
* If any error state was encountered.
|
||||
*/
|
||||
public function getItemLabel($item);
|
||||
|
||||
/**
|
||||
* Retrieves a URL at which the item can be viewed on the web.
|
||||
*
|
||||
* @param mixed $item
|
||||
* An item of this controller's type.
|
||||
*
|
||||
* @return array|null
|
||||
* Either an array containing the 'path' and 'options' keys used to build
|
||||
* the URL of the item, and matching the signature of url(), or NULL if the
|
||||
* item has no URL of its own.
|
||||
*
|
||||
* @throws SearchApiDataSourceException
|
||||
* If any error state was encountered.
|
||||
*/
|
||||
public function getItemUrl($item);
|
||||
|
||||
/**
|
||||
* Initializes tracking of the index status of items for the given indexes.
|
||||
*
|
||||
* All currently known items of this data source's type should be inserted
|
||||
* into the tracking table for the given indexes, with status "changed". If
|
||||
* items were already present, these should also be set to "changed" and not
|
||||
* be inserted again.
|
||||
*
|
||||
* @param SearchApiIndex[] $indexes
|
||||
* The SearchApiIndex objects for which item tracking should be initialized.
|
||||
*
|
||||
* @throws SearchApiDataSourceException
|
||||
* If any error state was encountered.
|
||||
*/
|
||||
public function startTracking(array $indexes);
|
||||
|
||||
/**
|
||||
* Stops tracking of the index status of items for the given indexes.
|
||||
*
|
||||
* The tracking tables of the given indexes should be completely cleared.
|
||||
*
|
||||
* @param SearchApiIndex[] $indexes
|
||||
* The SearchApiIndex objects for which item tracking should be stopped.
|
||||
*
|
||||
* @throws SearchApiDataSourceException
|
||||
* If any error state was encountered.
|
||||
*/
|
||||
public function stopTracking(array $indexes);
|
||||
|
||||
/**
|
||||
* Starts tracking the index status for the given items on the given indexes.
|
||||
*
|
||||
* @param array $item_ids
|
||||
* The IDs of new items to track.
|
||||
* @param SearchApiIndex[] $indexes
|
||||
* The indexes for which items should be tracked.
|
||||
*
|
||||
* @throws SearchApiDataSourceException
|
||||
* If any error state was encountered.
|
||||
*/
|
||||
public function trackItemInsert(array $item_ids, array $indexes);
|
||||
|
||||
/**
|
||||
* Sets the tracking status of the given items to "changed"/"dirty".
|
||||
*
|
||||
* Unless $dequeue is set to TRUE, this operation is ignored for items whose
|
||||
* status is not "indexed".
|
||||
*
|
||||
* @param array|false $item_ids
|
||||
* Either an array with the IDs of the changed items. Or FALSE to mark all
|
||||
* items as changed for the given indexes.
|
||||
* @param SearchApiIndex[] $indexes
|
||||
* The indexes for which the change should be tracked.
|
||||
* @param bool $dequeue
|
||||
* (deprecated) If set to TRUE, also change the status of queued items.
|
||||
* The concept of queued items will be removed in the Drupal 8 version of
|
||||
* this module.
|
||||
*
|
||||
* @throws SearchApiDataSourceException
|
||||
* If any error state was encountered.
|
||||
*/
|
||||
public function trackItemChange($item_ids, array $indexes, $dequeue = FALSE);
|
||||
|
||||
/**
|
||||
* Sets the tracking status of the given items to "queued".
|
||||
*
|
||||
* Queued items are not marked as "dirty" even when they are changed, and they
|
||||
* are not returned by the getChangedItems() method.
|
||||
*
|
||||
* @param array|false $item_ids
|
||||
* Either an array with the IDs of the queued items. Or FALSE to mark all
|
||||
* items as queued for the given indexes.
|
||||
* @param SearchApiIndex $index
|
||||
* The index for which the items were queued.
|
||||
*
|
||||
* @throws SearchApiDataSourceException
|
||||
* If any error state was encountered.
|
||||
*
|
||||
* @deprecated
|
||||
* As of Search API 1.10, the cron queue is not used for indexing anymore,
|
||||
* therefore this method has become useless. It will be removed in the
|
||||
* Drupal 8 version of this module.
|
||||
*/
|
||||
public function trackItemQueued($item_ids, SearchApiIndex $index);
|
||||
|
||||
/**
|
||||
* Sets the tracking status of the given items to "indexed".
|
||||
*
|
||||
* @param array $item_ids
|
||||
* The IDs of the indexed items.
|
||||
* @param SearchApiIndex $index
|
||||
* The index on which the items were indexed.
|
||||
*
|
||||
* @throws SearchApiDataSourceException
|
||||
* If any error state was encountered.
|
||||
*/
|
||||
public function trackItemIndexed(array $item_ids, SearchApiIndex $index);
|
||||
|
||||
/**
|
||||
* Stops tracking the index status for the given items on the given indexes.
|
||||
*
|
||||
* @param array $item_ids
|
||||
* The IDs of the removed items.
|
||||
* @param SearchApiIndex[] $indexes
|
||||
* The indexes for which the deletions should be tracked.
|
||||
*
|
||||
* @throws SearchApiDataSourceException
|
||||
* If any error state was encountered.
|
||||
*/
|
||||
public function trackItemDelete(array $item_ids, array $indexes);
|
||||
|
||||
/**
|
||||
* Retrieves a list of items that need to be indexed.
|
||||
*
|
||||
* If possible, completely unindexed items should be returned before items
|
||||
* that were indexed but later changed. Also, items that were changed longer
|
||||
* ago should be favored.
|
||||
*
|
||||
* @param SearchApiIndex $index
|
||||
* The index for which changed items should be returned.
|
||||
* @param int $limit
|
||||
* The maximum number of items to return. Negative values mean "unlimited".
|
||||
*
|
||||
* @return array
|
||||
* The IDs of items that need to be indexed for the given index.
|
||||
*
|
||||
* @throws SearchApiDataSourceException
|
||||
* If any error state was encountered.
|
||||
*/
|
||||
public function getChangedItems(SearchApiIndex $index, $limit = -1);
|
||||
|
||||
/**
|
||||
* Retrieves information on how many items have been indexed for a certain index.
|
||||
*
|
||||
* @param SearchApiIndex $index
|
||||
* The index whose index status should be returned.
|
||||
*
|
||||
* @return array
|
||||
* An associative array containing two keys (in this order):
|
||||
* - indexed: The number of items already indexed in their latest version.
|
||||
* - total: The total number of items that have to be indexed for this
|
||||
* index.
|
||||
*
|
||||
* @throws SearchApiDataSourceException
|
||||
* If any error state was encountered.
|
||||
*/
|
||||
public function getIndexStatus(SearchApiIndex $index);
|
||||
|
||||
/**
|
||||
* Retrieves the entity type of items from this datasource.
|
||||
*
|
||||
* @return string|null
|
||||
* An entity type string if the items provided by this datasource are
|
||||
* entities; NULL otherwise.
|
||||
*
|
||||
* @throws SearchApiDataSourceException
|
||||
* If any error state was encountered.
|
||||
*/
|
||||
public function getEntityType();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a default base class for datasource controllers.
|
||||
*
|
||||
* Contains default implementations for a number of methods which will be
|
||||
* similar for most data sources. Concrete data sources can decide to extend
|
||||
* this base class to save time, but can also implement the interface directly.
|
||||
*
|
||||
* A subclass will still have to provide implementations for the following
|
||||
* methods:
|
||||
* - getIdFieldInfo()
|
||||
* - loadItems()
|
||||
* - getMetadataWrapper() or getPropertyInfo()
|
||||
* - startTracking() or getAllItemIds()
|
||||
*
|
||||
* The table used by default for tracking the index status of items is
|
||||
* {search_api_item}. This can easily be changed, for example when an item type
|
||||
* has non-integer IDs, by changing the $table property.
|
||||
*/
|
||||
abstract class SearchApiAbstractDataSourceController implements SearchApiDataSourceControllerInterface {
|
||||
|
||||
/**
|
||||
* The item type for this controller instance.
|
||||
*/
|
||||
protected $type;
|
||||
|
||||
/**
|
||||
* The entity type for this controller instance.
|
||||
*
|
||||
* @var string|null
|
||||
*
|
||||
* @see getEntityType()
|
||||
*/
|
||||
protected $entityType = NULL;
|
||||
|
||||
/**
|
||||
* The info array for the item type, as specified via
|
||||
* hook_search_api_item_type_info().
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $info;
|
||||
|
||||
/**
|
||||
* The table used for tracking items. Set to NULL on subclasses to disable
|
||||
* the default tracking for an item type, or change the property to use a
|
||||
* different table for tracking.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'search_api_item';
|
||||
|
||||
/**
|
||||
* When using the default tracking mechanism: the name of the column on
|
||||
* $this->table containing the item ID.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $itemIdColumn = 'item_id';
|
||||
|
||||
/**
|
||||
* When using the default tracking mechanism: the name of the column on
|
||||
* $this->table containing the index ID.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $indexIdColumn = 'index_id';
|
||||
|
||||
/**
|
||||
* When using the default tracking mechanism: the name of the column on
|
||||
* $this->table containing the indexing status.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $changedColumn = 'changed';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __construct($type) {
|
||||
$this->type = $type;
|
||||
$this->info = search_api_get_item_type_info($type);
|
||||
|
||||
if (!empty($this->info['entity_type'])) {
|
||||
$this->entityType = $this->info['entity_type'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getEntityType() {
|
||||
return $this->entityType;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getMetadataWrapper($item = NULL, array $info = array()) {
|
||||
$info += $this->getPropertyInfo();
|
||||
return entity_metadata_wrapper($this->entityType ? $this->entityType : $this->type, $item, $info);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the property info for this item type.
|
||||
*
|
||||
* This is a helper method for getMetadataWrapper() that can be used by
|
||||
* subclasses to specify the property information to use when creating a
|
||||
* metadata wrapper.
|
||||
*
|
||||
* The data structure uses largely the format specified in
|
||||
* hook_entity_property_info(). However, the first level of keys (containing
|
||||
* the entity types) is omitted, and the "properties" key is called
|
||||
* "property info" instead. So, an example return value would look like this:
|
||||
*
|
||||
* @code
|
||||
* return array(
|
||||
* 'property info' => array(
|
||||
* 'foo' => array(
|
||||
* 'label' => t('Foo'),
|
||||
* 'type' => 'text',
|
||||
* ),
|
||||
* 'bar' => array(
|
||||
* 'label' => t('Bar'),
|
||||
* 'type' => 'list<integer>',
|
||||
* ),
|
||||
* ),
|
||||
* );
|
||||
* @endcode
|
||||
*
|
||||
* SearchApiExternalDataSourceController::getPropertyInfo() contains a working
|
||||
* example of this method.
|
||||
*
|
||||
* If the item type is an entity type, no additional property information is
|
||||
* required, the method will thus just return an empty array. You can still
|
||||
* use this to append additional properties to the entities, or the like,
|
||||
* though.
|
||||
*
|
||||
* @return array
|
||||
* Property information as specified by entity_metadata_wrapper().
|
||||
*
|
||||
* @throws SearchApiDataSourceException
|
||||
* If any error state was encountered.
|
||||
*
|
||||
* @see getMetadataWrapper()
|
||||
* @see hook_entity_property_info()
|
||||
*/
|
||||
protected function getPropertyInfo() {
|
||||
// If this is an entity type, no additional property info is needed.
|
||||
if ($this->entityType) {
|
||||
return array();
|
||||
}
|
||||
throw new SearchApiDataSourceException(t('No known property information for type @type.', array('@type' => $this->type)));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getItemId($item) {
|
||||
$id_info = $this->getIdFieldInfo();
|
||||
$field = $id_info['key'];
|
||||
$wrapper = $this->getMetadataWrapper($item);
|
||||
if (!isset($wrapper->$field)) {
|
||||
return NULL;
|
||||
}
|
||||
$id = $wrapper->$field->value();
|
||||
return $id ? $id : NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getItemLabel($item) {
|
||||
$label = $this->getMetadataWrapper($item)->label();
|
||||
return $label ? $label : NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getItemUrl($item) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function startTracking(array $indexes) {
|
||||
if (!$this->table) {
|
||||
return;
|
||||
}
|
||||
// We first clear the tracking table for all indexes, so we can just insert
|
||||
// all items again without any key conflicts.
|
||||
$this->stopTracking($indexes);
|
||||
// Insert all items as new.
|
||||
$this->trackItemInsert($this->getAllItemIds(), $indexes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the IDs of all items that are known for this controller's type.
|
||||
*
|
||||
* Helper method that can be used by subclasses instead of implementing
|
||||
* startTracking().
|
||||
*
|
||||
* @return array
|
||||
* An array containing all item IDs for this type.
|
||||
*
|
||||
* @throws SearchApiDataSourceException
|
||||
* If any error state was encountered.
|
||||
*/
|
||||
protected function getAllItemIds() {
|
||||
throw new SearchApiDataSourceException(t('Items not known for type @type.', array('@type' => $this->type)));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function stopTracking(array $indexes) {
|
||||
if (!$this->table) {
|
||||
return;
|
||||
}
|
||||
// We could also use a single query with "IN" operator, but this method
|
||||
// will mostly be called with only one index.
|
||||
foreach ($indexes as $index) {
|
||||
$this->checkIndex($index);
|
||||
db_delete($this->table)
|
||||
->condition($this->indexIdColumn, $index->id)
|
||||
->execute();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function trackItemInsert(array $item_ids, array $indexes) {
|
||||
if (!$this->table) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Since large amounts of items can overstrain the database, only add items
|
||||
// in chunks.
|
||||
foreach (array_chunk($item_ids, 1000) as $chunk) {
|
||||
$insert = db_insert($this->table)
|
||||
->fields(array($this->itemIdColumn, $this->indexIdColumn, $this->changedColumn));
|
||||
foreach ($chunk as $item_id) {
|
||||
foreach ($indexes as $index) {
|
||||
$this->checkIndex($index);
|
||||
$insert->values(array(
|
||||
$this->itemIdColumn => $item_id,
|
||||
$this->indexIdColumn => $index->id,
|
||||
$this->changedColumn => 1,
|
||||
));
|
||||
}
|
||||
}
|
||||
$insert->execute();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function trackItemChange($item_ids, array $indexes, $dequeue = FALSE) {
|
||||
if (!$this->table) {
|
||||
return;
|
||||
}
|
||||
$index_ids = array();
|
||||
foreach ($indexes as $index) {
|
||||
$this->checkIndex($index);
|
||||
$index_ids[] = $index->id;
|
||||
}
|
||||
$update = db_update($this->table)
|
||||
->fields(array(
|
||||
$this->changedColumn => REQUEST_TIME,
|
||||
))
|
||||
->condition($this->indexIdColumn, $index_ids, 'IN')
|
||||
->condition($this->changedColumn, 0, $dequeue ? '<=' : '=');
|
||||
if ($item_ids !== FALSE) {
|
||||
$update->condition($this->itemIdColumn, $item_ids, 'IN');
|
||||
}
|
||||
$update->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function trackItemQueued($item_ids, SearchApiIndex $index) {
|
||||
$this->checkIndex($index);
|
||||
if (!$this->table) {
|
||||
return;
|
||||
}
|
||||
$update = db_update($this->table)
|
||||
->fields(array(
|
||||
$this->changedColumn => -1,
|
||||
))
|
||||
->condition($this->indexIdColumn, $index->id);
|
||||
if ($item_ids !== FALSE) {
|
||||
$update->condition($this->itemIdColumn, $item_ids, 'IN');
|
||||
}
|
||||
$update->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function trackItemIndexed(array $item_ids, SearchApiIndex $index) {
|
||||
if (!$this->table) {
|
||||
return;
|
||||
}
|
||||
$this->checkIndex($index);
|
||||
db_update($this->table)
|
||||
->fields(array(
|
||||
$this->changedColumn => 0,
|
||||
))
|
||||
->condition($this->itemIdColumn, $item_ids, 'IN')
|
||||
->condition($this->indexIdColumn, $index->id)
|
||||
->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function trackItemDelete(array $item_ids, array $indexes) {
|
||||
if (!$this->table) {
|
||||
return;
|
||||
}
|
||||
$index_ids = array();
|
||||
foreach ($indexes as $index) {
|
||||
$this->checkIndex($index);
|
||||
$index_ids[] = $index->id;
|
||||
}
|
||||
db_delete($this->table)
|
||||
->condition($this->itemIdColumn, $item_ids, 'IN')
|
||||
->condition($this->indexIdColumn, $index_ids, 'IN')
|
||||
->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getChangedItems(SearchApiIndex $index, $limit = -1) {
|
||||
if ($limit == 0) {
|
||||
return array();
|
||||
}
|
||||
$this->checkIndex($index);
|
||||
$select = db_select($this->table, 't');
|
||||
$select->addField('t', 'item_id');
|
||||
$select->condition($this->indexIdColumn, $index->id);
|
||||
$select->condition($this->changedColumn, 0, '>');
|
||||
$select->orderBy($this->changedColumn, 'ASC');
|
||||
if ($limit > 0) {
|
||||
$select->range(0, $limit);
|
||||
}
|
||||
return $select->execute()->fetchCol();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getIndexStatus(SearchApiIndex $index) {
|
||||
if (!$this->table) {
|
||||
return array('indexed' => 0, 'total' => 0);
|
||||
}
|
||||
$this->checkIndex($index);
|
||||
$indexed = db_select($this->table, 'i')
|
||||
->condition($this->indexIdColumn, $index->id)
|
||||
->condition($this->changedColumn, 0)
|
||||
->countQuery()
|
||||
->execute()
|
||||
->fetchField();
|
||||
$total = db_select($this->table, 'i')
|
||||
->condition($this->indexIdColumn, $index->id)
|
||||
->countQuery()
|
||||
->execute()
|
||||
->fetchField();
|
||||
return array('indexed' => $indexed, 'total' => $total);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given index is valid for this datasource controller.
|
||||
*
|
||||
* Helper method used by various methods in this class. By default only checks
|
||||
* whether the types match.
|
||||
*
|
||||
* @param SearchApiIndex $index
|
||||
* The index to check.
|
||||
*
|
||||
* @throws SearchApiDataSourceException
|
||||
* If the index doesn't fit to this datasource controller.
|
||||
*/
|
||||
protected function checkIndex(SearchApiIndex $index) {
|
||||
if ($index->item_type != $this->type) {
|
||||
$index_type = search_api_get_item_type_info($index->item_type);
|
||||
$index_type = empty($index_type['name']) ? $index->item_type : $index_type['name'];
|
||||
$msg = t(
|
||||
'Invalid index @index of type @index_type passed to data source controller for type @this_type.',
|
||||
array('@index' => $index->name, '@index_type' => $index_type, '@this_type' => $this->info['name'])
|
||||
);
|
||||
throw new SearchApiDataSourceException($msg);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains the SearchApiEntityDataSourceController class.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents a datasource for all entities known to the Entity API.
|
||||
*/
|
||||
class SearchApiEntityDataSourceController extends SearchApiAbstractDataSourceController {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getIdFieldInfo() {
|
||||
$info = entity_get_info($this->entityType);
|
||||
$properties = entity_get_property_info($this->entityType);
|
||||
if (empty($info['entity keys']['id'])) {
|
||||
throw new SearchApiDataSourceException(t("Entity type @type doesn't specify an ID key.", array('@type' => $info['label'])));
|
||||
}
|
||||
$field = $info['entity keys']['id'];
|
||||
if (empty($properties['properties'][$field]['type'])) {
|
||||
throw new SearchApiDataSourceException(t("Entity type @type doesn't specify a type for the @prop property.", array('@type' => $info['label'], '@prop' => $field)));
|
||||
}
|
||||
$type = $properties['properties'][$field]['type'];
|
||||
if (search_api_is_list_type($type)) {
|
||||
throw new SearchApiDataSourceException(t("Entity type @type uses list field @prop as its ID.", array('@type' => $info['label'], '@prop' => $field)));
|
||||
}
|
||||
if ($type == 'token') {
|
||||
$type = 'string';
|
||||
}
|
||||
return array(
|
||||
'key' => $field,
|
||||
'type' => $type,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function loadItems(array $ids) {
|
||||
$items = entity_load($this->entityType, $ids);
|
||||
// If some items couldn't be loaded, remove them from tracking.
|
||||
if (count($items) != count($ids)) {
|
||||
$ids = array_flip($ids);
|
||||
$unknown = array_keys(array_diff_key($ids, $items));
|
||||
if ($unknown) {
|
||||
search_api_track_item_delete($this->type, $unknown);
|
||||
}
|
||||
}
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getMetadataWrapper($item = NULL, array $info = array()) {
|
||||
return entity_metadata_wrapper($this->entityType, $item, $info);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getItemId($item) {
|
||||
$id = entity_id($this->entityType, $item);
|
||||
return $id ? $id : NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getItemLabel($item) {
|
||||
$label = entity_label($this->entityType, $item);
|
||||
return $label ? $label : NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getItemUrl($item) {
|
||||
if ($this->entityType == 'file') {
|
||||
return array(
|
||||
'path' => file_create_url($item->uri),
|
||||
'options' => array(
|
||||
'entity_type' => 'file',
|
||||
'entity' => $item,
|
||||
),
|
||||
);
|
||||
}
|
||||
$url = entity_uri($this->entityType, $item);
|
||||
return $url ? $url : NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function startTracking(array $indexes) {
|
||||
if (!$this->table) {
|
||||
return;
|
||||
}
|
||||
// We first clear the tracking table for all indexes, so we can just insert
|
||||
// all items again without any key conflicts.
|
||||
$this->stopTracking($indexes);
|
||||
|
||||
$entity_info = entity_get_info($this->entityType);
|
||||
|
||||
if (!empty($entity_info['base table'])) {
|
||||
// Use a subselect, which will probably be much faster than entity_load().
|
||||
|
||||
// Assumes that all entities use the "base table" property and the
|
||||
// "entity keys[id]" in the same way as the default controller.
|
||||
$id_field = $entity_info['entity keys']['id'];
|
||||
$table = $entity_info['base table'];
|
||||
|
||||
// We could also use a single insert (with a JOIN in the nested query),
|
||||
// but this method will be mostly called with a single index, anyways.
|
||||
foreach ($indexes as $index) {
|
||||
// Select all entity ids.
|
||||
$query = db_select($table, 't');
|
||||
$query->addField('t', $id_field, 'item_id');
|
||||
$query->addExpression(':index_id', 'index_id', array(':index_id' => $index->id));
|
||||
$query->addExpression('1', 'changed');
|
||||
|
||||
// INSERT ... SELECT ...
|
||||
db_insert($this->table)
|
||||
->from($query)
|
||||
->execute();
|
||||
}
|
||||
}
|
||||
else {
|
||||
// In the absence of a 'base table', use the slow entity_load().
|
||||
parent::startTracking($indexes);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getAllItemIds() {
|
||||
return array_keys(entity_load($this->entityType));
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,259 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains the SearchApiExternalDataSourceController class.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Base class for data source controllers for external data sources.
|
||||
*
|
||||
* This data source controller is a base implementation for item types that
|
||||
* represent external data, not directly accessible in Drupal. You can use this
|
||||
* controller as a base class when you don't want to index items of the type via
|
||||
* Drupal, but only want the search capabilities of the Search API. In addition
|
||||
* you most probably also have to create a fitting service class for executing
|
||||
* the actual searches.
|
||||
*
|
||||
* To use most of the functionality of the Search API and related modules, you
|
||||
* will only have to specify some property information in getPropertyInfo(). If
|
||||
* you have a custom service class which already returns the extracted fields
|
||||
* with the search results, you will only have to provide a label and a type for
|
||||
* each field.
|
||||
*/
|
||||
class SearchApiExternalDataSourceController extends SearchApiAbstractDataSourceController {
|
||||
|
||||
/**
|
||||
* Return information on the ID field for this controller's type.
|
||||
*
|
||||
* This implementation will return a field named "id" of type "string". This
|
||||
* can also be used if the item type in question has no IDs.
|
||||
*
|
||||
* @return array
|
||||
* An associative array containing the following keys:
|
||||
* - key: The property key for the ID field, as used in the item wrapper.
|
||||
* - type: The type of the ID field. Has to be one of the types from
|
||||
* search_api_field_types(). List types ("list<*>") are not allowed.
|
||||
*/
|
||||
public function getIdFieldInfo() {
|
||||
return array(
|
||||
'key' => 'id',
|
||||
'type' => 'string',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load items of the type of this data source controller.
|
||||
*
|
||||
* Always returns an empty array. If you want the items of your type to be
|
||||
* loadable, specify a function here.
|
||||
*
|
||||
* @param array $ids
|
||||
* The IDs of the items to laod.
|
||||
*
|
||||
* @return array
|
||||
* The loaded items, keyed by ID.
|
||||
*/
|
||||
public function loadItems(array $ids) {
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides SearchApiAbstractDataSourceController::getPropertyInfo().
|
||||
*
|
||||
* Only returns a single string ID field.
|
||||
*/
|
||||
protected function getPropertyInfo() {
|
||||
$info['property info']['id'] = array(
|
||||
'label' => t('ID'),
|
||||
'type' => 'string',
|
||||
);
|
||||
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the unique ID of an item.
|
||||
*
|
||||
* Always returns 1.
|
||||
*
|
||||
* @param $item
|
||||
* An item of this controller's type.
|
||||
*
|
||||
* @return
|
||||
* Either the unique ID of the item, or NULL if none is available.
|
||||
*/
|
||||
public function getItemId($item) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a human-readable label for an item.
|
||||
*
|
||||
* Always returns NULL.
|
||||
*
|
||||
* @param $item
|
||||
* An item of this controller's type.
|
||||
*
|
||||
* @return
|
||||
* Either a human-readable label for the item, or NULL if none is available.
|
||||
*/
|
||||
public function getItemLabel($item) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a URL at which the item can be viewed on the web.
|
||||
*
|
||||
* Always returns NULL.
|
||||
*
|
||||
* @param $item
|
||||
* An item of this controller's type.
|
||||
*
|
||||
* @return
|
||||
* Either an array containing the 'path' and 'options' keys used to build
|
||||
* the URL of the item, and matching the signature of url(), or NULL if the
|
||||
* item has no URL of its own.
|
||||
*/
|
||||
public function getItemUrl($item) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize tracking of the index status of items for the given indexes.
|
||||
*
|
||||
* All currently known items of this data source's type should be inserted
|
||||
* into the tracking table for the given indexes, with status "changed". If
|
||||
* items were already present, these should also be set to "changed" and not
|
||||
* be inserted again.
|
||||
*
|
||||
* @param array $indexes
|
||||
* The SearchApiIndex objects for which item tracking should be initialized.
|
||||
*
|
||||
* @throws SearchApiDataSourceException
|
||||
* If any of the indexes doesn't use the same item type as this controller.
|
||||
*/
|
||||
public function startTracking(array $indexes) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop tracking of the index status of items for the given indexes.
|
||||
*
|
||||
* The tracking tables of the given indexes should be completely cleared.
|
||||
*
|
||||
* @param array $indexes
|
||||
* The SearchApiIndex objects for which item tracking should be stopped.
|
||||
*
|
||||
* @throws SearchApiDataSourceException
|
||||
* If any of the indexes doesn't use the same item type as this controller.
|
||||
*/
|
||||
public function stopTracking(array $indexes) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start tracking the index status for the given items on the given indexes.
|
||||
*
|
||||
* @param array $item_ids
|
||||
* The IDs of new items to track.
|
||||
* @param array $indexes
|
||||
* The indexes for which items should be tracked.
|
||||
*
|
||||
* @throws SearchApiDataSourceException
|
||||
* If any of the indexes doesn't use the same item type as this controller.
|
||||
*/
|
||||
public function trackItemInsert(array $item_ids, array $indexes) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the tracking status of the given items to "changed"/"dirty".
|
||||
*
|
||||
* @param $item_ids
|
||||
* Either an array with the IDs of the changed items. Or FALSE to mark all
|
||||
* items as changed for the given indexes.
|
||||
* @param array $indexes
|
||||
* The indexes for which the change should be tracked.
|
||||
* @param $dequeue
|
||||
* If set to TRUE, also change the status of queued items.
|
||||
*
|
||||
* @throws SearchApiDataSourceException
|
||||
* If any of the indexes doesn't use the same item type as this controller.
|
||||
*/
|
||||
public function trackItemChange($item_ids, array $indexes, $dequeue = FALSE) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the tracking status of the given items to "indexed".
|
||||
*
|
||||
* @param array $item_ids
|
||||
* The IDs of the indexed items.
|
||||
* @param SearchApiIndex $indexes
|
||||
* The index on which the items were indexed.
|
||||
*
|
||||
* @throws SearchApiDataSourceException
|
||||
* If the index doesn't use the same item type as this controller.
|
||||
*/
|
||||
public function trackItemIndexed(array $item_ids, SearchApiIndex $index) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop tracking the index status for the given items on the given indexes.
|
||||
*
|
||||
* @param array $item_ids
|
||||
* The IDs of the removed items.
|
||||
* @param array $indexes
|
||||
* The indexes for which the deletions should be tracked.
|
||||
*
|
||||
* @throws SearchApiDataSourceException
|
||||
* If any of the indexes doesn't use the same item type as this controller.
|
||||
*/
|
||||
public function trackItemDelete(array $item_ids, array $indexes) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of items that need to be indexed.
|
||||
*
|
||||
* If possible, completely unindexed items should be returned before items
|
||||
* that were indexed but later changed. Also, items that were changed longer
|
||||
* ago should be favored.
|
||||
*
|
||||
* @param SearchApiIndex $index
|
||||
* The index for which changed items should be returned.
|
||||
* @param $limit
|
||||
* The maximum number of items to return. Negative values mean "unlimited".
|
||||
*
|
||||
* @return array
|
||||
* The IDs of items that need to be indexed for the given index.
|
||||
*/
|
||||
public function getChangedItems(SearchApiIndex $index, $limit = -1) {
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get information on how many items have been indexed for a certain index.
|
||||
*
|
||||
* @param SearchApiIndex $index
|
||||
* The index whose index status should be returned.
|
||||
*
|
||||
* @return array
|
||||
* An associative array containing two keys (in this order):
|
||||
* - indexed: The number of items already indexed in their latest version.
|
||||
* - total: The total number of items that have to be indexed for this
|
||||
* index.
|
||||
*
|
||||
* @throws SearchApiDataSourceException
|
||||
* If the index doesn't use the same item type as this controller.
|
||||
*/
|
||||
public function getIndexStatus(SearchApiIndex $index) {
|
||||
return array(
|
||||
'indexed' => 0,
|
||||
'total' => 0,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains SearchApiException.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents an exception or error that occurred in some part of the Search API
|
||||
* framework.
|
||||
*/
|
||||
class SearchApiException extends Exception {
|
||||
|
||||
/**
|
||||
* Creates a new SearchApiException.
|
||||
*
|
||||
* @param $message
|
||||
* A string describing the cause of the exception.
|
||||
*/
|
||||
public function __construct($message = NULL) {
|
||||
if (!$message) {
|
||||
$message = t('An error occcurred in the Search API framework.');
|
||||
}
|
||||
parent::__construct($message);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an exception that occurred in a data source controller.
|
||||
*/
|
||||
class SearchApiDataSourceException extends SearchApiException {
|
||||
|
||||
}
|
@@ -0,0 +1,967 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains SearchApiIndex.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Class representing a search index.
|
||||
*/
|
||||
class SearchApiIndex extends Entity {
|
||||
|
||||
// Cache values, set when the corresponding methods are called for the first
|
||||
// time.
|
||||
|
||||
/**
|
||||
* Cached return value of datasource().
|
||||
*
|
||||
* @var SearchApiDataSourceControllerInterface
|
||||
*/
|
||||
protected $datasource = NULL;
|
||||
|
||||
/**
|
||||
* Cached return value of server().
|
||||
*
|
||||
* @var SearchApiServer
|
||||
*/
|
||||
protected $server_object = NULL;
|
||||
|
||||
/**
|
||||
* All enabled data alterations for this index.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $callbacks = NULL;
|
||||
|
||||
/**
|
||||
* All enabled processors for this index.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $processors = NULL;
|
||||
|
||||
/**
|
||||
* The properties added by data alterations on this index.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $added_properties = NULL;
|
||||
|
||||
/**
|
||||
* Static cache for the results of getFields().
|
||||
*
|
||||
* Can be accessed as follows: $this->fields[$only_indexed][$get_additional].
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fields = array();
|
||||
|
||||
/**
|
||||
* An array containing two arrays.
|
||||
*
|
||||
* At index 0, all fulltext fields of this index. At index 1, all indexed
|
||||
* fulltext fields of this index.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fulltext_fields = array();
|
||||
|
||||
// Database values that will be set when object is loaded.
|
||||
|
||||
/**
|
||||
* An integer identifying the index.
|
||||
* Immutable.
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
public $id;
|
||||
|
||||
/**
|
||||
* A name to be displayed for the index.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $name;
|
||||
|
||||
/**
|
||||
* The machine name of the index.
|
||||
* Immutable.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $machine_name;
|
||||
|
||||
/**
|
||||
* A string describing the index' use to users.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $description;
|
||||
|
||||
/**
|
||||
* The machine_name of the server with which data should be indexed.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $server;
|
||||
|
||||
/**
|
||||
* The type of items stored in this index.
|
||||
* Immutable.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $item_type;
|
||||
|
||||
/**
|
||||
* An array of options for configuring this index. The layout is as follows:
|
||||
* - cron_limit: The maximum number of items to be indexed per cron batch.
|
||||
* - index_directly: Boolean setting whether entities are indexed immediately
|
||||
* after they are created or updated.
|
||||
* - fields: An array of all indexed fields for this index. Keys are the field
|
||||
* identifiers, the values are arrays for specifying the field settings. The
|
||||
* structure of those arrays looks like this:
|
||||
* - type: The type set for this field. One of the types returned by
|
||||
* search_api_default_field_types().
|
||||
* - real_type: (optional) If a custom data type was selected for this
|
||||
* field, this type will be stored here, and "type" contain the fallback
|
||||
* default data type.
|
||||
* - boost: (optional) A boost value for terms found in this field during
|
||||
* searches. Usually only relevant for fulltext fields. Defaults to 1.0.
|
||||
* - entity_type (optional): If set, the type of this field is really an
|
||||
* entity. The "type" key will then just contain the primitive data type
|
||||
* of the ID field, meaning that servers will ignore this and merely index
|
||||
* the entity's ID. Components displaying this field, though, are advised
|
||||
* to use the entity label instead of the ID.
|
||||
* - additional fields: An associative array with keys and values being the
|
||||
* field identifiers of related entities whose fields should be displayed.
|
||||
* - data_alter_callbacks: An array of all data alterations available. Keys
|
||||
* are the alteration identifiers, the values are arrays containing the
|
||||
* settings for that data alteration. The inner structure looks like this:
|
||||
* - status: Boolean indicating whether the data alteration is enabled.
|
||||
* - weight: Used for sorting the data alterations.
|
||||
* - settings: Alteration-specific settings, configured via the alteration's
|
||||
* configuration form.
|
||||
* - processors: An array of all processors available for the index. The keys
|
||||
* are the processor identifiers, the values are arrays containing the
|
||||
* settings for that processor. The inner structure looks like this:
|
||||
* - status: Boolean indicating whether the processor is enabled.
|
||||
* - weight: Used for sorting the processors.
|
||||
* - settings: Processor-specific settings, configured via the processor's
|
||||
* configuration form.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $options = array();
|
||||
|
||||
/**
|
||||
* A flag indicating whether this index is enabled.
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
public $enabled = 1;
|
||||
|
||||
/**
|
||||
* A flag indicating whether to write to this index.
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
public $read_only = 0;
|
||||
|
||||
/**
|
||||
* Constructor as a helper to the parent constructor.
|
||||
*/
|
||||
public function __construct(array $values = array()) {
|
||||
parent::__construct($values, 'search_api_index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute necessary tasks for a newly created index.
|
||||
*/
|
||||
public function postCreate() {
|
||||
if ($this->enabled) {
|
||||
$this->queueItems();
|
||||
}
|
||||
if ($server = $this->server()) {
|
||||
// Tell the server about the new index.
|
||||
$server->addIndex($this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute necessary tasks when the index is removed from the database.
|
||||
*/
|
||||
public function postDelete() {
|
||||
if ($server = $this->server()) {
|
||||
$server->removeIndex($this);
|
||||
}
|
||||
|
||||
// Stop tracking entities for indexing.
|
||||
$this->dequeueItems();
|
||||
}
|
||||
|
||||
/**
|
||||
* Record entities to index.
|
||||
*/
|
||||
public function queueItems() {
|
||||
if (!$this->read_only) {
|
||||
$this->datasource()->startTracking(array($this));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all records of entities to index.
|
||||
*/
|
||||
public function dequeueItems() {
|
||||
$this->datasource()->stopTracking(array($this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves this index to the database.
|
||||
*
|
||||
* Either creates a new record or updates the existing one with the same ID.
|
||||
*
|
||||
* @return int|false
|
||||
* Failure to save the index will return FALSE. Otherwise, SAVED_NEW or
|
||||
* SAVED_UPDATED is returned depending on the operation performed. $this->id
|
||||
* will be set if a new index was inserted.
|
||||
*/
|
||||
public function save() {
|
||||
if (empty($this->description)) {
|
||||
$this->description = NULL;
|
||||
}
|
||||
if (empty($this->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;
|
||||
}
|
||||
|
||||
return parent::save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for updating entity properties.
|
||||
*
|
||||
* NOTE: You shouldn't change any properties of this object before calling
|
||||
* this method, as this might lead to the fields not being saved correctly.
|
||||
*
|
||||
* @param array $fields
|
||||
* The new field values.
|
||||
*
|
||||
* @return int|false
|
||||
* SAVE_UPDATED on success, FALSE on failure, 0 if the fields already had
|
||||
* the specified values.
|
||||
*/
|
||||
public function update(array $fields) {
|
||||
$changeable = array('name' => 1, 'enabled' => 1, 'description' => 1, 'server' => 1, 'options' => 1, 'read_only' => 1);
|
||||
$changed = FALSE;
|
||||
foreach ($fields as $field => $value) {
|
||||
if (isset($changeable[$field]) && $value !== $this->$field) {
|
||||
$this->$field = $value;
|
||||
$changed = TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
// If there are no new values, just return 0.
|
||||
if (!$changed) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Reset the index's internal property cache to correctly incorporate new
|
||||
// settings.
|
||||
$this->resetCaches();
|
||||
|
||||
return $this->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedules this search index for re-indexing.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE on success, FALSE on failure.
|
||||
*/
|
||||
public function reindex() {
|
||||
if (!$this->server || $this->read_only) {
|
||||
return TRUE;
|
||||
}
|
||||
_search_api_index_reindex($this);
|
||||
module_invoke_all('search_api_index_reindex', $this, FALSE);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears this search index and schedules all of its items for re-indexing.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE on success, FALSE on failure.
|
||||
*/
|
||||
public function clear() {
|
||||
if (!$this->server || $this->read_only) {
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
$this->server()->deleteItems('all', $this);
|
||||
|
||||
_search_api_index_reindex($this);
|
||||
module_invoke_all('search_api_index_reindex', $this, TRUE);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic method for determining which fields should be serialized.
|
||||
*
|
||||
* Don't serialize properties that are basically only caches.
|
||||
*
|
||||
* @return array
|
||||
* An array of properties to be serialized.
|
||||
*/
|
||||
public function __sleep() {
|
||||
$ret = get_object_vars($this);
|
||||
unset($ret['server_object'], $ret['datasource'], $ret['processors'], $ret['added_properties'], $ret['fulltext_fields']);
|
||||
return array_keys($ret);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the controller object of the data source used by this index.
|
||||
*
|
||||
* @throws SearchApiException
|
||||
* If the specified item type or data source doesn't exist or is invalid.
|
||||
*
|
||||
* @return SearchApiDataSourceControllerInterface
|
||||
* The data source controller for this index.
|
||||
*/
|
||||
public function datasource() {
|
||||
if (!isset($this->datasource)) {
|
||||
$this->datasource = search_api_get_datasource_controller($this->item_type);
|
||||
}
|
||||
return $this->datasource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entity type of items in this index.
|
||||
*
|
||||
* @return string|null
|
||||
* An entity type string if the items in this index are entities; NULL
|
||||
* otherwise.
|
||||
*/
|
||||
public function getEntityType() {
|
||||
return $this->datasource()->getEntityType();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the server this index lies on.
|
||||
*
|
||||
* @param $reset
|
||||
* Whether to reset the internal cache. Set to TRUE when the index' $server
|
||||
* property has just changed.
|
||||
*
|
||||
* @throws SearchApiException
|
||||
* If $this->server is set, but no server with that machine name exists.
|
||||
*
|
||||
* @return SearchApiServer
|
||||
* The server associated with this index, or NULL if this index currently
|
||||
* doesn't lie on a server.
|
||||
*/
|
||||
public function server($reset = FALSE) {
|
||||
if (!isset($this->server_object) || $reset) {
|
||||
$this->server_object = $this->server ? search_api_server_load($this->server) : FALSE;
|
||||
if ($this->server && !$this->server_object) {
|
||||
throw new SearchApiException(t('Unknown server @server specified for index @name.', array('@server' => $this->server, '@name' => $this->machine_name)));
|
||||
}
|
||||
}
|
||||
return $this->server_object ? $this->server_object : NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a query object for this index.
|
||||
*
|
||||
* @param $options
|
||||
* Associative array of options configuring this query. See
|
||||
* SearchApiQueryInterface::__construct().
|
||||
*
|
||||
* @throws SearchApiException
|
||||
* If the index is currently disabled.
|
||||
*
|
||||
* @return SearchApiQueryInterface
|
||||
* A query object for searching this index.
|
||||
*/
|
||||
public function query($options = array()) {
|
||||
if (!$this->enabled) {
|
||||
throw new SearchApiException(t('Cannot search on a disabled index.'));
|
||||
}
|
||||
return $this->server()->query($this, $options);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @return array
|
||||
* An array of the IDs of all items that should be marked as indexed.
|
||||
*/
|
||||
public function index(array $items) {
|
||||
if ($this->read_only) {
|
||||
return array();
|
||||
}
|
||||
if (!$this->enabled) {
|
||||
throw new SearchApiException(t("Couldn't index values on '@name' index (index is disabled)", array('@name' => $this->name)));
|
||||
}
|
||||
if (empty($this->options['fields'])) {
|
||||
throw new SearchApiException(t("Couldn't index values on '@name' index (no fields selected)", array('@name' => $this->name)));
|
||||
}
|
||||
$fields = $this->options['fields'];
|
||||
$custom_type_fields = array();
|
||||
foreach ($fields as $field => $info) {
|
||||
if (isset($info['real_type'])) {
|
||||
$custom_type = search_api_extract_inner_type($info['real_type']);
|
||||
if ($this->server()->supportsFeature('search_api_data_type_' . $custom_type)) {
|
||||
$fields[$field]['type'] = $info['real_type'];
|
||||
$custom_type_fields[$custom_type][$field] = search_api_list_nesting_level($info['real_type']);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (empty($fields)) {
|
||||
throw new SearchApiException(t("Couldn't index values on '@name' index (no fields selected)", array('@name' => $this->name)));
|
||||
}
|
||||
|
||||
// Mark all items that are rejected as indexed.
|
||||
$ret = array_keys($items);
|
||||
drupal_alter('search_api_index_items', $items, $this);
|
||||
if ($items) {
|
||||
$this->dataAlter($items);
|
||||
}
|
||||
$ret = array_diff($ret, array_keys($items));
|
||||
|
||||
// Items that are rejected should also be deleted from the server.
|
||||
if ($ret) {
|
||||
$this->server()->deleteItems($ret, $this);
|
||||
}
|
||||
if (!$items) {
|
||||
return $ret;
|
||||
}
|
||||
|
||||
$data = array();
|
||||
foreach ($items as $id => $item) {
|
||||
$data[$id] = search_api_extract_fields($this->entityWrapper($item), $fields);
|
||||
unset($items[$id]);
|
||||
foreach ($custom_type_fields as $type => $type_fields) {
|
||||
$info = search_api_get_data_type_info($type);
|
||||
if (isset($info['conversion callback']) && is_callable($info['conversion callback'])) {
|
||||
$callback = $info['conversion callback'];
|
||||
foreach ($type_fields as $field => $nesting_level) {
|
||||
if (isset($data[$id][$field]['value'])) {
|
||||
$value = $data[$id][$field]['value'];
|
||||
$original_type = $data[$id][$field]['original_type'];
|
||||
$data[$id][$field]['value'] = _search_api_convert_custom_type($callback, $value, $original_type, $type, $nesting_level);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->preprocessIndexItems($data);
|
||||
|
||||
return array_merge($ret, $this->server()->indexItems($this, $data));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls data alteration hooks for a set of items, according to the index
|
||||
* options.
|
||||
*
|
||||
* @param array $items
|
||||
* An array of items to be altered.
|
||||
*
|
||||
* @return SearchApiIndex
|
||||
* The called object.
|
||||
*/
|
||||
public function dataAlter(array &$items) {
|
||||
// First, execute our own search_api_language data alteration.
|
||||
foreach ($items as &$item) {
|
||||
$item->search_api_language = isset($item->language) ? $item->language : LANGUAGE_NONE;
|
||||
}
|
||||
|
||||
foreach ($this->getAlterCallbacks() as $callback) {
|
||||
$callback->alterItems($items);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Property info alter callback that adds the infos of the properties added by
|
||||
* data alter callbacks.
|
||||
*
|
||||
* @param EntityMetadataWrapper $wrapper
|
||||
* The wrapped data.
|
||||
* @param $property_info
|
||||
* The original property info.
|
||||
*
|
||||
* @return array
|
||||
* The altered property info.
|
||||
*/
|
||||
public function propertyInfoAlter(EntityMetadataWrapper $wrapper, array $property_info) {
|
||||
if (entity_get_property_info($wrapper->type())) {
|
||||
// Overwrite the existing properties with the list of properties including
|
||||
// all fields regardless of the used bundle.
|
||||
$property_info['properties'] = entity_get_all_property_info($wrapper->type());
|
||||
}
|
||||
|
||||
if (!isset($this->added_properties)) {
|
||||
$this->added_properties = array(
|
||||
'search_api_language' => array(
|
||||
'label' => t('Item language'),
|
||||
'description' => t("A field added by the search framework to let components determine an item's language. Is always indexed."),
|
||||
'type' => 'token',
|
||||
'options list' => 'entity_metadata_language_list',
|
||||
),
|
||||
);
|
||||
// We use the reverse order here so the hierarchy for overwriting property
|
||||
// infos is the same as for actually overwriting the properties.
|
||||
foreach (array_reverse($this->getAlterCallbacks()) as $callback) {
|
||||
$props = $callback->propertyInfo();
|
||||
if ($props) {
|
||||
$this->added_properties += $props;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Let fields added by data-alter callbacks override default fields.
|
||||
$property_info['properties'] = array_merge($property_info['properties'], $this->added_properties);
|
||||
|
||||
return $property_info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads all enabled data alterations for this index in proper order.
|
||||
*
|
||||
* @return array
|
||||
* All enabled callbacks for this index, as SearchApiAlterCallbackInterface
|
||||
* objects.
|
||||
*/
|
||||
public function getAlterCallbacks() {
|
||||
if (isset($this->callbacks)) {
|
||||
return $this->callbacks;
|
||||
}
|
||||
|
||||
$this->callbacks = array();
|
||||
if (empty($this->options['data_alter_callbacks'])) {
|
||||
return $this->callbacks;
|
||||
}
|
||||
$callback_settings = $this->options['data_alter_callbacks'];
|
||||
$infos = search_api_get_alter_callbacks();
|
||||
|
||||
foreach ($callback_settings as $id => $settings) {
|
||||
if (empty($settings['status'])) {
|
||||
continue;
|
||||
}
|
||||
if (empty($infos[$id]) || !class_exists($infos[$id]['class'])) {
|
||||
watchdog('search_api', t('Undefined data alteration @class specified in index @name', array('@class' => $id, '@name' => $this->name)), NULL, WATCHDOG_WARNING);
|
||||
continue;
|
||||
}
|
||||
$class = $infos[$id]['class'];
|
||||
$callback = new $class($this, empty($settings['settings']) ? array() : $settings['settings']);
|
||||
if (!($callback instanceof SearchApiAlterCallbackInterface)) {
|
||||
watchdog('search_api', t('Unknown callback class @class specified for data alteration @name', array('@class' => $class, '@name' => $id)), NULL, WATCHDOG_WARNING);
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->callbacks[$id] = $callback;
|
||||
}
|
||||
return $this->callbacks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads all enabled processors for this index in proper order.
|
||||
*
|
||||
* @return array
|
||||
* All enabled processors for this index, as SearchApiProcessorInterface
|
||||
* objects.
|
||||
*/
|
||||
public function getProcessors() {
|
||||
if (isset($this->processors)) {
|
||||
return $this->processors;
|
||||
}
|
||||
|
||||
$this->processors = array();
|
||||
if (empty($this->options['processors'])) {
|
||||
return $this->processors;
|
||||
}
|
||||
$processor_settings = $this->options['processors'];
|
||||
$infos = search_api_get_processors();
|
||||
|
||||
foreach ($processor_settings as $id => $settings) {
|
||||
if (empty($settings['status'])) {
|
||||
continue;
|
||||
}
|
||||
if (empty($infos[$id]) || !class_exists($infos[$id]['class'])) {
|
||||
watchdog('search_api', t('Undefined processor @class specified in index @name', array('@class' => $id, '@name' => $this->name)), NULL, WATCHDOG_WARNING);
|
||||
continue;
|
||||
}
|
||||
$class = $infos[$id]['class'];
|
||||
$processor = new $class($this, isset($settings['settings']) ? $settings['settings'] : array());
|
||||
if (!($processor instanceof SearchApiProcessorInterface)) {
|
||||
watchdog('search_api', t('Unknown processor class @class specified for processor @name', array('@class' => $class, '@name' => $id)), NULL, WATCHDOG_WARNING);
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->processors[$id] = $processor;
|
||||
}
|
||||
return $this->processors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Preprocess data items for indexing. Data added by data alter callbacks will
|
||||
* be available on the items.
|
||||
*
|
||||
* Typically, a preprocessor will execute its preprocessing (e.g. stemming,
|
||||
* n-grams, word splitting, stripping stop words, etc.) only on the items'
|
||||
* fulltext fields. Other fields should usually be left untouched.
|
||||
*
|
||||
* @param array $items
|
||||
* An array of items to be preprocessed for indexing.
|
||||
*
|
||||
* @return SearchApiIndex
|
||||
* The called object.
|
||||
*/
|
||||
public function preprocessIndexItems(array &$items) {
|
||||
foreach ($this->getProcessors() as $processor) {
|
||||
$processor->preprocessIndexItems($items);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Preprocess a search query.
|
||||
*
|
||||
* The same applies as when preprocessing indexed items: typically, only the
|
||||
* fulltext search keys should be processed, queries on specific fields should
|
||||
* usually not be altered.
|
||||
*
|
||||
* @param SearchApiQuery $query
|
||||
* The object representing the query to be executed.
|
||||
*
|
||||
* @return SearchApiIndex
|
||||
* The called object.
|
||||
*/
|
||||
public function preprocessSearchQuery(SearchApiQuery $query) {
|
||||
foreach ($this->getProcessors() as $processor) {
|
||||
$processor->preprocessSearchQuery($query);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Postprocess search results before display.
|
||||
*
|
||||
* If a class is used for both pre- and post-processing a search query, the
|
||||
* same object will be used for both calls (so preserving some data or state
|
||||
* locally is possible).
|
||||
*
|
||||
* @param array $response
|
||||
* An array containing the search results. See
|
||||
* SearchApiServiceInterface->search() for the detailed format.
|
||||
* @param SearchApiQuery $query
|
||||
* The object representing the executed query.
|
||||
*
|
||||
* @return SearchApiIndex
|
||||
* The called object.
|
||||
*/
|
||||
public function postprocessSearchResults(array &$response, SearchApiQuery $query) {
|
||||
// Postprocessing is done in exactly the opposite direction than preprocessing.
|
||||
foreach (array_reverse($this->getProcessors()) as $processor) {
|
||||
$processor->postprocessSearchResults($response, $query);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all known fields for this index.
|
||||
*
|
||||
* @param $only_indexed (optional)
|
||||
* Return only indexed fields, not all known fields. Defaults to TRUE.
|
||||
* @param $get_additional (optional)
|
||||
* Return not only known/indexed fields, but also related entities whose
|
||||
* fields could additionally be added to the index.
|
||||
*
|
||||
* @return array
|
||||
* An array of all known fields for this index. Keys are the field
|
||||
* identifiers, the values are arrays for specifying the field settings. The
|
||||
* structure of those arrays looks like this:
|
||||
* - name: The human-readable name for the field.
|
||||
* - description: A description of the field, if available.
|
||||
* - indexed: Boolean indicating whether the field is indexed or not.
|
||||
* - type: The type set for this field. One of the types returned by
|
||||
* search_api_default_field_types().
|
||||
* - real_type: (optional) If a custom data type was selected for this
|
||||
* field, this type will be stored here, and "type" contain the fallback
|
||||
* default data type.
|
||||
* - boost: A boost value for terms found in this field during searches.
|
||||
* Usually only relevant for fulltext fields.
|
||||
* - entity_type (optional): If set, the type of this field is really an
|
||||
* entity. The "type" key will then contain "integer", meaning that
|
||||
* servers will ignore this and merely index the entity's ID. Components
|
||||
* displaying this field, though, are advised to use the entity label
|
||||
* instead of the ID.
|
||||
* If $get_additional is TRUE, this array is encapsulated in another
|
||||
* associative array, which contains the above array under the "fields" key,
|
||||
* and a list of related entities (field keys mapped to names) under the
|
||||
* "additional fields" key.
|
||||
*/
|
||||
public function getFields($only_indexed = TRUE, $get_additional = FALSE) {
|
||||
$only_indexed = $only_indexed ? 1 : 0;
|
||||
$get_additional = $get_additional ? 1 : 0;
|
||||
|
||||
// First, try the static cache and the persistent cache bin.
|
||||
if (empty($this->fields[$only_indexed][$get_additional])) {
|
||||
$cid = $this->getCacheId() . "-$only_indexed-$get_additional";
|
||||
$cache = cache_get($cid);
|
||||
if ($cache) {
|
||||
$this->fields[$only_indexed][$get_additional] = $cache->data;
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, we have to compute the result.
|
||||
if (empty($this->fields[$only_indexed][$get_additional])) {
|
||||
$fields = empty($this->options['fields']) ? array() : $this->options['fields'];
|
||||
$wrapper = $this->entityWrapper();
|
||||
$additional = array();
|
||||
$entity_types = entity_get_info();
|
||||
|
||||
// First we need all already added prefixes.
|
||||
$added = ($only_indexed || empty($this->options['additional fields'])) ? array() : $this->options['additional fields'];
|
||||
foreach (array_keys($fields) as $key) {
|
||||
$len = strlen($key) + 1;
|
||||
$pos = $len;
|
||||
// The third parameter ($offset) to strrpos has rather weird behaviour,
|
||||
// necessitating this rather awkward code. It will iterate over all
|
||||
// prefixes of each field, beginning with the longest, adding all of them
|
||||
// to $added until one is encountered that was already added (which means
|
||||
// all shorter ones will have already been added, too).
|
||||
while ($pos = strrpos($key, ':', $pos - $len)) {
|
||||
$prefix = substr($key, 0, $pos);
|
||||
if (isset($added[$prefix])) {
|
||||
break;
|
||||
}
|
||||
$added[$prefix] = $prefix;
|
||||
}
|
||||
}
|
||||
|
||||
// Then we walk through all properties and look if they are already
|
||||
// contained in one of the arrays.
|
||||
// Since this uses an iterative instead of a recursive approach, it is a bit
|
||||
// complicated, with three arrays tracking the current depth.
|
||||
|
||||
// A wrapper for a specific field name prefix, e.g. 'user:' mapped to the user wrapper
|
||||
$wrappers = array('' => $wrapper);
|
||||
// Display names for the prefixes
|
||||
$prefix_names = array('' => '');
|
||||
// The list nesting level for entities with a certain prefix
|
||||
$nesting_levels = array('' => 0);
|
||||
|
||||
$types = search_api_default_field_types();
|
||||
$flat = array();
|
||||
while ($wrappers) {
|
||||
foreach ($wrappers as $prefix => $wrapper) {
|
||||
$prefix_name = $prefix_names[$prefix];
|
||||
// Deal with lists of entities.
|
||||
$nesting_level = $nesting_levels[$prefix];
|
||||
$type_prefix = str_repeat('list<', $nesting_level);
|
||||
$type_suffix = str_repeat('>', $nesting_level);
|
||||
if ($nesting_level) {
|
||||
$info = $wrapper->info();
|
||||
// The real nesting level of the wrapper, not the accumulated one.
|
||||
$level = search_api_list_nesting_level($info['type']);
|
||||
for ($i = 0; $i < $level; ++$i) {
|
||||
$wrapper = $wrapper[0];
|
||||
}
|
||||
}
|
||||
// Now look at all properties.
|
||||
foreach ($wrapper as $property => $value) {
|
||||
$info = $value->info();
|
||||
// We hide the complexity of multi-valued types from the user here.
|
||||
$type = search_api_extract_inner_type($info['type']);
|
||||
// Treat Entity API type "token" as our "string" type.
|
||||
// Also let text fields with limited options be of type "string" by default.
|
||||
if ($type == 'token' || ($type == 'text' && !empty($info['options list']))) {
|
||||
// Inner type is changed to "string".
|
||||
$type = 'string';
|
||||
// Set the field type accordingly.
|
||||
$info['type'] = search_api_nest_type('string', $info['type']);
|
||||
}
|
||||
$info['type'] = $type_prefix . $info['type'] . $type_suffix;
|
||||
$key = $prefix . $property;
|
||||
if ((isset($types[$type]) || isset($entity_types[$type])) && (!$only_indexed || !empty($fields[$key]))) {
|
||||
if (!empty($fields[$key])) {
|
||||
// This field is already known in the index configuration.
|
||||
$flat[$key] = $fields[$key] + array(
|
||||
'name' => $prefix_name . $info['label'],
|
||||
'description' => empty($info['description']) ? NULL : $info['description'],
|
||||
'boost' => '1.0',
|
||||
'indexed' => TRUE,
|
||||
);
|
||||
// Update the type and its nesting level for non-entity properties.
|
||||
if (!isset($entity_types[$type])) {
|
||||
$flat[$key]['type'] = search_api_nest_type(search_api_extract_inner_type($flat[$key]['type']), $info['type']);
|
||||
if (isset($flat[$key]['real_type'])) {
|
||||
$real_type = search_api_extract_inner_type($flat[$key]['real_type']);
|
||||
$flat[$key]['real_type'] = search_api_nest_type($real_type, $info['type']);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
$flat[$key] = array(
|
||||
'name' => $prefix_name . $info['label'],
|
||||
'description' => empty($info['description']) ? NULL : $info['description'],
|
||||
'type' => $info['type'],
|
||||
'boost' => '1.0',
|
||||
'indexed' => FALSE,
|
||||
);
|
||||
}
|
||||
if (isset($entity_types[$type])) {
|
||||
$base_type = isset($entity_types[$type]['entity keys']['name']) ? 'string' : 'integer';
|
||||
$flat[$key]['type'] = search_api_nest_type($base_type, $info['type']);
|
||||
$flat[$key]['entity_type'] = $type;
|
||||
}
|
||||
}
|
||||
if (empty($types[$type])) {
|
||||
if (isset($added[$key])) {
|
||||
// Visit this entity/struct in a later iteration.
|
||||
$wrappers[$key . ':'] = $value;
|
||||
$prefix_names[$key . ':'] = $prefix_name . $info['label'] . ' » ';
|
||||
$nesting_levels[$key . ':'] = search_api_list_nesting_level($info['type']);
|
||||
}
|
||||
else {
|
||||
$name = $prefix_name . $info['label'];
|
||||
// Add machine names to discern fields with identical labels.
|
||||
if (isset($used_names[$name])) {
|
||||
if ($used_names[$name] !== FALSE) {
|
||||
$additional[$used_names[$name]] .= ' [' . $used_names[$name] . ']';
|
||||
$used_names[$name] = FALSE;
|
||||
}
|
||||
$name .= ' [' . $key . ']';
|
||||
}
|
||||
$additional[$key] = $name;
|
||||
$used_names[$name] = $key;
|
||||
}
|
||||
}
|
||||
}
|
||||
unset($wrappers[$prefix]);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$get_additional) {
|
||||
$this->fields[$only_indexed][$get_additional] = $flat;
|
||||
}
|
||||
else {
|
||||
$options = array();
|
||||
$options['fields'] = $flat;
|
||||
$options['additional fields'] = $additional;
|
||||
$this->fields[$only_indexed][$get_additional] = $options;
|
||||
}
|
||||
cache_set($cid, $this->fields[$only_indexed][$get_additional]);
|
||||
}
|
||||
|
||||
return $this->fields[$only_indexed][$get_additional];
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method for getting all of this index's fulltext fields.
|
||||
*
|
||||
* @param boolean $only_indexed
|
||||
* If set to TRUE, only the indexed fulltext fields will be returned.
|
||||
*
|
||||
* @return array
|
||||
* An array containing all (or all indexed) fulltext fields defined for this
|
||||
* index.
|
||||
*/
|
||||
public function getFulltextFields($only_indexed = TRUE) {
|
||||
$i = $only_indexed ? 1 : 0;
|
||||
if (!isset($this->fulltext_fields[$i])) {
|
||||
$this->fulltext_fields[$i] = array();
|
||||
$fields = $only_indexed ? $this->options['fields'] : $this->getFields(FALSE);
|
||||
foreach ($fields as $key => $field) {
|
||||
if (search_api_is_text_type($field['type'])) {
|
||||
$this->fulltext_fields[$i][] = $key;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $this->fulltext_fields[$i];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cache ID prefix used for this index's caches.
|
||||
*
|
||||
* @param $type
|
||||
* The type of cache. Currently only "fields" is used.
|
||||
*
|
||||
* @return
|
||||
* The cache ID (prefix) for this index's caches.
|
||||
*/
|
||||
public function getCacheId($type = 'fields') {
|
||||
return 'search_api:index-' . $this->machine_name . '--' . $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for creating an entity metadata wrapper appropriate for
|
||||
* this index.
|
||||
*
|
||||
* @param $item
|
||||
* Unless NULL, an item of this index's item type which should be wrapped.
|
||||
* @param $alter
|
||||
* Whether to apply the index's active data alterations on the property
|
||||
* information used. To also apply the data alteration to the wrapped item,
|
||||
* execute SearchApiIndex::dataAlter() on it before calling this method.
|
||||
*
|
||||
* @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.
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to load items from the type lying on this index.
|
||||
*
|
||||
* @param array $ids
|
||||
* The IDs of the items to load.
|
||||
*
|
||||
* @return array
|
||||
* The requested items, as loaded by the data source.
|
||||
*
|
||||
* @see SearchApiDataSourceControllerInterface::loadItems()
|
||||
*/
|
||||
public function loadItems(array $ids) {
|
||||
return $this->datasource()->loadItems($ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset internal static caches.
|
||||
*
|
||||
* Should be used when things like fields or data alterations change to avoid
|
||||
* using stale data.
|
||||
*/
|
||||
public function resetCaches() {
|
||||
$this->datasource = NULL;
|
||||
$this->server_object = NULL;
|
||||
$this->callbacks = NULL;
|
||||
$this->processors = NULL;
|
||||
$this->added_properties = NULL;
|
||||
$this->fields = array();
|
||||
$this->fulltext_fields = array();
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,465 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains SearchApiProcessorInterface and SearchApiAbstractProcessor.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Interface representing a Search API pre- and/or post-processor.
|
||||
*
|
||||
* While processors are enabled or disabled for both pre- and postprocessing at
|
||||
* once, many processors will only need to run in one of those two phases. Then,
|
||||
* the other method(s) should simply be left blank. A processor should make it
|
||||
* clear in its description or documentation when it will run and what effect it
|
||||
* will have.
|
||||
* Usually, processors preprocessing indexed items will likewise preprocess
|
||||
* search queries, so these two methods should mostly be implemented either both
|
||||
* or neither.
|
||||
*/
|
||||
interface SearchApiProcessorInterface {
|
||||
|
||||
/**
|
||||
* Construct a processor.
|
||||
*
|
||||
* @param SearchApiIndex $index
|
||||
* The index for which processing is done.
|
||||
* @param array $options
|
||||
* The processor options set for this index.
|
||||
*/
|
||||
public function __construct(SearchApiIndex $index, array $options = array());
|
||||
|
||||
/**
|
||||
* Check whether this processor is applicable for a certain index.
|
||||
*
|
||||
* This can be used for hiding the processor on the index's "Filters" tab. To
|
||||
* avoid confusion, you should only use criteria that are immutable, such as
|
||||
* the index's item type. Also, since this is only used for UI purposes, you
|
||||
* should not completely rely on this to ensure certain index configurations
|
||||
* and at least throw an exception with a descriptive error message if this is
|
||||
* violated on runtime.
|
||||
*
|
||||
* @param SearchApiIndex $index
|
||||
* The index to check for.
|
||||
*
|
||||
* @return boolean
|
||||
* TRUE if the processor can run on the given index; FALSE otherwise.
|
||||
*/
|
||||
public function supportsIndex(SearchApiIndex $index);
|
||||
|
||||
/**
|
||||
* Display a form for configuring this processor.
|
||||
* Since forcing users to specify options for disabled processors makes no
|
||||
* sense, none of the form elements should have the '#required' attribute set.
|
||||
*
|
||||
* @return array
|
||||
* A form array for configuring this processor, or FALSE if no configuration
|
||||
* is possible.
|
||||
*/
|
||||
public function configurationForm();
|
||||
|
||||
/**
|
||||
* Validation callback for the form returned by configurationForm().
|
||||
*
|
||||
* @param array $form
|
||||
* The form returned by configurationForm().
|
||||
* @param array $values
|
||||
* The part of the $form_state['values'] array corresponding to this form.
|
||||
* @param array $form_state
|
||||
* The complete form state.
|
||||
*/
|
||||
public function configurationFormValidate(array $form, array &$values, array &$form_state);
|
||||
|
||||
/**
|
||||
* Submit callback for the form returned by configurationForm().
|
||||
*
|
||||
* This method should both return the new options and set them internally.
|
||||
*
|
||||
* @param array $form
|
||||
* The form returned by configurationForm().
|
||||
* @param array $values
|
||||
* The part of the $form_state['values'] array corresponding to this form.
|
||||
* @param array $form_state
|
||||
* The complete form state.
|
||||
*
|
||||
* @return array
|
||||
* The new options array for this callback.
|
||||
*/
|
||||
public function configurationFormSubmit(array $form, array &$values, array &$form_state);
|
||||
|
||||
/**
|
||||
* Preprocess data items for indexing.
|
||||
*
|
||||
* Typically, a preprocessor will execute its preprocessing (e.g. stemming,
|
||||
* n-grams, word splitting, stripping stop words, etc.) only on the items'
|
||||
* search_api_fulltext fields, if set. Other fields should usually be left
|
||||
* untouched.
|
||||
*
|
||||
* @param array $items
|
||||
* An array of items to be preprocessed for indexing, formatted as specified
|
||||
* by SearchApiServiceInterface::indexItems().
|
||||
*/
|
||||
public function preprocessIndexItems(array &$items);
|
||||
|
||||
/**
|
||||
* Preprocess a search query.
|
||||
*
|
||||
* The same applies as when preprocessing indexed items: typically, only the
|
||||
* fulltext search keys should be processed, queries on specific fields should
|
||||
* usually not be altered.
|
||||
*
|
||||
* @param SearchApiQuery $query
|
||||
* The object representing the query to be executed.
|
||||
*/
|
||||
public function preprocessSearchQuery(SearchApiQuery $query);
|
||||
|
||||
/**
|
||||
* Postprocess search results before display.
|
||||
*
|
||||
* If a class is used for both pre- and post-processing a search query, the
|
||||
* same object will be used for both calls (so preserving some data or state
|
||||
* locally is possible).
|
||||
*
|
||||
* @param array $response
|
||||
* An array containing the search results. See the return value of
|
||||
* SearchApiQueryInterface->execute() for the detailed format.
|
||||
* @param SearchApiQuery $query
|
||||
* The object representing the executed query.
|
||||
*/
|
||||
public function postprocessSearchResults(array &$response, SearchApiQuery $query);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract processor implementation that provides an easy framework for only
|
||||
* processing specific fields.
|
||||
*
|
||||
* Simple processors can just override process(), while others might want to
|
||||
* override the other process*() methods, and test*() (for restricting
|
||||
* processing to something other than all fulltext data).
|
||||
*/
|
||||
abstract class SearchApiAbstractProcessor implements SearchApiProcessorInterface {
|
||||
|
||||
/**
|
||||
* @var SearchApiIndex
|
||||
*/
|
||||
protected $index;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $options;
|
||||
|
||||
/**
|
||||
* Constructor, saving its arguments into properties.
|
||||
*/
|
||||
public function __construct(SearchApiIndex $index, array $options = array()) {
|
||||
$this->index = $index;
|
||||
$this->options = $options;
|
||||
}
|
||||
|
||||
public function supportsIndex(SearchApiIndex $index) {
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
public function configurationForm() {
|
||||
$form['#attached']['css'][] = drupal_get_path('module', 'search_api') . '/search_api.admin.css';
|
||||
|
||||
$fields = $this->index->getFields();
|
||||
$field_options = array();
|
||||
$default_fields = array();
|
||||
if (isset($this->options['fields'])) {
|
||||
$default_fields = drupal_map_assoc(array_keys($this->options['fields']));
|
||||
}
|
||||
foreach ($fields as $name => $field) {
|
||||
$field_options[$name] = $field['name'];
|
||||
if (!empty($default_fields[$name]) || (!isset($this->options['fields']) && $this->testField($name, $field))) {
|
||||
$default_fields[$name] = $name;
|
||||
}
|
||||
}
|
||||
|
||||
$form['fields'] = array(
|
||||
'#type' => 'checkboxes',
|
||||
'#title' => t('Fields to run on'),
|
||||
'#options' => $field_options,
|
||||
'#default_value' => $default_fields,
|
||||
'#attributes' => array('class' => array('search-api-checkboxes-list')),
|
||||
);
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
public function configurationFormValidate(array $form, array &$values, array &$form_state) {
|
||||
$fields = array_filter($values['fields']);
|
||||
if ($fields) {
|
||||
$fields = array_fill_keys($fields, TRUE);
|
||||
}
|
||||
$values['fields'] = $fields;
|
||||
}
|
||||
|
||||
public function configurationFormSubmit(array $form, array &$values, array &$form_state) {
|
||||
$this->options = $values;
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls processField() for all appropriate fields.
|
||||
*/
|
||||
public function preprocessIndexItems(array &$items) {
|
||||
foreach ($items as &$item) {
|
||||
foreach ($item as $name => &$field) {
|
||||
if ($this->testField($name, $field)) {
|
||||
$this->processField($field['value'], $field['type']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls processKeys() for the keys and processFilters() for the filters.
|
||||
*/
|
||||
public function preprocessSearchQuery(SearchApiQuery $query) {
|
||||
$keys = &$query->getKeys();
|
||||
$this->processKeys($keys);
|
||||
$filter = $query->getFilter();
|
||||
$filters = &$filter->getFilters();
|
||||
$this->processFilters($filters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Does nothing.
|
||||
*/
|
||||
public function postprocessSearchResults(array &$response, SearchApiQuery $query) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method for preprocessing field data.
|
||||
*
|
||||
* Calls process() either for the whole text, or each token, depending on the
|
||||
* type. Also takes care of extracting list values and of fusing returned
|
||||
* tokens back into a one-dimensional array.
|
||||
*/
|
||||
protected function processField(&$value, &$type) {
|
||||
if (!isset($value) || $value === '') {
|
||||
return;
|
||||
}
|
||||
if (substr($type, 0, 5) == 'list<') {
|
||||
$inner_type = $t = $t1 = substr($type, 5, -1);
|
||||
foreach ($value as &$v) {
|
||||
$t1 = $inner_type;
|
||||
$this->processField($v, $t1);
|
||||
// If one value got tokenized, all others have to follow.
|
||||
if ($t1 != $inner_type) {
|
||||
$t = $t1;
|
||||
}
|
||||
}
|
||||
if ($t == 'tokens') {
|
||||
foreach ($value as $i => &$v) {
|
||||
if (!$v) {
|
||||
unset($value[$i]);
|
||||
continue;
|
||||
}
|
||||
if (!is_array($v)) {
|
||||
$v = array(array('value' => $v, 'score' => 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
$type = "list<$t>";
|
||||
return;
|
||||
}
|
||||
if ($type == 'tokens') {
|
||||
foreach ($value as &$token) {
|
||||
$this->processFieldValue($token['value']);
|
||||
}
|
||||
}
|
||||
else {
|
||||
$this->processFieldValue($value);
|
||||
}
|
||||
if (is_array($value)) {
|
||||
// Don't tokenize non-fulltext content!
|
||||
if (in_array($type, array('text', 'tokens'))) {
|
||||
$type = 'tokens';
|
||||
$value = $this->normalizeTokens($value);
|
||||
}
|
||||
else {
|
||||
$value = $this->implodeTokens($value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal helper function for normalizing tokens.
|
||||
*/
|
||||
protected function normalizeTokens($tokens, $score = 1) {
|
||||
$ret = array();
|
||||
foreach ($tokens as $token) {
|
||||
if (empty($token['value']) && !is_numeric($token['value'])) {
|
||||
// Filter out empty tokens.
|
||||
continue;
|
||||
}
|
||||
if (!isset($token['score'])) {
|
||||
$token['score'] = $score;
|
||||
}
|
||||
else {
|
||||
$token['score'] *= $score;
|
||||
}
|
||||
if (is_array($token['value'])) {
|
||||
foreach ($this->normalizeTokens($token['value'], $token['score']) as $t) {
|
||||
$ret[] = $t;
|
||||
}
|
||||
}
|
||||
else {
|
||||
$ret[] = $token;
|
||||
}
|
||||
}
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal helper function for imploding tokens into a single string.
|
||||
*
|
||||
* @param array $tokens
|
||||
* The tokens array to implode.
|
||||
*
|
||||
* @return string
|
||||
* The text data from the tokens concatenated into a single string.
|
||||
*/
|
||||
protected function implodeTokens(array $tokens) {
|
||||
$ret = array();
|
||||
foreach ($tokens as $token) {
|
||||
if (empty($token['value']) && !is_numeric($token['value'])) {
|
||||
// Filter out empty tokens.
|
||||
continue;
|
||||
}
|
||||
if (is_array($token['value'])) {
|
||||
$ret[] = $this->implodeTokens($token['value']);
|
||||
}
|
||||
else {
|
||||
$ret[] = $token['value'];
|
||||
}
|
||||
}
|
||||
return implode(' ', $ret);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method for preprocessing search keys.
|
||||
*/
|
||||
protected function processKeys(&$keys) {
|
||||
if (is_array($keys)) {
|
||||
foreach ($keys as $key => &$v) {
|
||||
if (element_child($key)) {
|
||||
$this->processKeys($v);
|
||||
if (!$v && !is_numeric($v)) {
|
||||
unset($keys[$key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
$this->processKey($keys);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method for preprocessing query filters.
|
||||
*/
|
||||
protected function processFilters(array &$filters) {
|
||||
$fields = $this->index->options['fields'];
|
||||
foreach ($filters as $key => &$f) {
|
||||
if (is_array($f)) {
|
||||
if (isset($fields[$f[0]]) && $this->testField($f[0], $fields[$f[0]])) {
|
||||
// We want to allow processors also to easily remove complete filters.
|
||||
// However, we can't use empty() or the like, as that would sort out
|
||||
// filters for 0 or NULL. So we specifically check only for the empty
|
||||
// string, and we also make sure the filter value was actually changed
|
||||
// by storing whether it was empty before.
|
||||
$empty_string = $f[1] === '';
|
||||
$this->processFilterValue($f[1]);
|
||||
|
||||
if ($f[1] === '' && !$empty_string) {
|
||||
unset($filters[$key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
$child_filters = &$f->getFilters();
|
||||
$this->processFilters($child_filters);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $name
|
||||
* The field's machine name.
|
||||
* @param array $field
|
||||
* The field's information.
|
||||
*
|
||||
* @return
|
||||
* TRUE, iff the field should be processed.
|
||||
*/
|
||||
protected function testField($name, array $field) {
|
||||
if (empty($this->options['fields'])) {
|
||||
return $this->testType($field['type']);
|
||||
}
|
||||
return !empty($this->options['fields'][$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return
|
||||
* TRUE, iff the type should be processed.
|
||||
*/
|
||||
protected function testType($type) {
|
||||
return search_api_is_text_type($type, array('text', 'tokens'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Called for processing a single text element in a field. The default
|
||||
* implementation just calls process().
|
||||
*
|
||||
* $value can either be left a string, or changed into an array of tokens. A
|
||||
* token is an associative array containing:
|
||||
* - value: Either the text inside the token, or a nested array of tokens. The
|
||||
* score of nested tokens will be multiplied by their parent's score.
|
||||
* - score: The relative importance of the token, as a float, with 1 being
|
||||
* the default.
|
||||
*/
|
||||
protected function processFieldValue(&$value) {
|
||||
$this->process($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called for processing a single search keyword. The default implementation
|
||||
* just calls process().
|
||||
*
|
||||
* $value can either be left a string, or be changed into a nested keys array,
|
||||
* as defined by SearchApiQueryInterface.
|
||||
*/
|
||||
protected function processKey(&$value) {
|
||||
$this->process($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called for processing a single filter value. The default implementation
|
||||
* just calls process().
|
||||
*
|
||||
* $value has to remain a string.
|
||||
*/
|
||||
protected function processFilterValue(&$value) {
|
||||
$this->process($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Function that is ultimately called for all text by the standard
|
||||
* implementation, and does nothing by default.
|
||||
*
|
||||
* @param $value
|
||||
* The value to preprocess as a string. Can be manipulated directly, nothing
|
||||
* has to be returned. Since this can be called for all value types, $value
|
||||
* has to remain a string.
|
||||
*/
|
||||
protected function process(&$value) {
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,403 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains the SearchApiHighlight class.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Processor for highlighting search results.
|
||||
*/
|
||||
class SearchApiHighlight extends SearchApiAbstractProcessor {
|
||||
|
||||
/**
|
||||
* PREG regular expression for a word boundary.
|
||||
*
|
||||
* We highlight around non-indexable or CJK characters.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected static $boundary;
|
||||
|
||||
/**
|
||||
* PREG regular expression for splitting words.
|
||||
*
|
||||
* We highlight around non-indexable or CJK characters.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected static $split;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __construct(SearchApiIndex $index, array $options = array()) {
|
||||
parent::__construct($index, $options);
|
||||
|
||||
$cjk = '\x{1100}-\x{11FF}\x{3040}-\x{309F}\x{30A1}-\x{318E}' .
|
||||
'\x{31A0}-\x{31B7}\x{31F0}-\x{31FF}\x{3400}-\x{4DBF}\x{4E00}-\x{9FCF}' .
|
||||
'\x{A000}-\x{A48F}\x{A4D0}-\x{A4FD}\x{A960}-\x{A97F}\x{AC00}-\x{D7FF}' .
|
||||
'\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';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configurationForm() {
|
||||
$this->options += array(
|
||||
'prefix' => '<strong>',
|
||||
'suffix' => '</strong>',
|
||||
'excerpt' => TRUE,
|
||||
'excerpt_length' => 256,
|
||||
'highlight' => 'always',
|
||||
);
|
||||
|
||||
$form['prefix'] = array(
|
||||
'#type' => 'textfield',
|
||||
'#title' => t('Highlighting prefix'),
|
||||
'#description' => t('Text/HTML that will be prepended to all occurrences of search keywords in highlighted text.'),
|
||||
'#default_value' => $this->options['prefix'],
|
||||
);
|
||||
$form['suffix'] = array(
|
||||
'#type' => 'textfield',
|
||||
'#title' => t('Highlighting suffix'),
|
||||
'#description' => t('Text/HTML that will be appended to all occurrences of search keywords in highlighted text.'),
|
||||
'#default_value' => $this->options['suffix'],
|
||||
);
|
||||
$form['excerpt'] = array(
|
||||
'#type' => 'checkbox',
|
||||
'#title' => t('Create excerpt'),
|
||||
'#description' => t('When enabled, an excerpt will be created for searches with keywords, containing all occurrences of keywords in a fulltext field.'),
|
||||
'#default_value' => $this->options['excerpt'],
|
||||
);
|
||||
$form['excerpt_length'] = array(
|
||||
'#type' => 'textfield',
|
||||
'#title' => t('Excerpt length'),
|
||||
'#description' => t('The requested length of the excerpt, in characters.'),
|
||||
'#default_value' => $this->options['excerpt_length'],
|
||||
'#element_validate' => array('element_validate_integer_positive'),
|
||||
'#states' => array(
|
||||
'visible' => array(
|
||||
'#edit-processors-search-api-highlighting-settings-excerpt' => array(
|
||||
'checked' => TRUE,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
$form['highlight'] = array(
|
||||
'#type' => 'select',
|
||||
'#title' => t('Highlight returned field data'),
|
||||
'#description' => t('Select whether returned fields should be highlighted.'),
|
||||
'#options' => array(
|
||||
'always' => t('Always'),
|
||||
'server' => t('If the server returns fields'),
|
||||
'never' => t('Never'),
|
||||
),
|
||||
'#default_value' => $this->options['highlight'],
|
||||
);
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configurationFormValidate(array $form, array &$values, array &$form_state) {
|
||||
// Overridden so $form['fields'] is not checked.
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function postprocessSearchResults(array &$response, SearchApiQuery $query) {
|
||||
if (!$response['result count'] || !($keys = $this->getKeywords($query))) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($response['results'] as $id => &$result) {
|
||||
if ($this->options['excerpt']) {
|
||||
$text = array();
|
||||
$fields = $this->getFulltextFields($response['results'], $id);
|
||||
foreach ($fields as $data) {
|
||||
if (is_array($data)) {
|
||||
$text = array_merge($text, $data);
|
||||
}
|
||||
else {
|
||||
$text[] = $data;
|
||||
}
|
||||
}
|
||||
$result['excerpt'] = $this->createExcerpt(implode("\n\n", $text), $keys);
|
||||
}
|
||||
if ($this->options['highlight'] != 'never') {
|
||||
$fields = $this->getFulltextFields($response['results'], $id, $this->options['highlight'] == 'always');
|
||||
foreach ($fields as $field => $data) {
|
||||
if (is_array($data)) {
|
||||
foreach ($data as $i => $text) {
|
||||
$result['fields'][$field][$i] = $this->highlightField($text, $keys);
|
||||
}
|
||||
}
|
||||
else {
|
||||
$result['fields'][$field] = $this->highlightField($data, $keys);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the fulltext data of a result.
|
||||
*
|
||||
* @param array $results
|
||||
* All results returned in the search, by reference.
|
||||
* @param int|string $i
|
||||
* The index in the results array of the result whose data should be
|
||||
* returned.
|
||||
* @param bool $load
|
||||
* TRUE if the item should be loaded if necessary, FALSE if only fields
|
||||
* already returned in the results should be used.
|
||||
*
|
||||
* @return array
|
||||
* 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) {
|
||||
global $language;
|
||||
$data = array();
|
||||
|
||||
$result = &$results[$i];
|
||||
// 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();
|
||||
foreach ($fulltext_fields as $field) {
|
||||
if (array_key_exists($field, $result['fields'])) {
|
||||
$data[$field] = $result['fields'][$field];
|
||||
}
|
||||
elseif ($load) {
|
||||
$needs_extraction[$field] = $fields[$field];
|
||||
}
|
||||
}
|
||||
|
||||
if (!$needs_extraction) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
if (empty($result['entity'])) {
|
||||
$items = $this->index->loadItems(array_keys($results));
|
||||
foreach ($items as $id => $item) {
|
||||
$results[$id]['entity'] = $item;
|
||||
}
|
||||
}
|
||||
// If we still don't have a loaded item, we should stop trying.
|
||||
if (empty($result['entity'])) {
|
||||
return $data;
|
||||
}
|
||||
$wrapper = $this->index->entityWrapper($result['entity'], FALSE);
|
||||
$wrapper->language($language->language);
|
||||
$extracted = search_api_extract_fields($wrapper, $needs_extraction);
|
||||
|
||||
foreach ($extracted as $field => $info) {
|
||||
if (isset($info['value'])) {
|
||||
$data[$field] = $info['value'];
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the positive keywords used in a search query.
|
||||
*
|
||||
* @param SearchApiQuery $query
|
||||
* The query from which to extract the keywords.
|
||||
*
|
||||
* @return array
|
||||
* An array of all unique positive keywords used in the query.
|
||||
*/
|
||||
protected function getKeywords(SearchApiQuery $query) {
|
||||
$keys = $query->getKeys();
|
||||
if (!$keys) {
|
||||
return array();
|
||||
}
|
||||
if (is_array($keys)) {
|
||||
return $this->flattenKeysArray($keys);
|
||||
}
|
||||
|
||||
$keywords = preg_split(self::$split, $keys);
|
||||
// Assure there are no duplicates. (This is actually faster than
|
||||
// array_unique() by a factor of 3 to 4.)
|
||||
$keywords = drupal_map_assoc(array_filter($keywords));
|
||||
// Remove quotes from keywords.
|
||||
foreach ($keywords as $key) {
|
||||
$keywords[$key] = trim($key, "'\"");
|
||||
}
|
||||
return drupal_map_assoc(array_filter($keywords));
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the positive keywords from a keys array.
|
||||
*
|
||||
* @param array $keys
|
||||
* A search keys array, as specified by SearchApiQueryInterface::getKeys().
|
||||
*
|
||||
* @return array
|
||||
* An array of all unique positive keywords contained in the keys.
|
||||
*/
|
||||
protected function flattenKeysArray(array $keys) {
|
||||
if (!empty($keys['#negation'])) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$keywords = array();
|
||||
foreach ($keys as $i => $key) {
|
||||
if (!element_child($i)) {
|
||||
continue;
|
||||
}
|
||||
if (is_array($key)) {
|
||||
$keywords += $this->flattenKeysArray($key);
|
||||
}
|
||||
else {
|
||||
$keywords[$key] = $key;
|
||||
}
|
||||
}
|
||||
|
||||
return $keywords;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns snippets from a piece of text, with certain keywords highlighted.
|
||||
*
|
||||
* Largely copied from search_excerpt().
|
||||
*
|
||||
* @param string $text
|
||||
* The text to extract fragments from.
|
||||
* @param array $keys
|
||||
* Search keywords entered by the user.
|
||||
*
|
||||
* @return string
|
||||
* A string containing HTML for the excerpt.
|
||||
*/
|
||||
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);
|
||||
|
||||
// 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.
|
||||
$ranges = array();
|
||||
$included = array();
|
||||
$length = 0;
|
||||
$workkeys = $keys;
|
||||
while ($length < $this->options['excerpt_length'] && count($workkeys)) {
|
||||
foreach ($workkeys as $k => $key) {
|
||||
if ($length >= $this->options['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;
|
||||
}
|
||||
// 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])) {
|
||||
$p = $match[0][1];
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
else {
|
||||
unset($workkeys[$k]);
|
||||
}
|
||||
}
|
||||
else {
|
||||
unset($workkeys[$k]);
|
||||
}
|
||||
}
|
||||
else {
|
||||
unset($workkeys[$k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (count($ranges) == 0) {
|
||||
// We didn't find any keyword matches, so just 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).
|
||||
$newranges = array();
|
||||
foreach ($ranges as $from2 => $to2) {
|
||||
if (!isset($from1)) {
|
||||
$from1 = $from2;
|
||||
$to1 = $to2;
|
||||
continue;
|
||||
}
|
||||
if ($from2 <= $to1) {
|
||||
$to1 = max($to1, $to2);
|
||||
}
|
||||
else {
|
||||
$newranges[$from1] = $to1;
|
||||
$from1 = $from2;
|
||||
$to1 = $to2;
|
||||
}
|
||||
}
|
||||
$newranges[$from1] = $to1;
|
||||
|
||||
// Fetch text
|
||||
$out = array();
|
||||
foreach ($newranges as $from => $to) {
|
||||
$out[] = substr($text, $from, $to - $from);
|
||||
}
|
||||
|
||||
// Let translators have the ... separator text as one chunk.
|
||||
$dots = explode('!excerpt', t('... !excerpt ... !excerpt ...'));
|
||||
|
||||
$text = (isset($newranges[0]) ? '' : $dots[0]) . implode($dots[1], $out) . $dots[2];
|
||||
$text = check_plain($text);
|
||||
|
||||
return $this->highlightField($text, $keys);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks occurrences of the search keywords in a text field.
|
||||
*
|
||||
* @param string $text
|
||||
* The text of the field.
|
||||
* @param array $keys
|
||||
* Search keywords entered by the user.
|
||||
*
|
||||
* @return string
|
||||
* The field's text with all occurrences of search keywords highlighted.
|
||||
*/
|
||||
protected function highlightField($text, array $keys) {
|
||||
$replace = $this->options['prefix'] . '\0' . $this->options['suffix'];
|
||||
$keys = implode('|', array_map('preg_quote', $keys));
|
||||
$text = preg_replace('/' . self::$boundary . '(' . $keys . ')' . self::$boundary . '/iu', $replace, ' ' . $text . ' ');
|
||||
return substr($text, 1, -1);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains SearchApiHtmlFilter.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Processor for stripping HTML from indexed fulltext data. Supports assigning
|
||||
* custom boosts for any HTML element.
|
||||
*/
|
||||
// @todo Process query?
|
||||
class SearchApiHtmlFilter extends SearchApiAbstractProcessor {
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $tags;
|
||||
|
||||
public function __construct(SearchApiIndex $index, array $options = array()) {
|
||||
parent::__construct($index, $options);
|
||||
$this->options += array(
|
||||
'title' => FALSE,
|
||||
'alt' => TRUE,
|
||||
'tags' => "h1 = 5\n" .
|
||||
"h2 = 3\n" .
|
||||
"h3 = 2\n" .
|
||||
"strong = 2\n" .
|
||||
"b = 2\n" .
|
||||
"em = 1.5\n" .
|
||||
'u = 1.5',
|
||||
);
|
||||
$this->tags = drupal_parse_info_format($this->options['tags']);
|
||||
// Specifying empty tags doesn't make sense.
|
||||
unset($this->tags['br'], $this->tags['hr']);
|
||||
}
|
||||
|
||||
public function configurationForm() {
|
||||
$form = parent::configurationForm();
|
||||
$form += array(
|
||||
'title' => array(
|
||||
'#type' => 'checkbox',
|
||||
'#title' => t('Index title attribute'),
|
||||
'#description' => t('If set, the contents of title attributes will be indexed.'),
|
||||
'#default_value' => $this->options['title'],
|
||||
),
|
||||
'alt' => array(
|
||||
'#type' => 'checkbox',
|
||||
'#title' => t('Index alt attribute'),
|
||||
'#description' => t('If set, the alternative text of images will be indexed.'),
|
||||
'#default_value' => $this->options['alt'],
|
||||
),
|
||||
'tags' => array(
|
||||
'#type' => 'textarea',
|
||||
'#title' => t('Tag boosts'),
|
||||
'#description' => t('Specify special boost values for certain HTML elements, in <a href="@link">INI file format</a>. ' .
|
||||
'The boost values of nested elements are multiplied, elements not mentioned will have the default boost value of 1. ' .
|
||||
'Assign a boost of 0 to ignore the text content of that HTML element.',
|
||||
array('@link' => url('http://api.drupal.org/api/function/drupal_parse_info_format/7'))),
|
||||
'#default_value' => $this->options['tags'],
|
||||
),
|
||||
);
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
public function configurationFormValidate(array $form, array &$values, array &$form_state) {
|
||||
parent::configurationFormValidate($form, $values, $form_state);
|
||||
|
||||
if (empty($values['tags'])) {
|
||||
return;
|
||||
}
|
||||
$tags = drupal_parse_info_format($values['tags']);
|
||||
$errors = array();
|
||||
foreach ($tags as $key => $value) {
|
||||
if (is_array($value)) {
|
||||
$errors[] = t("Boost value for tag <@tag> can't be an array.", array('@tag' => $key));
|
||||
}
|
||||
elseif (!is_numeric($value)) {
|
||||
$errors[] = t("Boost value for tag <@tag> must be numeric.", array('@tag' => $key));
|
||||
}
|
||||
elseif ($value < 0) {
|
||||
$errors[] = t('Boost value for tag <@tag> must be non-negative.', array('@tag' => $key));
|
||||
}
|
||||
}
|
||||
if ($errors) {
|
||||
form_error($form['tags'], implode("<br />\n", $errors));
|
||||
}
|
||||
}
|
||||
|
||||
protected function processFieldValue(&$value) {
|
||||
$text = str_replace(array('<', '>'), array(' <', '> '), $value); // Let removed tags still delimit words.
|
||||
if ($this->options['title']) {
|
||||
$text = preg_replace('/(<[-a-z_]+[^>]+)\btitle\s*=\s*("([^"]+)"|\'([^\']+)\')([^>]*>)/i', '$1 $5 $3$4 ', $text);
|
||||
}
|
||||
if ($this->options['alt']) {
|
||||
$text = preg_replace('/<img\b[^>]+\balt\s*=\s*("([^"]+)"|\'([^\']+)\')[^>]*>/i', ' <img>$2$3</img> ', $text);
|
||||
}
|
||||
if ($this->tags) {
|
||||
$text = strip_tags($text, '<' . implode('><', array_keys($this->tags)) . '>');
|
||||
$value = $this->parseText($text);
|
||||
}
|
||||
else {
|
||||
$value = strip_tags($text);
|
||||
}
|
||||
}
|
||||
|
||||
protected function parseText(&$text, $active_tag = NULL, $boost = 1) {
|
||||
$ret = array();
|
||||
while (($pos = strpos($text, '<')) !== FALSE) {
|
||||
if ($boost && $pos > 0) {
|
||||
$ret[] = array(
|
||||
'value' => html_entity_decode(substr($text, 0, $pos), ENT_QUOTES, 'UTF-8'),
|
||||
'score' => $boost,
|
||||
);
|
||||
}
|
||||
$text = substr($text, $pos + 1);
|
||||
preg_match('#^(/?)([-:_a-zA-Z]+)#', $text, $m);
|
||||
$text = substr($text, strpos($text, '>') + 1);
|
||||
if ($m[1]) {
|
||||
// Closing tag.
|
||||
if ($active_tag && $m[2] == $active_tag) {
|
||||
return $ret;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Opening tag => recursive call.
|
||||
$inner_boost = $boost * (isset($this->tags[$m[2]]) ? $this->tags[$m[2]] : 1);
|
||||
$ret = array_merge($ret, $this->parseText($text, $m[2], $inner_boost));
|
||||
}
|
||||
}
|
||||
if ($text) {
|
||||
$ret[] = array(
|
||||
'value' => html_entity_decode($text, ENT_QUOTES, 'UTF-8'),
|
||||
'score' => $boost,
|
||||
);
|
||||
$text = '';
|
||||
}
|
||||
return $ret;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains SearchApiIgnoreCase.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Processor for making searches case-insensitive.
|
||||
*/
|
||||
class SearchApiIgnoreCase extends SearchApiAbstractProcessor {
|
||||
|
||||
protected function process(&$value) {
|
||||
// We don't touch integers, NULL values or the like.
|
||||
if (is_string($value)) {
|
||||
$value = drupal_strtolower($value);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains SearchApiStopWords.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Processor for removing stopwords from index and search terms.
|
||||
*/
|
||||
class SearchApiStopWords extends SearchApiAbstractProcessor {
|
||||
|
||||
/**
|
||||
* Holds all words ignored for the last query.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $ignored = array();
|
||||
|
||||
public function configurationForm() {
|
||||
$form = parent::configurationForm();
|
||||
|
||||
$form += array(
|
||||
'help' => array(
|
||||
'#markup' => '<p>' . t('Provide a stopwords file or enter the words in this form. If you do both, both will be used. Read about !stopwords.', array('!stopwords' => l(t('stop words'), "http://en.wikipedia.org/wiki/Stop_words"))) . '</p>',
|
||||
),
|
||||
'file' => array(
|
||||
'#type' => 'textfield',
|
||||
'#title' => t('Stopwords file'),
|
||||
'#description' => t('This must be a stream-type description like <code>public://stopwords/stopwords.txt</code> or <code>http://example.com/stopwords.txt</code> or <code>private://stopwords.txt</code>.'),
|
||||
),
|
||||
'stopwords' => array(
|
||||
'#type' => 'textarea',
|
||||
'#title' => t('Stopwords'),
|
||||
'#description' => t('Enter a space and/or linebreak separated list of stopwords that will be removed from content before it is indexed and from search terms before searching.'),
|
||||
'#default_value' => t("but\ndid\nthe this that those\netc"),
|
||||
),
|
||||
);
|
||||
|
||||
if (!empty($this->options)) {
|
||||
$form['file']['#default_value'] = $this->options['file'];
|
||||
$form['stopwords']['#default_value'] = $this->options['stopwords'];
|
||||
}
|
||||
return $form;
|
||||
}
|
||||
|
||||
public function configurationFormValidate(array $form, array &$values, array &$form_state) {
|
||||
parent::configurationFormValidate($form, $values, $form_state);
|
||||
|
||||
$uri = $values['file'];
|
||||
if (!empty($uri) && !@file_get_contents($uri)) {
|
||||
$el = $form['file'];
|
||||
form_error($el, t('Stopwords file') . ': ' . t('The file %uri is not readable or does not exist.', array('%uri' => $uri)));
|
||||
}
|
||||
}
|
||||
|
||||
public function process(&$value) {
|
||||
$stopwords = $this->getStopWords();
|
||||
if (empty($stopwords) || !is_string($value)) {
|
||||
return;
|
||||
}
|
||||
$words = preg_split('/\s+/', $value);
|
||||
foreach ($words as $sub_key => $sub_value) {
|
||||
if (isset($stopwords[$sub_value])) {
|
||||
unset($words[$sub_key]);
|
||||
$this->ignored[] = $sub_value;
|
||||
}
|
||||
}
|
||||
$value = implode(' ', $words);
|
||||
}
|
||||
|
||||
public function preprocessSearchQuery(SearchApiQuery $query) {
|
||||
$this->ignored = array();
|
||||
parent::preprocessSearchQuery($query);
|
||||
}
|
||||
|
||||
public function postprocessSearchResults(array &$response, SearchApiQuery $query) {
|
||||
if ($this->ignored) {
|
||||
if (isset($response['ignored'])) {
|
||||
$response['ignored'] = array_merge($response['ignored'], $this->ignored);
|
||||
}
|
||||
else {
|
||||
$response['ignored'] = $this->ignored;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return
|
||||
* An array whose keys are the stopwords set in either the file or the text
|
||||
* field.
|
||||
*/
|
||||
protected function getStopWords() {
|
||||
if (isset($this->stopwords)) {
|
||||
return $this->stopwords;
|
||||
}
|
||||
$file_words = $form_words = array();
|
||||
if (!empty($this->options['file']) && $stopwords_file = file_get_contents($this->options['file'])) {
|
||||
$file_words = preg_split('/\s+/', $stopwords_file);
|
||||
}
|
||||
if (!empty($this->options['stopwords'])) {
|
||||
$form_words = preg_split('/\s+/', $this->options['stopwords']);
|
||||
}
|
||||
$this->stopwords = array_flip(array_merge($file_words, $form_words));
|
||||
return $this->stopwords;
|
||||
}
|
||||
}
|
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains SearchApiTokenizer.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Processor for tokenizing fulltext data by replacing (configurable)
|
||||
* non-letters with spaces.
|
||||
*/
|
||||
class SearchApiTokenizer extends SearchApiAbstractProcessor {
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $spaces;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $ignorable;
|
||||
|
||||
public function configurationForm() {
|
||||
$form = parent::configurationForm();
|
||||
|
||||
// Only make fulltext fields available as options.
|
||||
$fields = $this->index->getFields();
|
||||
$field_options = array();
|
||||
foreach ($fields as $name => $field) {
|
||||
if (empty($field['real_type']) && search_api_is_text_type($field['type'])) {
|
||||
$field_options[$name] = $field['name'];
|
||||
}
|
||||
}
|
||||
$form['fields']['#options'] = $field_options;
|
||||
|
||||
$form += array(
|
||||
'spaces' => array(
|
||||
'#type' => 'textfield',
|
||||
'#title' => t('Whitespace characters'),
|
||||
'#description' => t('Specify the characters that should be regarded as whitespace and therefore used as word-delimiters. ' .
|
||||
'Specify the characters as a <a href="@link">PCRE character class</a>. ' .
|
||||
'Note: For non-English content, the default setting might not be suitable.',
|
||||
array('@link' => url('http://www.php.net/manual/en/regexp.reference.character-classes.php'))),
|
||||
'#default_value' => "[^[:alnum:]]",
|
||||
),
|
||||
'ignorable' => array(
|
||||
'#type' => 'textfield',
|
||||
'#title' => t('Ignorable characters'),
|
||||
'#description' => t('Specify characters which should be removed from fulltext fields and search strings (e.g., "-"). The same format as above is used.'),
|
||||
'#default_value' => "[']",
|
||||
),
|
||||
);
|
||||
|
||||
if (!empty($this->options)) {
|
||||
$form['spaces']['#default_value'] = $this->options['spaces'];
|
||||
$form['ignorable']['#default_value'] = $this->options['ignorable'];
|
||||
}
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
public function configurationFormValidate(array $form, array &$values, array &$form_state) {
|
||||
parent::configurationFormValidate($form, $values, $form_state);
|
||||
|
||||
$spaces = str_replace('/', '\/', $values['spaces']);
|
||||
$ignorable = str_replace('/', '\/', $values['ignorable']);
|
||||
if (@preg_match('/(' . $spaces . ')+/u', '') === FALSE) {
|
||||
$el = $form['spaces'];
|
||||
form_error($el, $el['#title'] . ': ' . t('The entered text is no valid regular expression.'));
|
||||
}
|
||||
if (@preg_match('/(' . $ignorable . ')+/u', '') === FALSE) {
|
||||
$el = $form['ignorable'];
|
||||
form_error($el, $el['#title'] . ': ' . t('The entered text is no valid regular expression.'));
|
||||
}
|
||||
}
|
||||
|
||||
protected function processFieldValue(&$value) {
|
||||
$this->prepare();
|
||||
if ($this->ignorable) {
|
||||
$value = preg_replace('/(' . $this->ignorable . ')+/u', '', $value);
|
||||
}
|
||||
if ($this->spaces) {
|
||||
$arr = preg_split('/(' . $this->spaces . ')+/u', $value);
|
||||
if (count($arr) > 1) {
|
||||
$value = array();
|
||||
foreach ($arr as $token) {
|
||||
$value[] = array('value' => $token);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function process(&$value) {
|
||||
// We don't touch integers, NULL values or the like.
|
||||
if (is_string($value)) {
|
||||
$this->prepare();
|
||||
if ($this->ignorable) {
|
||||
$value = preg_replace('/' . $this->ignorable . '+/u', '', $value);
|
||||
}
|
||||
if ($this->spaces) {
|
||||
$value = preg_replace('/' . $this->spaces . '+/u', ' ', $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function prepare() {
|
||||
if (!isset($this->spaces)) {
|
||||
$this->spaces = str_replace('/', '\/', $this->options['spaces']);
|
||||
$this->ignorable = str_replace('/', '\/', $this->options['ignorable']);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains SearchApiTransliteration.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Processor for making searches insensitive to accents and other non-ASCII characters.
|
||||
*/
|
||||
class SearchApiTransliteration extends SearchApiAbstractProcessor {
|
||||
|
||||
protected function process(&$value) {
|
||||
// We don't touch integers, NULL values or the like.
|
||||
if (is_string($value)) {
|
||||
$value = transliteration_get($value, '', language_default('language'));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
1013
sites/all/modules/contrib/search/search_api/includes/query.inc
Normal file
1013
sites/all/modules/contrib/search/search_api/includes/query.inc
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,396 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains SearchApiServer.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Class representing a search server.
|
||||
*
|
||||
* This can handle the same calls as defined in the SearchApiServiceInterface
|
||||
* and pass it on to the service implementation appropriate for this server.
|
||||
*/
|
||||
class SearchApiServer extends Entity {
|
||||
|
||||
/* Database values that will be set when object is loaded: */
|
||||
|
||||
/**
|
||||
* The primary identifier for a server.
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
public $id = 0;
|
||||
|
||||
/**
|
||||
* The displayed name for a server.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $name = '';
|
||||
|
||||
/**
|
||||
* The machine name for a server.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $machine_name = '';
|
||||
|
||||
/**
|
||||
* The displayed description for a server.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $description = '';
|
||||
|
||||
/**
|
||||
* The id of the service class to use for this server.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $class = '';
|
||||
|
||||
/**
|
||||
* The options used to configure the service object.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $options = array();
|
||||
|
||||
/**
|
||||
* A flag indicating whether the server is enabled.
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
public $enabled = 1;
|
||||
|
||||
/**
|
||||
* Proxy object for invoking service methods.
|
||||
*
|
||||
* @var SearchApiServiceInterface
|
||||
*/
|
||||
protected $proxy;
|
||||
|
||||
/**
|
||||
* Constructor as a helper to the parent constructor.
|
||||
*/
|
||||
public function __construct(array $values = array()) {
|
||||
parent::__construct($values, 'search_api_server');
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for updating entity properties.
|
||||
*
|
||||
* NOTE: You shouldn't change any properties of this object before calling
|
||||
* this method, as this might lead to the fields not being saved correctly.
|
||||
*
|
||||
* @param array $fields
|
||||
* The new field values.
|
||||
*
|
||||
* @return int|false
|
||||
* SAVE_UPDATED on success, FALSE on failure, 0 if the fields already had
|
||||
* the specified values.
|
||||
*/
|
||||
public function update(array $fields) {
|
||||
$changeable = array('name' => 1, 'enabled' => 1, 'description' => 1, 'options' => 1);
|
||||
$changed = FALSE;
|
||||
foreach ($fields as $field => $value) {
|
||||
if (isset($changeable[$field]) && $value !== $this->$field) {
|
||||
$this->$field = $value;
|
||||
$changed = TRUE;
|
||||
}
|
||||
}
|
||||
// If there are no new values, just return 0.
|
||||
if (!$changed) {
|
||||
return 0;
|
||||
}
|
||||
return $this->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic method for determining which fields should be serialized.
|
||||
*
|
||||
* Serialize all properties except the proxy object.
|
||||
*
|
||||
* @return array
|
||||
* An array of properties to be serialized.
|
||||
*/
|
||||
public function __sleep() {
|
||||
$ret = get_object_vars($this);
|
||||
unset($ret['proxy'], $ret['status'], $ret['module'], $ret['is_new']);
|
||||
return array_keys($ret);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for ensuring the proxy object is set up.
|
||||
*/
|
||||
protected function ensureProxy() {
|
||||
if (!isset($this->proxy)) {
|
||||
$class = search_api_get_service_info($this->class);
|
||||
if ($class && class_exists($class['class'])) {
|
||||
if (empty($this->options)) {
|
||||
// We always have to provide the options.
|
||||
$this->options = array();
|
||||
}
|
||||
$this->proxy = new $class['class']($this);
|
||||
}
|
||||
if (!($this->proxy instanceof SearchApiServiceInterface)) {
|
||||
throw new SearchApiException(t('Search server with machine name @name specifies illegal service class @class.', array('@name' => $this->machine_name, '@class' => $this->class)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reacts to calls of undefined methods on this object.
|
||||
*
|
||||
* If the service class defines additional methods, not specified in the
|
||||
* SearchApiServiceInterface interface, then they are called via this magic
|
||||
* method.
|
||||
*/
|
||||
public function __call($name, $arguments = array()) {
|
||||
$this->ensureProxy();
|
||||
return call_user_func_array(array($this->proxy, $name), $arguments);
|
||||
}
|
||||
|
||||
// Proxy methods
|
||||
|
||||
// For increased clarity, and since some parameters are passed by reference,
|
||||
// we don't use the __call() magic method for those. This also gives us the
|
||||
// opportunity to do additional error handling.
|
||||
|
||||
/**
|
||||
* Form constructor for the server configuration form.
|
||||
*
|
||||
* @see SearchApiServiceInterface::configurationForm()
|
||||
*/
|
||||
public function configurationForm(array $form, array &$form_state) {
|
||||
$this->ensureProxy();
|
||||
return $this->proxy->configurationForm($form, $form_state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation callback for the form returned by configurationForm().
|
||||
*
|
||||
* @see SearchApiServiceInterface::configurationFormValidate()
|
||||
*/
|
||||
public function configurationFormValidate(array $form, array &$values, array &$form_state) {
|
||||
$this->ensureProxy();
|
||||
return $this->proxy->configurationFormValidate($form, $values, $form_state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit callback for the form returned by configurationForm().
|
||||
*
|
||||
* @see SearchApiServiceInterface::configurationFormSubmit()
|
||||
*/
|
||||
public function configurationFormSubmit(array $form, array &$values, array &$form_state) {
|
||||
$this->ensureProxy();
|
||||
return $this->proxy->configurationFormSubmit($form, $values, $form_state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether this service class supports a given feature.
|
||||
*
|
||||
* @see SearchApiServiceInterface::supportsFeature()
|
||||
*/
|
||||
public function supportsFeature($feature) {
|
||||
$this->ensureProxy();
|
||||
return $this->proxy->supportsFeature($feature);
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays this server's settings.
|
||||
*
|
||||
* @see SearchApiServiceInterface::viewSettings()
|
||||
*/
|
||||
public function viewSettings() {
|
||||
$this->ensureProxy();
|
||||
return $this->proxy->viewSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reacts to the server's creation.
|
||||
*
|
||||
* @see SearchApiServiceInterface::postCreate()
|
||||
*/
|
||||
public function postCreate() {
|
||||
$this->ensureProxy();
|
||||
return $this->proxy->postCreate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies this server that its fields are about to be updated.
|
||||
*
|
||||
* @see SearchApiServiceInterface::postUpdate()
|
||||
*/
|
||||
public function postUpdate() {
|
||||
$this->ensureProxy();
|
||||
return $this->proxy->postUpdate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies this server that it is about to be deleted from the database.
|
||||
*
|
||||
* @see SearchApiServiceInterface::preDelete()
|
||||
*/
|
||||
public function preDelete() {
|
||||
$this->ensureProxy();
|
||||
return $this->proxy->preDelete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new index to this server.
|
||||
*
|
||||
* If an exception in the service class implementation of this method occcurs,
|
||||
* it will be caught and the operation saved as an pending server task.
|
||||
*
|
||||
* @see SearchApiServiceInterface::addIndex()
|
||||
* @see search_api_server_tasks_add()
|
||||
*/
|
||||
public function addIndex(SearchApiIndex $index) {
|
||||
$this->ensureProxy();
|
||||
try {
|
||||
$this->proxy->addIndex($index);
|
||||
}
|
||||
catch (SearchApiException $e) {
|
||||
$vars = array(
|
||||
'%server' => $this->name,
|
||||
'%index' => $index->name,
|
||||
);
|
||||
watchdog_exception('search_api', $e, '%type while adding index %index to server %server: !message in %function (line %line of %file).', $vars);
|
||||
search_api_server_tasks_add($this, __FUNCTION__, $index);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the server that the field settings for the index have changed.
|
||||
*
|
||||
* If the service class implementation of the method returns TRUE, this will
|
||||
* automatically take care of marking the items on the index for re-indexing.
|
||||
*
|
||||
* If an exception in the service class implementation of this method occcurs,
|
||||
* it will be caught and the operation saved as an pending server task.
|
||||
*
|
||||
* @see SearchApiServiceInterface::fieldsUpdated()
|
||||
* @see search_api_server_tasks_add()
|
||||
*/
|
||||
public function fieldsUpdated(SearchApiIndex $index) {
|
||||
$this->ensureProxy();
|
||||
try {
|
||||
if ($this->proxy->fieldsUpdated($index)) {
|
||||
_search_api_index_reindex($index);
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
catch (SearchApiException $e) {
|
||||
$vars = array(
|
||||
'%server' => $this->name,
|
||||
'%index' => $index->name,
|
||||
);
|
||||
watchdog_exception('search_api', $e, '%type while updating the fields of index %index on server %server: !message in %function (line %line of %file).', $vars);
|
||||
search_api_server_tasks_add($this, __FUNCTION__, $index, isset($index->original) ? $index->original : NULL);
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an index from this server.
|
||||
*
|
||||
* If an exception in the service class implementation of this method occcurs,
|
||||
* it will be caught and the operation saved as an pending server task.
|
||||
*
|
||||
* @see SearchApiServiceInterface::removeIndex()
|
||||
* @see search_api_server_tasks_add()
|
||||
*/
|
||||
public function removeIndex($index) {
|
||||
// When removing an index from a server, it doesn't make any sense anymore to
|
||||
// delete items from it, or react to other changes.
|
||||
search_api_server_tasks_delete(NULL, $this, $index);
|
||||
|
||||
$this->ensureProxy();
|
||||
try {
|
||||
$this->proxy->removeIndex($index);
|
||||
}
|
||||
catch (SearchApiException $e) {
|
||||
$vars = array(
|
||||
'%server' => $this->name,
|
||||
'%index' => is_object($index) ? $index->name : $index,
|
||||
);
|
||||
watchdog_exception('search_api', $e, '%type while removing index %index from server %server: !message in %function (line %line of %file).', $vars);
|
||||
search_api_server_tasks_add($this, __FUNCTION__, $index);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indexes the specified items.
|
||||
*
|
||||
* @see SearchApiServiceInterface::indexItems()
|
||||
*/
|
||||
public function indexItems(SearchApiIndex $index, array $items) {
|
||||
$this->ensureProxy();
|
||||
return $this->proxy->indexItems($index, $items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes indexed items from this server.
|
||||
*
|
||||
* If an exception in the service class implementation of this method occcurs,
|
||||
* it will be caught and the operation saved as an pending server task.
|
||||
*
|
||||
* @see SearchApiServiceInterface::deleteItems()
|
||||
* @see search_api_server_tasks_add()
|
||||
*/
|
||||
public function deleteItems($ids = 'all', SearchApiIndex $index = NULL) {
|
||||
$this->ensureProxy();
|
||||
try {
|
||||
$this->proxy->deleteItems($ids, $index);
|
||||
}
|
||||
catch (SearchApiException $e) {
|
||||
$vars = array(
|
||||
'%server' => $this->name,
|
||||
);
|
||||
watchdog_exception('search_api', $e, '%type while deleting items from server %server: !message in %function (line %line of %file).', $vars);
|
||||
search_api_server_tasks_add($this, __FUNCTION__, $index, $ids);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a query object for searching on an index lying on this server.
|
||||
*
|
||||
* @see SearchApiServiceInterface::query()
|
||||
*/
|
||||
public function query(SearchApiIndex $index, $options = array()) {
|
||||
$this->ensureProxy();
|
||||
return $this->proxy->query($index, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a search on the server represented by this object.
|
||||
*
|
||||
* @see SearchApiServiceInterface::search()
|
||||
*/
|
||||
public function search(SearchApiQueryInterface $query) {
|
||||
$this->ensureProxy();
|
||||
return $this->proxy->search($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves additional information for the server, if available.
|
||||
*
|
||||
* Retrieving such information is only supported if the service class supports
|
||||
* the "search_api_service_extra" feature.
|
||||
*
|
||||
* @return array
|
||||
* An array containing additional, service class-specific information about
|
||||
* the server.
|
||||
*
|
||||
* @see SearchApiAbstractService::getExtraInformation()
|
||||
*/
|
||||
public function getExtraInformation() {
|
||||
if ($this->proxy->supportsFeature('search_api_service_extra')) {
|
||||
return $this->proxy->getExtraInformation();
|
||||
}
|
||||
return array();
|
||||
}
|
||||
|
||||
}
|
465
sites/all/modules/contrib/search/search_api/includes/service.inc
Normal file
465
sites/all/modules/contrib/search/search_api/includes/service.inc
Normal file
@@ -0,0 +1,465 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains SearchApiServiceInterface and SearchApiAbstractService.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Interface defining the methods search services have to implement.
|
||||
*
|
||||
* Before a service object is used, the corresponding server's data will be read
|
||||
* from the database (see SearchApiAbstractService for a list of fields).
|
||||
*
|
||||
* Most methods in this interface (where any change in data occurs) can throw a
|
||||
* SearchApiException. The server entity class SearchApiServer catches these
|
||||
* exceptions and uses the server tasks system to assure that the action is
|
||||
* later retried.
|
||||
*/
|
||||
interface SearchApiServiceInterface {
|
||||
|
||||
/**
|
||||
* Constructs a service object.
|
||||
*
|
||||
* This will set the server configuration used with this service.
|
||||
*
|
||||
* @param SearchApiServer $server
|
||||
* The server object for this service.
|
||||
*/
|
||||
public function __construct(SearchApiServer $server);
|
||||
|
||||
/**
|
||||
* Form constructor for the server configuration form.
|
||||
*
|
||||
* Might be called with an incomplete server (no ID). In this case, the form
|
||||
* is displayed for the initial creation of the server.
|
||||
*
|
||||
* @param array $form
|
||||
* The server options part of the form.
|
||||
* @param array $form_state
|
||||
* The current form state.
|
||||
*
|
||||
* @return array
|
||||
* A form array for setting service-specific options.
|
||||
*/
|
||||
public function configurationForm(array $form, array &$form_state);
|
||||
|
||||
/**
|
||||
* Validation callback for the form returned by configurationForm().
|
||||
*
|
||||
* $form_state['server'] will contain the server that is created or edited.
|
||||
* Use form_error() to flag errors on form elements.
|
||||
*
|
||||
* @param array $form
|
||||
* The form returned by configurationForm().
|
||||
* @param array $values
|
||||
* The part of the $form_state['values'] array corresponding to this form.
|
||||
* @param array $form_state
|
||||
* The complete form state.
|
||||
*/
|
||||
public function configurationFormValidate(array $form, array &$values, array &$form_state);
|
||||
|
||||
/**
|
||||
* Submit callback for the form returned by configurationForm().
|
||||
*
|
||||
* This method should set the options of this service' server according to
|
||||
* $values.
|
||||
*
|
||||
* @param array $form
|
||||
* The form returned by configurationForm().
|
||||
* @param array $values
|
||||
* The part of the $form_state['values'] array corresponding to this form.
|
||||
* @param array $form_state
|
||||
* The complete form state.
|
||||
*/
|
||||
public function configurationFormSubmit(array $form, array &$values, array &$form_state);
|
||||
|
||||
/**
|
||||
* Determines whether this service class supports a given feature.
|
||||
*
|
||||
* Features are optional extensions to Search API functionality and usually
|
||||
* defined and used by third-party modules.
|
||||
*
|
||||
* There are currently three features defined directly in the Search API
|
||||
* project:
|
||||
* - "search_api_facets", by the search_api_facetapi module.
|
||||
* - "search_api_facets_operator_or", also by the search_api_facetapi module.
|
||||
* - "search_api_mlt", by the search_api_views module.
|
||||
* Other contrib modules might define additional features. These should always
|
||||
* be properly documented in the module by which they are defined.
|
||||
*
|
||||
* @param string $feature
|
||||
* The name of the optional feature.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if this service knows and supports the specified feature. FALSE
|
||||
* otherwise.
|
||||
*/
|
||||
public function supportsFeature($feature);
|
||||
|
||||
/**
|
||||
* Displays this server's settings.
|
||||
*
|
||||
* Output can be HTML or a render array, a <dl> listing all relevant settings
|
||||
* is preferred.
|
||||
*/
|
||||
public function viewSettings();
|
||||
|
||||
/**
|
||||
* Reacts to the server's creation.
|
||||
*
|
||||
* Called once, when the server is first created. Allows it to set up its
|
||||
* necessary infrastructure.
|
||||
*/
|
||||
public function postCreate();
|
||||
|
||||
/**
|
||||
* Notifies this server that its fields are about to be updated.
|
||||
*
|
||||
* The server's $original property can be used to inspect the old property
|
||||
* values.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE, if the update requires reindexing of all content on the server.
|
||||
*/
|
||||
public function postUpdate();
|
||||
|
||||
/**
|
||||
* Notifies this server that it is about to be deleted from the database.
|
||||
*
|
||||
* This should execute any necessary cleanup operations.
|
||||
*
|
||||
* Note that you shouldn't call the server's save() method, or any
|
||||
* methods that might do that, from inside of this method as the server isn't
|
||||
* present in the database anymore at this point.
|
||||
*/
|
||||
public function preDelete();
|
||||
|
||||
/**
|
||||
* Adds a new index to this server.
|
||||
*
|
||||
* If the index was already added to the server, the object should treat this
|
||||
* as if removeIndex() and then addIndex() were called.
|
||||
*
|
||||
* @param SearchApiIndex $index
|
||||
* The index to add.
|
||||
*
|
||||
* @throws SearchApiException
|
||||
* If an error occurred while adding the index.
|
||||
*/
|
||||
public function addIndex(SearchApiIndex $index);
|
||||
|
||||
/**
|
||||
* Notifies the server that the field settings for the index have changed.
|
||||
*
|
||||
* If any user action is necessary as a result of this, the method should
|
||||
* use drupal_set_message() to notify the user.
|
||||
*
|
||||
* @param SearchApiIndex $index
|
||||
* The updated index.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE, if this change affected the server in any way that forces it to
|
||||
* re-index the content. FALSE otherwise.
|
||||
*
|
||||
* @throws SearchApiException
|
||||
* If an error occurred while reacting to the change of fields.
|
||||
*/
|
||||
public function fieldsUpdated(SearchApiIndex $index);
|
||||
|
||||
/**
|
||||
* Removes an index from this server.
|
||||
*
|
||||
* This might mean that the index has been deleted, or reassigned to a
|
||||
* different server. If you need to distinguish between these cases, inspect
|
||||
* $index->server.
|
||||
*
|
||||
* If the index wasn't added to the server, the method call should be ignored.
|
||||
*
|
||||
* Implementations of this method should also check whether $index->read_only
|
||||
* is set, and don't delete any indexed data if it is.
|
||||
*
|
||||
* @param $index
|
||||
* Either an object representing the index to remove, or its machine name
|
||||
* (if the index was completely deleted).
|
||||
*
|
||||
* @throws SearchApiException
|
||||
* If an error occurred while removing the index.
|
||||
*/
|
||||
public function removeIndex($index);
|
||||
|
||||
/**
|
||||
* Indexes the specified items.
|
||||
*
|
||||
* @param SearchApiIndex $index
|
||||
* The search index for which items should be indexed.
|
||||
* @param array $items
|
||||
* An array of items to be indexed, keyed by their id. The values are
|
||||
* associative arrays of the fields to be stored, where each field is an
|
||||
* array with the following keys:
|
||||
* - type: One of the data types recognized by the Search API, or the
|
||||
* special type "tokens" for fulltext fields.
|
||||
* - original_type: The original type of the property, as defined by the
|
||||
* datasource controller for the index's item type.
|
||||
* - value: The value to index.
|
||||
*
|
||||
* The special field "search_api_language" contains the item's language and
|
||||
* should always be indexed.
|
||||
*
|
||||
* The value of fields with the "tokens" type is an array of tokens. Each
|
||||
* token is an array containing the following keys:
|
||||
* - value: The word that the token represents.
|
||||
* - score: A score for the importance of that word.
|
||||
*
|
||||
* @return array
|
||||
* An array of the ids of all items that were successfully indexed.
|
||||
*
|
||||
* @throws SearchApiException
|
||||
* If indexing was prevented by a fundamental configuration error.
|
||||
*/
|
||||
public function indexItems(SearchApiIndex $index, array $items);
|
||||
|
||||
/**
|
||||
* Deletes indexed items from this server.
|
||||
*
|
||||
* Might be either used to delete some items (given by their ids) from a
|
||||
* specified index, or all items from that index, or all items from all
|
||||
* indexes on this server.
|
||||
*
|
||||
* @param $ids
|
||||
* Either an array containing the ids of the items that should be deleted,
|
||||
* or 'all' if all items should be deleted. Other formats might be
|
||||
* recognized by implementing classes, but these are not standardized.
|
||||
* @param SearchApiIndex $index
|
||||
* The index from which items should be deleted, or NULL if all indexes on
|
||||
* this server should be cleared (then, $ids has to be 'all').
|
||||
*
|
||||
* @throws SearchApiException
|
||||
* If an error occurred while trying to delete the items.
|
||||
*/
|
||||
public function deleteItems($ids = 'all', SearchApiIndex $index = NULL);
|
||||
|
||||
/**
|
||||
* Creates a query object for searching on an index lying on this server.
|
||||
*
|
||||
* @param SearchApiIndex $index
|
||||
* The index to search on.
|
||||
* @param $options
|
||||
* Associative array of options configuring this query. See
|
||||
* SearchApiQueryInterface::__construct().
|
||||
*
|
||||
* @return SearchApiQueryInterface
|
||||
* An object for searching the given index.
|
||||
*
|
||||
* @throws SearchApiException
|
||||
* If the server is currently disabled.
|
||||
*/
|
||||
public function query(SearchApiIndex $index, $options = array());
|
||||
|
||||
/**
|
||||
* Executes a search on the server represented by this object.
|
||||
*
|
||||
* @param $query
|
||||
* The SearchApiQueryInterface object to execute.
|
||||
*
|
||||
* @return array
|
||||
* An associative array containing the search results, as required by
|
||||
* SearchApiQueryInterface::execute().
|
||||
*
|
||||
* @throws SearchApiException
|
||||
* If an error prevented the search from completing.
|
||||
*/
|
||||
public function search(SearchApiQueryInterface $query);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract class with generic implementation of most service methods.
|
||||
*
|
||||
* For creating your own service class extending this class, you only need to
|
||||
* implement indexItems(), deleteItems() and search() from the
|
||||
* SearchApiServiceInterface interface.
|
||||
*/
|
||||
abstract class SearchApiAbstractService implements SearchApiServiceInterface {
|
||||
|
||||
/**
|
||||
* @var SearchApiServer
|
||||
*/
|
||||
protected $server;
|
||||
|
||||
/**
|
||||
* Direct reference to the server's $options property.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $options = array();
|
||||
|
||||
/**
|
||||
* Implements SearchApiServiceInterface::__construct().
|
||||
*
|
||||
* The default implementation sets $this->server and $this->options.
|
||||
*/
|
||||
public function __construct(SearchApiServer $server) {
|
||||
$this->server = $server;
|
||||
$this->options = &$server->options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements SearchApiServiceInterface::__construct().
|
||||
*
|
||||
* Returns an empty form by default.
|
||||
*/
|
||||
public function configurationForm(array $form, array &$form_state) {
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements SearchApiServiceInterface::__construct().
|
||||
*
|
||||
* Does nothing by default.
|
||||
*/
|
||||
public function configurationFormValidate(array $form, array &$values, array &$form_state) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements SearchApiServiceInterface::__construct().
|
||||
*
|
||||
* The default implementation just ensures that additional elements in
|
||||
* $options, not present in the form, don't get lost at the update.
|
||||
*/
|
||||
public function configurationFormSubmit(array $form, array &$values, array &$form_state) {
|
||||
if (!empty($this->options)) {
|
||||
$values += $this->options;
|
||||
}
|
||||
$this->options = $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements SearchApiServiceInterface::__construct().
|
||||
*
|
||||
* The default implementation always returns FALSE.
|
||||
*/
|
||||
public function supportsFeature($feature) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements SearchApiServiceInterface::__construct().
|
||||
*
|
||||
* The default implementation does a crude output as a definition list, with
|
||||
* option names taken from the configuration form.
|
||||
*/
|
||||
public function viewSettings() {
|
||||
$output = '';
|
||||
$form = $form_state = array();
|
||||
$option_form = $this->configurationForm($form, $form_state);
|
||||
$option_names = array();
|
||||
foreach ($option_form as $key => $element) {
|
||||
if (isset($element['#title']) && isset($this->options[$key])) {
|
||||
$option_names[$key] = $element['#title'];
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($option_names as $key => $name) {
|
||||
$value = $this->options[$key];
|
||||
$output .= '<dt>' . check_plain($name) . '</dt>' . "\n";
|
||||
$output .= '<dd>' . nl2br(check_plain(print_r($value, TRUE))) . '</dd>' . "\n";
|
||||
}
|
||||
|
||||
return $output ? "<dl>\n$output</dl>" : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns additional, service-specific information about this server.
|
||||
*
|
||||
* If a service class implements this method and supports the
|
||||
* "search_api_service_extra" option, this method will be used to add extra
|
||||
* information to the server's "View" tab.
|
||||
*
|
||||
* In the default theme implementation this data will be output in a table
|
||||
* with two columns along with other, generic information about the server.
|
||||
*
|
||||
* @return array
|
||||
* An array of additional server information, with each piece of information
|
||||
* being an associative array with the following keys:
|
||||
* - label: The human-readable label for this data.
|
||||
* - info: The information, as HTML.
|
||||
* - status: (optional) The status associated with this information. One of
|
||||
* "info", "ok", "warning" or "error". Defaults to "info".
|
||||
*
|
||||
* @see supportsFeature()
|
||||
*/
|
||||
public function getExtraInformation() {
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements SearchApiServiceInterface::__construct().
|
||||
*
|
||||
* Does nothing, by default.
|
||||
*/
|
||||
public function postCreate() {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements SearchApiServiceInterface::__construct().
|
||||
*
|
||||
* The default implementation always returns FALSE.
|
||||
*/
|
||||
public function postUpdate() {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements SearchApiServiceInterface::__construct().
|
||||
*
|
||||
* By default, deletes all indexes from this server.
|
||||
*/
|
||||
public function preDelete() {
|
||||
$indexes = search_api_index_load_multiple(FALSE, array('server' => $this->server->machine_name));
|
||||
foreach ($indexes as $index) {
|
||||
$this->removeIndex($index);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements SearchApiServiceInterface::__construct().
|
||||
*
|
||||
* Does nothing, by default.
|
||||
*/
|
||||
public function addIndex(SearchApiIndex $index) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements SearchApiServiceInterface::__construct().
|
||||
*
|
||||
* The default implementation always returns FALSE.
|
||||
*/
|
||||
public function fieldsUpdated(SearchApiIndex $index) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements SearchApiServiceInterface::__construct().
|
||||
*
|
||||
* By default, removes all items from that index.
|
||||
*/
|
||||
public function removeIndex($index) {
|
||||
if (is_object($index) && empty($index->read_only)) {
|
||||
$this->deleteItems('all', $index);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements SearchApiServiceInterface::__construct().
|
||||
*
|
||||
* The default implementation returns a SearchApiQuery object.
|
||||
*/
|
||||
public function query(SearchApiIndex $index, $options = array()) {
|
||||
return new SearchApiQuery($index, $options);
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user