first import
This commit is contained in:
220
sites/all/modules/search_api/includes/callback.inc
Normal file
220
sites/all/modules/search_api/includes/callback.inc
Normal file
@@ -0,0 +1,220 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 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 "Workflow" 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 (or can be) added to items with this
|
||||
* callback. If a property with this name 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.
|
||||
*
|
||||
* @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, implementing 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;
|
||||
|
||||
/**
|
||||
* 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()) {
|
||||
$this->index = $index;
|
||||
$this->options = $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether this data-alter callback is applicable for a certain index.
|
||||
*
|
||||
* This can be used for hiding the callback on the index's "Workflow" 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.
|
||||
*
|
||||
* The default implementation always returns TRUE.
|
||||
*
|
||||
* @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 TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
$this->options = $values;
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Declare the properties that are (or can be) added to items with this
|
||||
* callback. If a property with this name 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.
|
||||
*
|
||||
* @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() {
|
||||
return array();
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,313 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 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] = $field['name'];
|
||||
}
|
||||
$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(
|
||||
'#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'];
|
||||
}
|
243
sites/all/modules/search_api/includes/callback_add_hierarchy.inc
Normal file
243
sites/all/modules/search_api/includes/callback_add_hierarchy.inc
Normal file
@@ -0,0 +1,243 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Search API data alteration callback that adds an URL field for all items.
|
||||
*/
|
||||
class SearchApiAlterAddHierarchy extends SearchApiAbstractAlterCallback {
|
||||
|
||||
/**
|
||||
* Cached value for the hierarchical field options.
|
||||
*
|
||||
* @var array
|
||||
*
|
||||
* @see getHierarchicalFields()
|
||||
*/
|
||||
protected $field_options;
|
||||
|
||||
/**
|
||||
* Enable this data alteration only if any hierarchical fields are available.
|
||||
*
|
||||
* @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 (bool) $this->getHierarchicalFields();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
$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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
// 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, $prop) = 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, $prop) = 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
if (empty($this->options['fields'])) {
|
||||
return array();
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Declare the properties that are (or can be) added to items with this
|
||||
* callback. If a property with this name 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.
|
||||
*
|
||||
* @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() {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for finding all hierarchical fields of an index's type.
|
||||
*
|
||||
* @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;
|
||||
}
|
||||
$v = $wrapper->value(array('identifier' => TRUE));
|
||||
if ($v && !isset($values[$v])) {
|
||||
$values[$v] = $v;
|
||||
if (isset($wrapper->$property) && $wrapper->$property->value()) {
|
||||
$this->extractHierarchy($wrapper->$property, $property, $values);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
29
sites/all/modules/search_api/includes/callback_add_url.inc
Normal file
29
sites/all/modules/search_api/includes/callback_add_url.inc
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 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 $id => &$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,98 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 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) entity_get_info($index->item_type);
|
||||
}
|
||||
|
||||
public function configurationForm() {
|
||||
$info = entity_get_info($this->index->item_type);
|
||||
$view_modes = array();
|
||||
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->item_type;
|
||||
$mode = empty($this->options['mode']) ? 'full' : $this->options['mode'];
|
||||
foreach ($items as $id => &$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,72 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Search API data alteration callback that filters out items based on their
|
||||
* bundle.
|
||||
*/
|
||||
class SearchApiAlterBundleFilter extends SearchApiAbstractAlterCallback {
|
||||
|
||||
public function supportsIndex(SearchApiIndex $index) {
|
||||
return ($info = entity_get_info($index->item_type)) && self::hasBundles($info);
|
||||
}
|
||||
|
||||
public function alterItems(array &$items) {
|
||||
$info = entity_get_info($this->index->item_type);
|
||||
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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function configurationForm() {
|
||||
$info = entity_get_info($this->index->item_type);
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for figuring out if the entities with the given entity info
|
||||
* can be filtered by bundle.
|
||||
*/
|
||||
protected static function hasBundles(array $entity_info) {
|
||||
return !empty($entity_info['entity keys']['bundle']) && !empty($entity_info['bundles']);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Search API data alteration callback that filters out items based on their
|
||||
* bundle.
|
||||
*/
|
||||
class SearchApiAlterLanguageControl extends SearchApiAbstractAlterCallback {
|
||||
|
||||
/**
|
||||
* 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()) {
|
||||
$options += array(
|
||||
'lang_field' => '',
|
||||
'languages' => array(),
|
||||
);
|
||||
parent::__construct($index, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether this data-alter callback is applicable for a certain index.
|
||||
*
|
||||
* Only returns TRUE if the system is multilingual.
|
||||
*
|
||||
* @param SearchApiIndex $index
|
||||
* The index to check for.
|
||||
*
|
||||
* @return boolean
|
||||
* TRUE if the callback can run on the given index; FALSE otherwise.
|
||||
*
|
||||
* @see drupal_multilingual()
|
||||
*/
|
||||
public function supportsIndex(SearchApiIndex $index) {
|
||||
return drupal_multilingual();
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a form for configuring this data alteration.
|
||||
*
|
||||
* @return array
|
||||
* A form array for configuring this data alteration.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
$values['languages'] = array_filter($values['languages']);
|
||||
return parent::configurationFormSubmit($form, $values, $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) {
|
||||
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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
109
sites/all/modules/search_api/includes/callback_node_access.inc
Normal file
109
sites/all/modules/search_api/includes/callback_node_access.inc
Normal file
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
/**
|
||||
* @file
|
||||
* Contains the SearchApiAlterNodeAccess class.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Adds node access information to node indexes.
|
||||
*/
|
||||
class SearchApiAlterNodeAccess 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) {
|
||||
// Currently only node access is supported.
|
||||
return $index->item_type === 'node';
|
||||
}
|
||||
|
||||
/**
|
||||
* Declare the properties that are (or can be) added to items with this callback.
|
||||
*
|
||||
* Adds the "search_api_access_node" property.
|
||||
*
|
||||
* @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() {
|
||||
return array(
|
||||
'search_api_access_node' => array(
|
||||
'label' => t('Node access information'),
|
||||
'description' => t('Data needed to apply node access.'),
|
||||
'type' => 'list<token>',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
static $account;
|
||||
|
||||
if (!isset($account)) {
|
||||
// Load the anonymous user.
|
||||
$account = drupal_anonymous_user();
|
||||
}
|
||||
|
||||
foreach ($items as $nid => &$item) {
|
||||
// Check whether all users have access to the node.
|
||||
if (!node_access('view', $item, $account)) {
|
||||
// Get node access grants.
|
||||
$result = db_query('SELECT * FROM {node_access} WHERE (nid = 0 OR nid = :nid) AND grant_view = 1', array(':nid' => $item->nid));
|
||||
|
||||
// Store all grants together with it's realms in the item.
|
||||
foreach ($result as $grant) {
|
||||
if (!isset($items[$nid]->search_api_access_node)) {
|
||||
$items[$nid]->search_api_access_node = array();
|
||||
}
|
||||
$items[$nid]->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[$nid]->search_api_access_node = array('node_access__all');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit callback for the configuration form.
|
||||
*
|
||||
* 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->item_type === '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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
698
sites/all/modules/search_api/includes/datasource.inc
Normal file
698
sites/all/modules/search_api/includes/datasource.inc
Normal file
@@ -0,0 +1,698 @@
|
||||
<?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.
|
||||
*
|
||||
* All methods of the data source may throw exceptions of type
|
||||
* SearchApiDataSourceException if any exception or error state is encountered.
|
||||
*/
|
||||
interface SearchApiDataSourceControllerInterface {
|
||||
|
||||
/**
|
||||
* Constructor for a data source controller.
|
||||
*
|
||||
* @param $type
|
||||
* The item type for which this controller is created.
|
||||
*/
|
||||
public function __construct($type);
|
||||
|
||||
/**
|
||||
* Return 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.
|
||||
*/
|
||||
public function getIdFieldInfo();
|
||||
|
||||
/**
|
||||
* Load 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.
|
||||
*/
|
||||
public function loadItems(array $ids);
|
||||
|
||||
/**
|
||||
* Get a metadata wrapper for the item type of this data source controller.
|
||||
*
|
||||
* @param $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.
|
||||
*
|
||||
* @see entity_metadata_wrapper()
|
||||
*/
|
||||
public function getMetadataWrapper($item = NULL, array $info = array());
|
||||
|
||||
/**
|
||||
* Get the unique ID of an item.
|
||||
*
|
||||
* @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);
|
||||
|
||||
/**
|
||||
* Get a human-readable label for an item.
|
||||
*
|
||||
* @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);
|
||||
|
||||
/**
|
||||
* Get a URL at which the item can be viewed on the web.
|
||||
*
|
||||
* @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);
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
/**
|
||||
* Set 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 $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);
|
||||
|
||||
/**
|
||||
* Set 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 $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 of the indexes doesn't use the same item type as this controller.
|
||||
*/
|
||||
public function trackItemQueued($item_ids, SearchApiIndex $index);
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Default base class for the SearchApiDataSourceControllerInterface.
|
||||
*
|
||||
* 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 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';
|
||||
|
||||
/**
|
||||
* Constructor for a data source controller.
|
||||
*
|
||||
* @param $type
|
||||
* The item type for which this controller is created.
|
||||
*/
|
||||
public function __construct($type) {
|
||||
$this->type = $type;
|
||||
$this->info = search_api_get_item_type_info($type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a metadata wrapper for the item type of this data source controller.
|
||||
*
|
||||
* @param $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.
|
||||
*
|
||||
* @see entity_metadata_wrapper()
|
||||
*/
|
||||
public function getMetadataWrapper($item = NULL, array $info = array()) {
|
||||
$info += $this->getPropertyInfo();
|
||||
return entity_metadata_wrapper($this->type, $item, $info);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method that can be used by subclasses to specify the property
|
||||
* information to use when creating a metadata wrapper.
|
||||
*
|
||||
* @return array
|
||||
* Property information as specified by hook_entity_property_info().
|
||||
*
|
||||
* @see hook_entity_property_info()
|
||||
*/
|
||||
protected function getPropertyInfo() {
|
||||
throw new SearchApiDataSourceException(t('No known property information for type @type.', array('@type' => $this->type)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the unique ID of an item.
|
||||
*
|
||||
* @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) {
|
||||
$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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a human-readable label for an item.
|
||||
*
|
||||
* @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) {
|
||||
$label = $this->getMetadataWrapper($item)->label();
|
||||
return $label ? $label : NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a URL at which the item can be viewed on the web.
|
||||
*
|
||||
* @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) {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method that can be used by subclasses instead of implementing startTracking().
|
||||
*
|
||||
* Returns the IDs of all items that are known for this controller's type.
|
||||
*
|
||||
* @return array
|
||||
* An array containing all item IDs for this type.
|
||||
*/
|
||||
protected function getAllItemIds() {
|
||||
throw new SearchApiDataSourceException(t('Items not known for type @type.', array('@type' => $this->type)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
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);
|
||||
$query = db_delete($this->table)
|
||||
->condition($this->indexIdColumn, $index->id)
|
||||
->execute();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set 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 $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) {
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set 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 $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 of the indexes doesn't use the same item type as this controller.
|
||||
*/
|
||||
public function trackItemQueued($item_ids, SearchApiIndex $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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for ensuring that an index uses the same item type as this controller.
|
||||
*
|
||||
* @param SearchApiIndex $index
|
||||
* The index to check.
|
||||
*
|
||||
* @throws SearchApiDataSourceException
|
||||
* If the index doesn't use the same type as this 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
206
sites/all/modules/search_api/includes/datasource_entity.inc
Normal file
206
sites/all/modules/search_api/includes/datasource_entity.inc
Normal file
@@ -0,0 +1,206 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains the SearchApiEntityDataSourceController class.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Data source for all entities known to the Entity API.
|
||||
*/
|
||||
class SearchApiEntityDataSourceController extends SearchApiAbstractDataSourceController {
|
||||
|
||||
/**
|
||||
* Return 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.
|
||||
*/
|
||||
public function getIdFieldInfo() {
|
||||
$info = entity_get_info($this->type);
|
||||
$properties = entity_get_property_info($this->type);
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load 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.
|
||||
*/
|
||||
public function loadItems(array $ids) {
|
||||
$items = entity_load($this->type, $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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a metadata wrapper for the item type of this data source controller.
|
||||
*
|
||||
* @param $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.
|
||||
*
|
||||
* @see entity_metadata_wrapper()
|
||||
*/
|
||||
public function getMetadataWrapper($item = NULL, array $info = array()) {
|
||||
return entity_metadata_wrapper($this->type, $item, $info);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the unique ID of an item.
|
||||
*
|
||||
* @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) {
|
||||
$id = entity_id($this->type, $item);
|
||||
return $id ? $id : NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a human-readable label for an item.
|
||||
*
|
||||
* @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) {
|
||||
$label = entity_label($this->type, $item);
|
||||
return $label ? $label : NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a URL at which the item can be viewed on the web.
|
||||
*
|
||||
* @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) {
|
||||
if ($this->type == 'file') {
|
||||
return array(
|
||||
'path' => file_create_url($item->uri),
|
||||
'options' => array(
|
||||
'entity_type' => 'file',
|
||||
'entity' => $item,
|
||||
),
|
||||
);
|
||||
}
|
||||
$url = entity_uri($this->type, $item);
|
||||
return $url ? $url : 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) {
|
||||
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->type);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method that can be used by subclasses instead of implementing startTracking().
|
||||
*
|
||||
* Returns the IDs of all items that are known for this controller's type.
|
||||
*
|
||||
* Will be used when the entity type doesn't specify a "base table".
|
||||
*
|
||||
* @return array
|
||||
* An array containing all item IDs for this type.
|
||||
*/
|
||||
protected function getAllItemIds() {
|
||||
return array_keys(entity_load($this->type));
|
||||
}
|
||||
|
||||
}
|
268
sites/all/modules/search_api/includes/datasource_external.inc
Normal file
268
sites/all/modules/search_api/includes/datasource_external.inc
Normal file
@@ -0,0 +1,268 @@
|
||||
<?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. To make this use case easier, there is also a
|
||||
* getFieldInformation() method which you can implement instead of directly
|
||||
* implementing getPropertyInfo().
|
||||
*/
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method that can be used by subclasses to specify the property
|
||||
* information to use when creating a metadata wrapper.
|
||||
*
|
||||
* For most use cases, you will have to override this method to provide the
|
||||
* real property information for your item type.
|
||||
*
|
||||
* @return array
|
||||
* Property information as specified by hook_entity_property_info().
|
||||
*
|
||||
* @see hook_entity_property_info()
|
||||
*/
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
29
sites/all/modules/search_api/includes/exception.inc
Normal file
29
sites/all/modules/search_api/includes/exception.inc
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
|
||||
}
|
936
sites/all/modules/search_api/includes/index_entity.inc
Normal file
936
sites/all/modules/search_api/includes/index_entity.inc
Normal file
@@ -0,0 +1,936 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
$server = $this->server();
|
||||
if ($server) {
|
||||
// Tell the server about the new index.
|
||||
if ($server->enabled) {
|
||||
$server->addIndex($this);
|
||||
}
|
||||
else {
|
||||
$tasks = variable_get('search_api_tasks', array());
|
||||
// When we add or remove an index, we can ignore all other tasks.
|
||||
$tasks[$server->machine_name][$this->machine_name] = array('add');
|
||||
variable_set('search_api_tasks', $tasks);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute necessary tasks when the index is removed from the database.
|
||||
*/
|
||||
public function postDelete() {
|
||||
if ($server = $this->server()) {
|
||||
if ($server->enabled) {
|
||||
$server->removeIndex($this);
|
||||
}
|
||||
else {
|
||||
$tasks = variable_get('search_api_tasks', array());
|
||||
$tasks[$server->machine_name][$this->machine_name] = array('remove');
|
||||
variable_set('search_api_tasks', $tasks);
|
||||
}
|
||||
}
|
||||
|
||||
// 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));
|
||||
_search_api_empty_cron_queue($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves this index to the database, either creating a new record or updating
|
||||
* an existing one.
|
||||
*
|
||||
* @return
|
||||
* 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;
|
||||
}
|
||||
|
||||
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
|
||||
* 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
|
||||
* 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
|
||||
* TRUE on success, FALSE on failure.
|
||||
*/
|
||||
public function clear() {
|
||||
if (!$this->server || $this->read_only) {
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
$server = $this->server();
|
||||
if ($server->enabled) {
|
||||
$server->deleteItems('all', $this);
|
||||
}
|
||||
else {
|
||||
$tasks = variable_get('search_api_tasks', array());
|
||||
// If the index was cleared or newly added since the server was last enabled, we don't need to do anything.
|
||||
if (!isset($tasks[$server->machine_name][$this->machine_name])
|
||||
|| (array_search('add', $tasks[$server->machine_name][$this->machine_name]) === FALSE
|
||||
&& array_search('clear', $tasks[$server->machine_name][$this->machine_name]) === FALSE)) {
|
||||
$tasks[$server->machine_name][$this->machine_name][] = 'clear';
|
||||
variable_set('search_api_tasks', $tasks);
|
||||
}
|
||||
}
|
||||
|
||||
_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 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills the $processors array for use by the pre-/postprocessing functions.
|
||||
*
|
||||
* @return SearchApiIndex
|
||||
* The called object.
|
||||
* @return array
|
||||
* All enabled callbacks for this index, as SearchApiAlterCallbackInterface
|
||||
* objects.
|
||||
*/
|
||||
protected 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
* All enabled processors for this index, as SearchApiProcessorInterface
|
||||
* objects.
|
||||
*/
|
||||
protected 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) {
|
||||
$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) {
|
||||
return $flat;
|
||||
}
|
||||
$options = array();
|
||||
$options['fields'] = $flat;
|
||||
$options['additional fields'] = $additional;
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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->fulltext_fields = array();
|
||||
}
|
||||
|
||||
}
|
418
sites/all/modules/search_api/includes/processor.inc
Normal file
418
sites/all/modules/search_api/includes/processor.inc
Normal file
@@ -0,0 +1,418 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 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 "Workflow" 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_combine($fields, array_fill(0, count($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)) {
|
||||
$type = 'tokens';
|
||||
$value = $this->normalizeTokens($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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 &$f) {
|
||||
if (is_array($f)) {
|
||||
if (isset($fields[$f[0]]) && $this->testField($f[0], $fields[$f[0]])) {
|
||||
$this->processFilterValue($f[1]);
|
||||
}
|
||||
}
|
||||
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) {
|
||||
|
||||
}
|
||||
|
||||
}
|
137
sites/all/modules/search_api/includes/processor_html_filter.inc
Normal file
137
sites/all/modules/search_api/includes/processor_html_filter.inc
Normal file
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 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,12 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Processor for making searches case-insensitive.
|
||||
*/
|
||||
class SearchApiIgnoreCase extends SearchApiAbstractProcessor {
|
||||
|
||||
protected function process(&$value) {
|
||||
$value = drupal_strtolower($value);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Processor for removing stopwords from index and search terms.
|
||||
*/
|
||||
class SearchApiStopWords extends SearchApiAbstractProcessor {
|
||||
|
||||
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 URI'),
|
||||
'#title' => t('Enter the URI of your stopwords.txt 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);
|
||||
|
||||
$stopwords = trim($values['stopwords']);
|
||||
$uri = $values['file'];
|
||||
if (empty($stopwords) && empty($uri)) {
|
||||
$el = $form['file'];
|
||||
form_error($el, $el['#title'] . ': ' . t('At stopwords file or words are required.'));
|
||||
}
|
||||
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)) {
|
||||
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 postprocessSearchResults(array &$response, SearchApiQuery $query) {
|
||||
if (isset($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,95 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 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();
|
||||
$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) {
|
||||
$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']);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
1066
sites/all/modules/search_api/includes/query.inc
Normal file
1066
sites/all/modules/search_api/includes/query.inc
Normal file
File diff suppressed because it is too large
Load Diff
228
sites/all/modules/search_api/includes/server_entity.inc
Normal file
228
sites/all/modules/search_api/includes/server_entity.inc
Normal file
@@ -0,0 +1,228 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 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
|
||||
* 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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
||||
public function configurationForm(array $form, array &$form_state) {
|
||||
$this->ensureProxy();
|
||||
return $this->proxy->configurationForm($form, $form_state);
|
||||
}
|
||||
|
||||
public function configurationFormValidate(array $form, array &$values, array &$form_state) {
|
||||
$this->ensureProxy();
|
||||
return $this->proxy->configurationFormValidate($form, $values, $form_state);
|
||||
}
|
||||
|
||||
public function configurationFormSubmit(array $form, array &$values, array &$form_state) {
|
||||
$this->ensureProxy();
|
||||
return $this->proxy->configurationFormSubmit($form, $values, $form_state);
|
||||
}
|
||||
|
||||
public function supportsFeature($feature) {
|
||||
$this->ensureProxy();
|
||||
return $this->proxy->supportsFeature($feature);
|
||||
}
|
||||
|
||||
public function viewSettings() {
|
||||
$this->ensureProxy();
|
||||
return $this->proxy->viewSettings();
|
||||
}
|
||||
|
||||
public function postCreate() {
|
||||
$this->ensureProxy();
|
||||
return $this->proxy->postCreate();
|
||||
}
|
||||
|
||||
public function postUpdate() {
|
||||
$this->ensureProxy();
|
||||
return $this->proxy->postUpdate();
|
||||
}
|
||||
|
||||
public function preDelete() {
|
||||
$this->ensureProxy();
|
||||
return $this->proxy->preDelete();
|
||||
}
|
||||
|
||||
public function addIndex(SearchApiIndex $index) {
|
||||
$this->ensureProxy();
|
||||
return $this->proxy->addIndex($index);
|
||||
}
|
||||
|
||||
public function fieldsUpdated(SearchApiIndex $index) {
|
||||
$this->ensureProxy();
|
||||
return $this->proxy->fieldsUpdated($index);
|
||||
}
|
||||
|
||||
public function removeIndex($index) {
|
||||
$this->ensureProxy();
|
||||
return $this->proxy->removeIndex($index);
|
||||
}
|
||||
|
||||
public function indexItems(SearchApiIndex $index, array $items) {
|
||||
$this->ensureProxy();
|
||||
return $this->proxy->indexItems($index, $items);
|
||||
}
|
||||
|
||||
public function deleteItems($ids = 'all', SearchApiIndex $index = NULL) {
|
||||
$this->ensureProxy();
|
||||
return $this->proxy->deleteItems($ids, $index);
|
||||
}
|
||||
|
||||
public function query(SearchApiIndex $index, $options = array()) {
|
||||
$this->ensureProxy();
|
||||
return $this->proxy->query($index, $options);
|
||||
}
|
||||
|
||||
public function search(SearchApiQueryInterface $query) {
|
||||
$this->ensureProxy();
|
||||
return $this->proxy->search($query);
|
||||
}
|
||||
|
||||
}
|
469
sites/all/modules/search_api/includes/service.inc
Normal file
469
sites/all/modules/search_api/includes/service.inc
Normal file
@@ -0,0 +1,469 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 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).
|
||||
*/
|
||||
interface SearchApiServiceInterface {
|
||||
|
||||
/**
|
||||
* Constructor for a service class, setting the server configuration used with
|
||||
* this service.
|
||||
*
|
||||
* @param SearchApiServer $server
|
||||
* The server object for this service.
|
||||
*/
|
||||
public function __construct(SearchApiServer $server);
|
||||
|
||||
/**
|
||||
* Form callback. Might be called on an uninitialized object - in this case,
|
||||
* the form is for configuring a newly created server.
|
||||
*
|
||||
* @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 implementation 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 boolean
|
||||
* TRUE if this service knows and supports the specified feature. FALSE
|
||||
* otherwise.
|
||||
*/
|
||||
public function supportsFeature($feature);
|
||||
|
||||
/**
|
||||
* View this server's settings. Output can be HTML or a render array, a <dl>
|
||||
* listing all relevant settings is preferred.
|
||||
*/
|
||||
public function viewSettings();
|
||||
|
||||
/**
|
||||
* 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
|
||||
* 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 and
|
||||
* should therefore clean up, if appropriate.
|
||||
*
|
||||
* 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();
|
||||
|
||||
/**
|
||||
* Add 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.
|
||||
*/
|
||||
public function addIndex(SearchApiIndex $index);
|
||||
|
||||
/**
|
||||
* Notify the server that the indexed 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
|
||||
* TRUE, if this change affected the server in any way that forces it to
|
||||
* re-index the content. FALSE otherwise.
|
||||
*/
|
||||
public function fieldsUpdated(SearchApiIndex $index);
|
||||
|
||||
/**
|
||||
* Remove 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.
|
||||
*
|
||||
* @param $index
|
||||
* Either an object representing the index to remove, or its machine name
|
||||
* (if the index was completely deleted).
|
||||
*/
|
||||
public function removeIndex($index);
|
||||
|
||||
/**
|
||||
* Index 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);
|
||||
|
||||
/**
|
||||
* Delete items from an index on 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').
|
||||
*/
|
||||
public function deleteItems($ids = 'all', SearchApiIndex $index = NULL);
|
||||
|
||||
/**
|
||||
* Create 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.
|
||||
*/
|
||||
abstract class SearchApiAbstractService implements SearchApiServiceInterface {
|
||||
|
||||
/**
|
||||
* @var SearchApiServer
|
||||
*/
|
||||
protected $server;
|
||||
|
||||
/**
|
||||
* Direct reference to the server's $options property.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $options = array();
|
||||
|
||||
/**
|
||||
* Constructor for a service class, setting the server configuration used with
|
||||
* this service.
|
||||
*
|
||||
* The default implementation sets $this->server and $this->options.
|
||||
*
|
||||
* @param SearchApiServer $server
|
||||
* The server object for this service.
|
||||
*/
|
||||
public function __construct(SearchApiServer $server) {
|
||||
$this->server = $server;
|
||||
$this->options = &$server->options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Form callback. Might be called on an uninitialized object - in this case,
|
||||
* the form is for configuring a newly created server.
|
||||
*
|
||||
* Returns an empty form by default.
|
||||
*
|
||||
* @return array
|
||||
* A form array for setting service-specific options.
|
||||
*/
|
||||
public function configurationForm(array $form, array &$form_state) {
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation callback for the form returned by configurationForm().
|
||||
*
|
||||
* Does nothing by default.
|
||||
*
|
||||
* @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) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit callback for the form returned by configurationForm().
|
||||
*
|
||||
* The default implementation just ensures that additional elements in
|
||||
* $options, not present in the form, don't get lost at the update.
|
||||
*
|
||||
* @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) {
|
||||
if (!empty($this->options)) {
|
||||
$values += $this->options;
|
||||
}
|
||||
$this->options = $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether this service class implementation 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 boolean
|
||||
* TRUE if this service knows and supports the specified feature. FALSE
|
||||
* otherwise.
|
||||
*/
|
||||
public function supportsFeature($feature) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* View this server's settings. Output can be HTML or a render array, a <dl>
|
||||
* listing all relevant settings is preferred.
|
||||
*
|
||||
* 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>" : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Called once, when the server is first created. Allows it to set up its
|
||||
* necessary infrastructure.
|
||||
*
|
||||
* Does nothing, by default.
|
||||
*/
|
||||
public function postCreate() {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* TRUE, if the update requires reindexing of all content on the server.
|
||||
*/
|
||||
public function postUpdate() {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies this server that it is about to be deleted from the database and
|
||||
* should therefore clean up, if appropriate.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new index to this server.
|
||||
*
|
||||
* Does nothing, by default.
|
||||
*
|
||||
* @param SearchApiIndex $index
|
||||
* The index to add.
|
||||
*/
|
||||
public function addIndex(SearchApiIndex $index) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify the server that the indexed 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
|
||||
* TRUE, if this change affected the server in any way that forces it to
|
||||
* re-index the content. FALSE otherwise.
|
||||
*/
|
||||
public function fieldsUpdated(SearchApiIndex $index) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove 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.
|
||||
*
|
||||
* By default, removes all items from that index.
|
||||
*
|
||||
* @param $index
|
||||
* Either an object representing the index to remove, or its machine name
|
||||
* (if the index was completely deleted).
|
||||
*/
|
||||
public function removeIndex($index) {
|
||||
$this->deleteItems('all', $index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create 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()) {
|
||||
return new SearchApiQuery($index, $options);
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user