diff --git a/README.txt b/README.txt index 37ec0a3..9d9513c 100644 --- a/README.txt +++ b/README.txt @@ -288,6 +288,25 @@ includes/datasource_external.inc) which you can extend. For a minimal use case, you will then only have to define the available fields that can be retrieved by the server. +- Data type + Hook: hook_search_api_data_type_info() + +You can specify new data types for indexing fields. These new types can then be +selected on indexes' „Fields“ tabs. You just have to implement the hook, +returning some information on your data type, and specify in your module's +documentation the format of your data type and how it should be used. + +For a custom data type to have an effect, in most cases the server's service +class has to support that data type. A service class can advertize its support +of a data type by declaring support for the "search_api_data_type_TYPE" feature +in its supportsFeature() method. If this support isn't declared, a fallback data +type is automatically used instead of the custom one. + +If a field is indexed with a custom data type, its entry in the index's options +array will have the selected type in "real_type", while "type" contains the +fallback type (which is always one of the default data types, as returned by +search_api_default_field_types(). + - Data-alter callbacks Interface: SearchApiAlterCallbackInterface Base class: SearchApiAbstractAlterCallback diff --git a/includes/index_entity.inc b/includes/index_entity.inc index 35f0769..7441e28 100644 --- a/includes/index_entity.inc +++ b/includes/index_entity.inc @@ -111,10 +111,13 @@ class SearchApiIndex extends Entity { * - name: The human-readable name for the field. * - 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_field_types(). + * 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_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 @@ -416,11 +419,21 @@ class SearchApiIndex extends Entity { } $fields = $this->options['fields']; + $custom_type_fields = array(); foreach ($fields as $field => $info) { if (!$info['indexed']) { unset($fields[$field]); } - unset($fields[$field]['indexed']); + else { + 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']); + } + } + unset($fields[$field]['indexed']); + } } if (empty($fields)) { throw new SearchApiException(t("Couldn't index values on '!name' index (no fields selected)", array('!name' => $this->name))); @@ -445,6 +458,19 @@ class SearchApiIndex extends Entity { $data = array(); foreach ($items as $id => $item) { $data[$id] = search_api_extract_fields($this->entityWrapper($item), $fields); + 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); diff --git a/search_api.admin.inc b/search_api.admin.inc index 6854d83..4aba7a2 100644 --- a/search_api.admin.inc +++ b/search_api.admin.inc @@ -1459,6 +1459,7 @@ function search_api_admin_index_fields(array $form, array &$form_state, SearchAp $types = array(0 => search_api_field_types()); $fulltext_type = array(0 => 'text'); $entity_types = entity_get_info(); + $default_types = search_api_default_field_types(); $boosts = drupal_map_assoc(array('0.1', '0.2', '0.3', '0.5', '0.8', '1.0', '2.0', '3.0', '5.0', '8.0', '13.0', '21.0')); $form_state['index'] = $index; @@ -1493,7 +1494,7 @@ function search_api_admin_index_fields(array $form, array &$form_state, SearchAp '#default_value' => $info['indexed'], ); $inner_type = search_api_extract_inner_type($info['type']); - if (isset($types[0][$inner_type])) { + if (isset($default_types[$inner_type])) { // Determine the correct type options (i.e., with the correct nesting level). $level = search_api_list_nesting_level($info['type']); if (empty($types[$level])) { @@ -1510,7 +1511,7 @@ function search_api_admin_index_fields(array $form, array &$form_state, SearchAp $form['fields'][$key]['type'] = array( '#type' => 'select', '#options' => $types[$level], - '#default_value' => $info['type'], + '#default_value' => isset($info['real_type']) ? $info['real_type'] : $info['type'], '#states' => array( 'visible' => array( $css_key . '-indexed' => array('checked' => TRUE), @@ -1546,7 +1547,7 @@ function search_api_admin_index_fields(array $form, array &$form_state, SearchAp ); $form['fields'][$key]['entity_type'] = array( '#type' => 'value', - '#value' => search_api_extract_inner_type($info['type']), + '#value' => $inner_type, ); $form['fields'][$key]['type_name'] = array( '#markup' => check_plain($entity_types[$inner_type]['label']), @@ -1627,7 +1628,7 @@ function _search_api_admin_get_fields(SearchApiIndex $index, EntityMetadataWrapp // The list nesting level for entities with a certain prefix $nesting_levels = array('' => 0); - $types = search_api_field_types(); + $types = search_api_default_field_types(); $flat = array(); while ($wrappers) { foreach ($wrappers as $prefix => $wrapper) { @@ -1651,7 +1652,7 @@ function _search_api_admin_get_fields(SearchApiIndex $index, EntityMetadataWrapp $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' && $value->optionsList('view'))) { + if ($type == 'token' || ($type == 'text' && !empty($info['options list']))) { // Inner type is changed to "string". $type = 'string'; // Set the field type accordingly. @@ -1672,7 +1673,9 @@ function _search_api_admin_get_fields(SearchApiIndex $index, EntityMetadataWrapp } else { // Else, only update the nesting level. - $flat[$key]['type'] = search_api_nest_type(search_api_extract_inner_type($flat[$key]['type']), $info['type']); + $set_type = search_api_extract_inner_type(isset($flat[$key]['real_type']) ? $flat[$key]['real_type'] : $flat[$key]['type']); + $flat[$key]['type'] = $info['type']; + $flat[$key]['real_type'] = search_api_nest_type($set_type, $info['type']); } } else { @@ -1768,8 +1771,18 @@ function search_api_admin_index_fields_submit(array $form, array &$form_state) { $options = isset($index->options) ? $index->options : array(); if ($form_state['values']['op'] == t('Save changes')) { $fields = $form_state['values']['fields']; + $default_types = search_api_default_field_types(); + $custom_types = search_api_get_data_type_info(); foreach ($fields as $name => &$field) { + // Don't store the description. unset($field['description']); + // For non-default types, set type to the fallback and only real_type to + // the custom type. + $inner_type = search_api_extract_inner_type($field['type']); + if (!isset($default_types[$inner_type])) { + $field['real_type'] = $field['type']; + $field['type'] = search_api_nest_type($custom_types[$inner_type]['fallback'], $field['type']); + } } $options['fields'] = $fields; $ret = $index->update(array('options' => $options)); diff --git a/search_api.api.php b/search_api.api.php index e245a4d..f7506f3 100644 --- a/search_api.api.php +++ b/search_api.api.php @@ -129,7 +129,6 @@ function hook_search_api_item_type_info() { * @see hook_search_api_item_type_info() */ function hook_search_api_item_type_info_alter(array &$infos) { - hook_entity_info_alter(); // Adds a boolean value is_entity to all type options telling whether the item // type represents an entity type. foreach ($infos as $type => $info) { @@ -138,6 +137,55 @@ function hook_search_api_item_type_info_alter(array &$infos) { } /** + * Define new data types for indexed properties. + * + * New data types will appear as new option for the „Type“ field on indexes' + * „Fields“ tabs. Whether choosing a custom data type will have any effect + * depends on the server on which the data is indexed. + * + * @return array + * An array containing custom data type definitions, keyed by their type + * identifier and containing the following keys: + * - name: The human-readable name of the type. + * - fallback: (optional) One of the default data types (the keys from + * search_api_default_field_types()) which should be used as a fallback if + * the server doesn't support this data type. Defaults to "string". + * - conversion callback: (optional) If specified, a callback function for + * converting raw values to the given type, if possible. For the contract + * of such a callback, see example_data_type_conversion(). + * + * @see hook_search_api_data_type_info_alter() + * @see search_api_get_data_type_info() + * @see example_data_type_conversion() + */ +function hook_search_api_data_type_info() { + return array( + 'example_type' => array( + 'name' => t('Example type'), + // Could be omitted, as "string" is the default. + 'fallback' => 'string', + 'conversion callback' => 'example_data_type_conversion', + ), + ); +} + +/** + * Alter the data type info. + * + * Modules may implement this hook to alter the information that defines a data + * type, or to add/remove some entirely. All properties that are available in + * hook_search_api_data_type_info() can be altered here. + * + * @param array $infos + * The data type info array, keyed by type identifier. + * + * @see hook_search_api_data_type_info() + */ +function hook_search_api_data_type_info_alter(array &$infos) { + $infos['example_type']['name'] .= ' 2'; +} + +/** * Registers one or more callbacks that can be called at index time to add * additional data to the indexed items (e.g. comments or attachments to nodes), * alter the data in other forms or remove items from the array. @@ -453,3 +501,35 @@ function hook_default_search_api_index_alter(array &$defaults) { /** * @} End of "addtogroup hooks". */ + +/** + * Convert a raw value from an entity to a custom data type. + * + * This function will be called for fields of the specific data type to convert + * all individual values of the field to the correct format. + * + * @param $value + * The raw, single value, as extracted from an entity wrapper. + * @param $original_type + * The original Entity API type of the value. + * @param $type + * The custom data type to which the value should be converted. Can be ignored + * if the callback is only used for a single data type. + * + * @return + * The converted value, if a conversion could be executed. NULL otherwise. + * + * @see hook_search_api_data_type_info() + */ +function example_data_type_conversion($value, $original_type, $type) { + if ($type === 'example_type') { + // The example_type type apparently requires a rather complex data format. + return array( + 'value' => $value, + 'original' => $original_type, + ); + } + // Someone used this callback for another, unknown type. Return NULL. + // (Normally, you can just assume that the/a correct type is given.) + return NULL; +} diff --git a/search_api.module b/search_api.module index 0d8d407..9f72778 100644 --- a/search_api.module +++ b/search_api.module @@ -1071,8 +1071,26 @@ function search_api_current_search($search_id = NULL, SearchApiQuery $query = NU * @return array * An associative array with all recognized types as keys, mapped to their * translated display names. + * + * @see search_api_default_field_types() + * @see search_api_get_data_type_info() */ function search_api_field_types() { + $types = search_api_default_field_types(); + foreach (search_api_get_data_type_info() as $id => $type) { + $types[$id] = $type['name']; + } + return $types; +} + +/** + * Returns the default field types recognized by the Search API framework. + * + * @return array + * An associative array with the default types as keys, mapped to their + * translated display names. + */ +function search_api_default_field_types() { return array( 'text' => t('Fulltext'), 'string' => t('String'), @@ -1086,6 +1104,38 @@ function search_api_field_types() { } /** + * Returns either all custom field type definitions, or a specific one. + * + * @param $type + * If specified, the type whose definition should be returned. + * + * @return array + * If $type was not given, an array containing all custom data types, in the + * format specified by hook_search_api_data_type_info(). + * Otherwise, the definition for the given type, or NULL if it is unknown. + * + * @see hook_search_api_data_type_info() + */ +function search_api_get_data_type_info($type = NULL) { + $types = &drupal_static(__FUNCTION__); + if (!isset($types)) { + $default_types = search_api_default_field_types(); + $types = module_invoke_all('search_api_data_type_info'); + $types = $types ? $types : array(); + foreach ($types as &$type_info) { + if (!isset($type_info['fallback']) || !isset($default_types[$type_info['fallback']])) { + $type_info['fallback'] = 'string'; + } + } + drupal_alter('search_api_data_type_info', $types); + } + if (isset($type)) { + return isset($types[$type]) ? $types[$type] : NULL; + } + return $types; +} + +/** * Returns either a list of all available service infos, or a specific one. * * @see hook_search_api_service_info() @@ -1936,3 +1986,24 @@ function _search_api_wrapper_add_all_properties(EntityMetadataWrapper $wrapper, } return $property_info; } + +/** + * Helper function for converting data to a custom type. + */ +function _search_api_convert_custom_type($callback, $value, $original_type, $type, $nesting_level) { + if ($nesting_level == 0) { + return call_user_func($callback, $value, $original_type, $type); + } + if (!is_array($value)) { + return NULL; + } + --$nesting_level; + $values = array(); + foreach ($value as $v) { + $v = _search_api_convert_custom_type($callback, $v, $original_type, $type, $nesting_level); + if (isset($v) && !(is_array($v) && !$v)) { + $values[] = $v; + } + } + return $values; +}