diff --git README.txt README.txt index 9a65786..b84cb42 100644 --- README.txt +++ README.txt @@ -1,4 +1,3 @@ - Search API ---------- @@ -145,7 +144,7 @@ available for searches. Select the "Fulltext" data type for fields which you want search for keywords, and other data types when you want to use the field for filtering (e.g., as facets). The "Item language" field will always be indexed as it contains important information for processors and hooks. -You can also add fields of related entities here, via the "Add related entity" +You can also add fields of related entities here, via the "Add related fields" form at the bottom of the page. For instance, you might want to index the author's username to the indexed data of a node, and you need to add the "Body" entity to the node when you want to index the actual text it contains. @@ -280,7 +279,7 @@ Included components * Bundle filter Enables the admin to prevent entities from being indexed based on their bundle (content type for nodes, vocabulary for taxonomy terms, etc.). - * Complete entity view + * Complete entity view Adds a field containing the whole HTML content of the entity as it is viewed on the site. The view mode used can be selected. Note, however, that this might not work for entities of all types. All core @@ -301,7 +300,7 @@ Included components This processor allows you to specify how indexed fulltext content is split into seperate tokens – which characters are ignored and which treated as white-space that seperates words. - + - Additional modules * Search pages @@ -313,5 +312,5 @@ Included components For service classes supporting this feature (e.g. Solr search), this module automatically provides configurable facet blocks on pages that execute a search query. - + [1] http://drupal.org/project/views diff --git contrib/search_api_facets/search_api_facets.admin.inc contrib/search_api_facets/search_api_facets.admin.inc index 9a07eeb..0cb4fa5 100644 --- contrib/search_api_facets/search_api_facets.admin.inc +++ contrib/search_api_facets/search_api_facets.admin.inc @@ -82,12 +82,18 @@ function search_api_facets_index_select(array $form, array &$form_state, SearchA $empty_status = ' '; } $types = search_api_field_types(); + $entity_types = entity_get_info(); foreach ($index->options['fields'] as $key => $field) { if (!$field['indexed']) { continue; } - $type = search_api_extract_inner_type($field['type']); - $type = isset($types[$type]) ? $types[$type] : $type; + if (isset($field['entity_type']) && isset($entity_types[$field['entity_type']]['label'])) { + $type = $entity_types[$field['entity_type']]['label']; + } + else { + $type = search_api_extract_inner_type($field['type']); + $type = isset($types[$type]) ? $types[$type] : $type; + } if (empty($facets[$key])) { $facets[$key][] = new SearchApiFacet(array( 'index_id' => $index->machine_name, diff --git contrib/search_api_facets/search_api_facets.module contrib/search_api_facets/search_api_facets.module index 47185b8..458b2b1 100644 --- contrib/search_api_facets/search_api_facets.module +++ contrib/search_api_facets/search_api_facets.module @@ -337,6 +337,26 @@ function search_api_facets_block_view($delta = '') { } $type = $options['type']; $values = isset($options['options']) ? $options['options'] : array(); + if (!$values && isset($options['entity_type']) && !empty($results['search_api_facets'][$delta]) && entity_get_info($options['entity_type'])) { + $ids = array(); + foreach ($results['search_api_facets'][$delta] as $i => $term) { + if ($term['filter'][0] == '"') { + $ids[] = substr($term['filter'], 1, -1); + } + } + if ($ids) { + $entities = entity_load($options['entity_type'], $ids); + if ($entities) { + $values = array(); + foreach ($entities as $id => $entity) { + $label = entity_label($options['entity_type'], $entity); + if ($label) { + $values[$id] = $label; + } + } + } + } + } // Process available facet terms. $terms = empty($results['search_api_facets'][$delta]) ? array() : $results['search_api_facets'][$delta]; @@ -554,6 +574,27 @@ function search_api_facets_block_current_search_view() { $values = isset($facet->options['options']) ? $facet->options['options'] : array(); $field_name = $options['field_name'] ? $facet->name : ''; + if (!$values && isset($facet->options['entity_type'])) { + $ids = array(); + foreach ($field_filters as $i => $v) { + if ($v[0] == '"') { + $ids[] = substr($v, 1, -1); + } + } + if ($ids) { + $entities = entity_load($facet->options['entity_type'], $ids); + if ($entities) { + $values = array(); + foreach ($entities as $id => $entity) { + $label = entity_label($facet->options['entity_type'], $entity); + if ($label) { + $values[$id] = $label; + } + } + } + } + } + $theme_suffix = ''; $theme_suffix .= '__' . preg_replace('/\W+/', '_', $index->entity_type); $theme_suffix .= '__' . preg_replace('/\W+/', '_', $field); @@ -866,11 +907,13 @@ function search_api_facets_get_filters(SearchApiQueryInterface $query) { * @param stdClass $facet * The facet whose type information should be refreshed. * @param array $options - * An array for which 'type' and, if applicable, 'options' will be set. + * An array for which 'type' and, if applicable, 'options' and 'entity_type' + * will be set. */ function _search_api_facets_refresh_type(SearchApiFacet $facet, array &$options) { $index = search_api_index_load($facet->index_id); - $type = $index->options['fields'][$facet->field]['type']; + $field_info = $index->options['fields'][$facet->field]; + $type = $field_info['type']; $options['type'] = search_api_extract_inner_type($type); $wrapper = $index->entityWrapper(); foreach (explode(':', $facet->field) as $part) { @@ -889,6 +932,9 @@ function _search_api_facets_refresh_type(SearchApiFacet $facet, array &$options) $options['type'] = 'options'; $options['options'] = $wrapper->optionsList('view'); } + if (isset($field_info['entity_type'])) { + $options['entity_type'] = $field_info['entity_type']; + } } /** @@ -901,7 +947,7 @@ function _search_api_create_filter_name($filter, $type, array $values = array()) } if ($filter[0] == '"') { $filter = substr($filter, 1, -1); - if ($filter == '' || $filter == '*') { + if ($filter == '') { return t('any'); } return _search_api_create_value_name($filter, $type, $values); @@ -923,10 +969,12 @@ function _search_api_create_filter_name($filter, $type, array $values = array()) } /** - * Creates a human-readable name for a single filter value. $values is only - * needed if $type is "options". + * Creates a human-readable name for a single filter value. */ function _search_api_create_value_name($value, $type, array $values = array()) { + if (isset($values[$value])) { + return $values[$value]; + } switch ($type) { case 'boolean': return $value ? t('true') : t('false'); @@ -935,8 +983,6 @@ function _search_api_create_value_name($value, $type, array $values = array()) { return format_date($value, 'short'); case 'duration': return format_interval($value); - case 'options': - return empty($values[$value]) ? $value : $values[$value]; default: // Nothing we can do about it. diff --git contrib/search_api_views/includes/handler_field_entity.inc contrib/search_api_views/includes/handler_field_entity.inc new file mode 100644 index 0000000..ffe78b3 --- /dev/null +++ contrib/search_api_views/includes/handler_field_entity.inc @@ -0,0 +1,79 @@ +entity_type = search_api_extract_inner_type($this->definition['type']); + } + + /** + * Specifies the options this handler uses. + */ + public function option_definition() { + $options = parent::option_definition(); + $options['format_name'] = array('default' => TRUE); + return $options; + } + + /** + * Returns an option form for setting this handler's options. + */ + public function options_form(array &$form, array &$form_state) { + parent::options_form($form, $form_state); + + $form['format_name'] = array( + '#title' => t('Display label instead of ID'), + '#type' => 'checkbox', + '#description' => t("If this is checked, the entities' labels will be displayed instead of their numeric identifiers."), + '#default_value' => $this->options['format_name'], + '#weight' => -5, + ); + } + + /** + * Render a value as a link to the entity, if applicable. + */ + protected function renderLink($value, array $values) { + if (!$this->options['link_to_entity']) { + return $value; + } + $entity = entity_load($this->entity_type, array($values[$this->real_field])); + if (!$entity) { + return $value; + } + $entity = $entity[$values[$this->real_field]]; + $url = entity_uri($this->entity_type, $entity); + return l($value, $url['path'], array('html' => TRUE) + $url['options']); + } + + /** + * Helper function for rendering a single value. + */ + protected function renderValue($value) { + if ($this->options['format_name']) { + $entity = entity_load($this->entity_type, array($value)); + if ($entity) { + $entity = $entity[$value]; + $label = entity_label($this->entity_type, $entity); + if ($label) { + return check_plain($label); + } + } + } + return check_plain($value); + } + +} diff --git contrib/search_api_views/search_api_views.info contrib/search_api_views/search_api_views.info index c0d6295..c77da3b 100644 --- contrib/search_api_views/search_api_views.info +++ contrib/search_api_views/search_api_views.info @@ -15,6 +15,7 @@ files[] = includes/handler_field.inc files[] = includes/handler_field_boolean.inc files[] = includes/handler_field_date.inc files[] = includes/handler_field_duration.inc +files[] = includes/handler_field_entity.inc files[] = includes/handler_field_options.inc files[] = includes/handler_filter.inc files[] = includes/handler_filter_boolean.inc diff --git contrib/search_api_views/search_api_views.views.inc contrib/search_api_views/search_api_views.views.inc index 2761a70..1a1140b 100644 --- contrib/search_api_views/search_api_views.views.inc +++ contrib/search_api_views/search_api_views.views.inc @@ -5,6 +5,7 @@ */ function search_api_views_views_data() { $data = array(); + $entity_types = entity_get_info(); foreach (search_api_index_load_multiple(FALSE) as $index) { // Base data $key = 'search_api_index_' . $index->machine_name; @@ -69,7 +70,8 @@ function search_api_views_views_data() { // information. For the moment, we just don't add such a field. continue; } - if (isset($types[$inner_type])) { + + if (isset($types[$inner_type]) || isset($entity_types[$inner_type])) { if ($value->optionsList('view')) { $inner_type = 'options'; $type = search_api_nest_type('options', $type); @@ -98,7 +100,7 @@ function search_api_views_views_data() { } unset($fields[$key]); } - elseif ($depth < $max_depth) { + if (!isset($types[$inner_type]) && $depth < $max_depth) { // Visit this entity/struct in a later iteration. $key .= ':'; $wrappers[$key] = $value; @@ -183,6 +185,9 @@ function _search_api_views_field_handler($type, $inner_type) { return 'SearchApiViewsHandlerFieldOptions'; default: + if (entity_get_info($inner_type)) { + return 'SearchApiViewsHandlerFieldEntity'; + } return 'SearchApiViewsHandlerField'; } } diff --git includes/index_entity.inc includes/index_entity.inc index 94710bf..7042b82 100644 --- includes/index_entity.inc +++ includes/index_entity.inc @@ -65,6 +65,37 @@ class SearchApiIndex extends Entity { public $entity_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 run. + * - fields: 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. + * - 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(). + * - 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. + * - 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; diff --git search_api.admin.inc search_api.admin.inc index ed0dae5..d4830ce 100644 --- search_api.admin.inc +++ search_api.admin.inc @@ -1440,44 +1440,68 @@ function search_api_admin_index_fields(array $form, array &$form_state, SearchAp '#value' => $info['description'], ); } - // Determine the correct type options (i.e., with the correct nesting level). - $level = search_api_list_nesting_level($info['type']); - if (empty($types[$level])) { - $type_prefix = str_repeat('list<', $level); - $type_suffix = str_repeat('>', $level); - $types[$level] = array(); - foreach ($types[0] as $type => $name) { - // We use the singular name for list types, since the user usually doesn't care about the nesting level. - $types[$level][$type_prefix . $type . $type_suffix] = $name; - } - $fulltext_type[$level] = $type_prefix . 'text' . $type_suffix; - } $form['fields'][$key]['indexed'] = array( '#type' => 'checkbox', '#default_value' => $info['indexed'], ); - $css_key = '#edit-fields-' . drupal_clean_css_identifier($key); - $form['fields'][$key]['type'] = array( - '#type' => 'select', - '#options' => $types[$level], - '#default_value' => $info['type'], - '#states' => array( - 'visible' => array( - $css_key . '-indexed' => array('checked' => TRUE), + $inner_type = search_api_extract_inner_type($info['type']); + if (isset($types[0][$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])) { + $type_prefix = str_repeat('list<', $level); + $type_suffix = str_repeat('>', $level); + $types[$level] = array(); + foreach ($types[0] as $type => $name) { + // We use the singular name for list types, since the user usually doesn't care about the nesting level. + $types[$level][$type_prefix . $type . $type_suffix] = $name; + } + $fulltext_type[$level] = $type_prefix . 'text' . $type_suffix; + } + $css_key = '#edit-fields-' . drupal_clean_css_identifier($key); + $form['fields'][$key]['type'] = array( + '#type' => 'select', + '#options' => $types[$level], + '#default_value' => $info['type'], + '#states' => array( + 'visible' => array( + $css_key . '-indexed' => array('checked' => TRUE), + ), ), - ), - ); - $form['fields'][$key]['boost'] = array( - '#type' => 'select', - '#options' => $boosts, - '#default_value' => $info['boost'], - '#states' => array( - 'visible' => array( - $css_key . '-indexed' => array('checked' => TRUE), - $css_key . '-type' => array('value' => $fulltext_type[$level]), + ); + $form['fields'][$key]['boost'] = array( + '#type' => 'select', + '#options' => $boosts, + '#default_value' => $info['boost'], + '#states' => array( + 'visible' => array( + $css_key . '-indexed' => array('checked' => TRUE), + $css_key . '-type' => array('value' => $fulltext_type[$level]), + ), ), - ), - ); + ); + } + else { + // This is an entity. + $form['fields'][$key]['type'] = array( + '#type' => 'value', + '#value' => search_api_nest_type('integer', $info['type']), + ); + $form['fields'][$key]['entity_type'] = array( + '#type' => 'value', + '#value' => $info['type'], + ); + $form['fields'][$key]['type_name'] = array( + '#markup' => $entity_types[$inner_type]['label'], + ); + $form['fields'][$key]['boost'] = array( + '#type' => 'value', + '#value' => $info['boost'], + ); + $form['fields'][$key]['boost_text'] = array( + '#markup' => ' ', + ); + } if ($key == 'search_api_language') { // Is treated specially to always index the language. $form['fields'][$key]['type']['#default_value'] = 'string'; @@ -1498,7 +1522,7 @@ function search_api_admin_index_fields(array $form, array &$form_state, SearchAp reset($additional); $form['additional'] = array( '#type' => 'fieldset', - '#title' => t('Add related entity'), + '#title' => t('Add related fields'), '#description' => t('There are entities related to entities of this type. ' . 'You can add their fields to the list above so they can be indexed too.') . '
', '#collapsible' => TRUE, @@ -1527,6 +1551,7 @@ function search_api_admin_index_fields(array $form, array &$form_state, SearchAp function _search_api_admin_get_fields(SearchApiIndex $index, EntityMetadataWrapper $wrapper) { $fields = empty($index->options['fields']) ? array() : $index->options['fields']; $additional = array(); + $entity_types = entity_get_info(); // First we need all already added prefixes. $added = array(); @@ -1577,11 +1602,14 @@ function _search_api_admin_get_fields(SearchApiIndex $index, EntityMetadataWrapp } $info['type'] = $type_prefix . $info['type'] . $type_suffix; $key = $prefix . $property; - if (isset($types[$type])) { + if (isset($types[$type]) || isset($entity_types[$type])) { if (isset($fields[$key])) { $fields[$key]['name'] = $prefix_name . $info['label']; $fields[$key]['description'] = empty($info['description']) ? NULL : $info['description']; $flat[$key] = $fields[$key]; + if (isset($entity_types[$type])) { + $flat[$key]['type'] = $info['type']; + } } else { $flat[$key] = array( @@ -1593,7 +1621,7 @@ function _search_api_admin_get_fields(SearchApiIndex $index, EntityMetadataWrapp ); } } - else { + if (empty($types[$type])) { if (isset($added[$key])) { // Visit this entity/struct in a later iteration. $wrappers[$key . ':'] = $value; @@ -1650,7 +1678,7 @@ function theme_search_api_admin_fields_table($variables) { else { $rows[] = array( 'data' => $row, - 'title' => $form['fields'][$name]['description']['#value'], + 'title' => strip_tags($form['fields'][$name]['description']['#value']), ); } } diff --git search_api.module search_api.module index 32a1528..9303348 100644 --- search_api.module +++ search_api.module @@ -1084,6 +1084,7 @@ function search_api_extract_fields(EntityMetadataWrapper $wrapper, array $fields } $nested = array(); + $entity_infos = entity_get_info(); foreach ($fields as $field => &$info) { $pos = strpos($field, ':'); if ($pos === FALSE) { @@ -1092,11 +1093,25 @@ function search_api_extract_fields(EntityMetadataWrapper $wrapper, array $fields $info['original_type'] = $info['type']; try { $info['value'] = $wrapper->$field->value($value_options); + // For fulltext fields with options, also include the option labels. if (search_api_is_text_type($info['type']) && $wrapper->$field->optionsList('view')) { _search_api_add_option_values($info['value'], $wrapper->$field->optionsList('view')); } $property_info = $wrapper->$field->info(); $info['original_type'] = $property_info['type']; + // For entities, we extract the entity ID instead of the whole object. + $t = search_api_extract_inner_type($property_info['type']); + if (isset($entity_infos[$t])) { + if (!search_api_is_list_type($property_info['type'])) { + $info['value'] = $wrapper->$field->getIdentifier(); + } + else { + $id_key = $entity_infos[$t]['entity keys']['id']; + $tmp_fields["$field:$id_key"]['type'] = 'integer'; + $tmp_fields = search_api_extract_fields($wrapper, $tmp_fields, $value_options); + $info['value'] = $tmp_fields["$field:$id_key"]['value']; + } + } } catch (EntityMetadataWrapperException $e) { // This might happen for entity-typed properties that are NULL, e.g.,