diff --git a/README.txt b/README.txt index 3b6ed02..63fe32b 100644 --- a/README.txt +++ b/README.txt @@ -35,11 +35,15 @@ Terms as used in this module. e.g. be some tables in a database, a connection to a Solr server or other external services, etc. - Index: - One set of data for searching a specific entity. What and how data is - indexed is determined by its settings. Also keeps track of which items still - need to be indexed (or re-indexed, if they were updated). Needs to lie on a - server in order to be really used (although configuration is independent of a - server). + A configuration object for indexing data of a specific type. What and how data + is indexed is determined by its settings. Also keeps track of which items + still need to be indexed (or re-indexed, if they were updated). Needs to lie + on a server in order to be really used (although configuration is independent + of a server). +- Item type: + A type of data which can be indexed (i.e., for which indexes can be created). + Most entity types (like Content, User, Taxonomy term, etc.) are available, but + possibly also other types provided by contrib modules. - Entity: One object of data, usually stored in the database. Might for example be a node, a user or a file. @@ -178,6 +182,7 @@ Information for developers | searchable with the Search API, your module will need to implement | hook_entity_property_info() in addition to the normal hook_entity_info(). | hook_entity_property_info() is documented in the entity module. + | For making certain non-entities searchable, see "Item type" below. | For custom field types to be available for indexing, provide a | "property_type" key in hook_field_info(), and optionally a callback at the | "property_callbacks" key. @@ -229,6 +234,25 @@ For the query class to become available (other than through manual creation), you need a custom service class where you override the query() method to return an instance of your query class. +- Item type + Interface: SearchApiDataSourceControllerInterface + Base class: SearchApiAbstractDataSourceController + Hook: hook_search_api_item_type_info() + +If you want to index some data which is not defined as an entity, you can +specify it as a new item type here. For defining a new item type, you have to +create a data source controller for the type and track new, changed and deleted +items of the type by calling the search_api_track_item_*() functions. +An instance of the data source controller class will then be used by indexes +when handling items of your newly-defined type. + +If you want to make external data that is indexed on some search server +available to the Search API, there is a handy base class for your data source +controller (SearchApiExternalDataSourceController in +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-alter callbacks Interface: SearchApiAlterCallbackInterface Base class: SearchApiAbstractAlterCallback diff --git a/contrib/search_api_db/search_api_db.test b/contrib/search_api_db/search_api_db.test index 1ffbc4d..b8dc0f4 100644 --- a/contrib/search_api_db/search_api_db.test +++ b/contrib/search_api_db/search_api_db.test @@ -118,11 +118,11 @@ class SearchApiDbTest extends DrupalWebTestCase { $values = array( 'name' => 'Test index', 'machine_name' => 'test_index', - 'entity_type' => 'search_api_test', + 'item_type' => 'search_api_test', 'enabled' => 1, 'description' => 'An index used for testing.', 'server' => $this->server_id, - 'cron_limit' => 5, + 'options[cron_limit]' => 5, ); $this->drupalPost('admin/config/search/search_api/add_index', $values, t('Create index')); diff --git a/contrib/search_api_db/service.inc b/contrib/search_api_db/service.inc index 039eb13..a09946a 100644 --- a/contrib/search_api_db/service.inc +++ b/contrib/search_api_db/service.inc @@ -108,7 +108,7 @@ class SearchApiDbService extends SearchApiAbstractService { continue; } $table = $this->findFreeTable($prefix, $name); - $this->createFieldTable($field, $table); + $this->createFieldTable($index, $field, $table); $indexes[$index->machine_name][$name]['table'] = $table; $indexes[$index->machine_name][$name]['type'] = $field['type']; $indexes[$index->machine_name][$name]['boost'] = $field['boost']; @@ -137,19 +137,25 @@ class SearchApiDbService extends SearchApiAbstractService { /** * Helper method for creating the table for a field. */ - protected function createFieldTable($field, $name) { + protected function createFieldTable(SearchApiIndex $index, $field, $name) { $table = array( 'name' => $name, 'module' => 'search_api_db', 'fields' => array( 'item_id' => array( - 'description' => 'The primary identifier of the entity.', - 'type' => 'int', - 'unsigned' => TRUE, + 'description' => 'The primary identifier of the item.', 'not null' => TRUE, ), ), ); + // The type of the item_id field depends on the ID field's type. + $id_field = $index->datasource()->getIdFieldInfo(); + $table['fields']['item_id'] += $this->sqlType($id_field['type'] == 'text' ? 'string' : $id_field['type']); + if (isset($table['fields']['item_id']['length'])) { + // A length of 255 is overkill for IDs. 50 should be more than enough. + $table['fields']['item_id']['length'] = 50; + } + $type = search_api_extract_inner_type($field['type']); if ($type == 'text') { $table['fields']['word'] = array( @@ -238,7 +244,7 @@ class SearchApiDbService extends SearchApiAbstractService { $this->deleteItems('all', $index); } db_drop_table($field['table']); - $this->createFieldTable($new_fields[$name], $field['table']); + $this->createFieldTable($index, $new_fields[$name], $field['table']); } elseif ($this->sqlType($old_type) != $this->sqlType($new_type)) { // There is a change in SQL type. We don't have to clear the index, since types can be converted. @@ -263,7 +269,7 @@ class SearchApiDbService extends SearchApiAbstractService { foreach ($new_fields as $name => $field) { $reindex = TRUE; $table = $this->findFreeTable($prefix, $name); - $this->createFieldTable($field, $table); + $this->createFieldTable($index, $field, $table); $fields[$name]['table'] = $table; $fields[$name]['type'] = $field['type']; $fields[$name]['boost'] = $field['boost']; @@ -331,9 +337,11 @@ class SearchApiDbService extends SearchApiAbstractService { if (search_api_is_text_type($type, array('text', 'tokens'))) { $words = array(); foreach ($value as $token) { - // Taken from core search to reflect less importance of words later in the text. - // Focus is a decaying value in terms of the amount of unique words up to this point. - // From 100 words and more, it decays, to e.g. 0.5 at 500 words and 0.3 at 1000 words. + // Taken from core search to reflect less importance of words later + // in the text. + // Focus is a decaying value in terms of the amount of unique words + // up to this point. From 100 words and more, it decays, to e.g. 0.5 + // at 500 words and 0.3 at 1000 words. $focus = min(1, .01 + 3.5 / (2 + count($words) * .015)); $value = &$token['value']; @@ -807,7 +815,7 @@ class SearchApiDbService extends SearchApiAbstractService { $negated = array(); $db_query = NULL; $mul_words = FALSE; - $not_nested = FALSE; // If the query will nest UNIONed subqueries or just leave them that way. + $not_nested = FALSE; foreach ($keys as $i => $key) { if (!element_child($i)) { @@ -818,7 +826,9 @@ class SearchApiDbService extends SearchApiAbstractService { } elseif (empty($key['#negation'])) { if ($neg) { - $key['#negation'] = TRUE; // If this query is negated, we also only need item_ids from subqueries. + // If this query is negated, we also only need item_ids from + // subqueries. + $key['#negation'] = TRUE; } $nested[] = $key; } diff --git a/contrib/search_api_facets/search_api_facets.module b/contrib/search_api_facets/search_api_facets.module index 5b827aa..733dcf6 100644 --- a/contrib/search_api_facets/search_api_facets.module +++ b/contrib/search_api_facets/search_api_facets.module @@ -399,7 +399,7 @@ function search_api_facets_block_view($delta = '') { } $theme_suffix = ''; - $theme_suffix .= '__' . preg_replace('/\W+/', '_', $query->getIndex()->entity_type); + $theme_suffix .= '__' . preg_replace('/\W+/', '_', $query->getIndex()->item_type); $theme_suffix .= '__' . preg_replace('/\W+/', '_', $facet->field); $theme_suffix .= '__' . preg_replace('/\W+/', '_', $facet->delta); $theme = array( @@ -596,7 +596,7 @@ function search_api_facets_block_current_search_view() { } $theme_suffix = ''; - $theme_suffix .= '__' . preg_replace('/\W+/', '_', $index->entity_type); + $theme_suffix .= '__' . preg_replace('/\W+/', '_', $index->item_type); $theme_suffix .= '__' . preg_replace('/\W+/', '_', $field); $theme_suffix .= '__' . preg_replace('/\W+/', '_', $facet->delta); foreach ($field_filters as $i => $v) { diff --git a/contrib/search_api_page/search_api_page.admin.inc b/contrib/search_api_page/search_api_page.admin.inc index a76f782..99dc25b 100644 --- a/contrib/search_api_page/search_api_page.admin.inc +++ b/contrib/search_api_page/search_api_page.admin.inc @@ -170,12 +170,14 @@ function search_api_page_admin_add(array $form, array &$form_state) { '#default_value' => 10, ); - $entity_info = entity_get_info($index->entity_type); $view_modes = array( 'search_api_page_result' => t('Themed as search results'), ); - foreach ($entity_info['view modes'] as $mode => $mode_info) { - $view_modes[$mode] = $mode_info['label']; + // For entities, we also add all entity view modes. + if ($entity_info = entity_get_info($index->item_type)) { + foreach ($entity_info['view modes'] as $mode => $mode_info) { + $view_modes[$mode] = $mode_info['label']; + } } if (count($view_modes) > 1) { $form['view_mode'] = array( @@ -343,12 +345,14 @@ function search_api_page_admin_edit(array $form, array &$form_state, stdClass $p '#default_value' => $page->options['per_page'], ); - $entity_info = entity_get_info($index->entity_type); $view_modes = array( 'search_api_page_result' => t('Themed as search results'), ); - foreach ($entity_info['view modes'] as $mode => $mode_info) { - $view_modes[$mode] = $mode_info['label']; + // For entities, we also add all entity view modes. + if ($entity_info = entity_get_info($index->item_type)) { + foreach ($entity_info['view modes'] as $mode => $mode_info) { + $view_modes[$mode] = $mode_info['label']; + } } if (count($view_modes) > 1) { $form['options']['view_mode'] = array( diff --git a/contrib/search_api_page/search_api_page.module b/contrib/search_api_page/search_api_page.module index 3ed6d05..acdf8ce 100644 --- a/contrib/search_api_page/search_api_page.module +++ b/contrib/search_api_page/search_api_page.module @@ -57,7 +57,7 @@ function search_api_page_theme() { 'variables' => array( 'index' => NULL, 'results' => array('result count' => 0), - 'entities' => array(), + 'items' => array(), 'view_mode' => 'search_api_page_result', 'keys' => '', ), @@ -67,7 +67,7 @@ function search_api_page_theme() { 'variables' => array( 'index' => NULL, 'result' => NULL, - 'entity' => NULL, + 'item' => NULL, 'keys' => '', ), 'file' => 'search_api_page.pages.inc', @@ -153,11 +153,14 @@ function search_api_page_entity_property_info() { 'label' => t('ID'), 'type' => 'integer', 'description' => t('The primary identifier for a search page.'), + 'schema field' => 'id', + 'validation callback' => 'entity_metadata_validate_integer_positive', ), 'index_id' => array( 'label' => t('Index ID'), 'type' => 'token', - 'description' => t('The ID of the index this search page uses.'), + 'description' => t('The machine name of the index this search page uses.'), + 'schema field' => 'index_id', ), 'index' => array( 'label' => t('Index'), @@ -169,18 +172,21 @@ function search_api_page_entity_property_info() { 'label' => t('Name'), 'type' => 'text', 'description' => t('The displayed name for a search page.'), + 'schema field' => 'name', 'required' => TRUE, ), 'description' => array( 'label' => t('Description'), 'type' => 'text', 'description' => t('The displayed description for a search page.'), + 'schema field' => 'description', 'sanitize' => 'filter_xss', ), 'enabled' => array( 'label' => t('Enabled'), 'type' => 'boolean', 'description' => t('A flag indicating whether the search page is enabled.'), + 'schema field' => 'enabled', ), ); diff --git a/contrib/search_api_page/search_api_page.pages.inc b/contrib/search_api_page/search_api_page.pages.inc index b040439..ee24f36 100644 --- a/contrib/search_api_page/search_api_page.pages.inc +++ b/contrib/search_api_page/search_api_page.pages.inc @@ -100,7 +100,7 @@ function search_api_page_search_execute(stdClass $page, $keys) { */ function template_preprocess_search_api_page_results(array &$variables) { if (!empty($variables['results']['results'])) { - $variables['entities'] = entity_load($variables['index']->entity_type, array_keys($variables['results']['results'])); + $variables['items'] = $variables['index']->loadItems(array_keys($variables['results']['results'])); } } @@ -112,8 +112,8 @@ function template_preprocess_search_api_page_results(array &$variables) { * - index: The index this search was executed on. * - results: An array of search results, as returned by * SearchApiQueryInterface::execute(). - * - entities: The loaded entities for all results, in an array keyed by ID. - * - "view_mode": The view mode to use for displaying the individual results, + * - items: The loaded items for all results, in an array keyed by ID. + * - view_mode: The view mode to use for displaying the individual results, * or the special mode "search_api_page_result" to use the theme function * of the same name. * - keys: The keywords of the executed search. @@ -123,7 +123,7 @@ function theme_search_api_page_results(array $variables) { $index = $variables['index']; $results = $variables['results']; - $entities = $variables['entities']; + $items = $variables['items']; $keys = $variables['keys']; $output = '

' . format_plural($results['result count'], @@ -139,16 +139,16 @@ function theme_search_api_page_results(array $variables) { $output .= "\n

" . t('Search results') . "

\n"; if ($variables['view_mode'] == 'search_api_page_result') { - entity_prepare_view($index->entity_type, $entities); $output .= '
    '; foreach ($results['results'] as $item) { - $output .= '
  1. ' . theme('search_api_page_result', array('index' => $index, 'result' => $item, 'entity' => isset($entities[$item['id']]) ? $entities[$item['id']] : NULL, 'keys' => $keys)) . '
  2. '; + $output .= '
  3. ' . theme('search_api_page_result', array('index' => $index, 'result' => $item, 'item' => isset($items[$item['id']]) ? $items[$item['id']] : NULL, 'keys' => $keys)) . '
  4. '; } $output .= '
'; } else { + // This option can only be set when the items are entities. $output .= '
'; - $render = entity_view($index->entity_type, $entities, $variables['view_mode']); + $render = entity_view($index->item_type, $items, $variables['view_mode']); $output .= render($render); $output .= '
'; } @@ -164,25 +164,19 @@ function theme_search_api_page_results(array $variables) { * - index: The index this search was executed on. * - result: One item of the search results, an array containing the keys * 'id' and 'score'. - * - entity: The loaded entity corresponding to the result. + * - item: The loaded item corresponding to the result. * - keys: The keywords of the executed search. */ function theme_search_api_page_result(array $variables) { $index = $variables['index']; $id = $variables['result']['id']; - $entity = $variables['entity']; + $item = $variables['item']; - $wrapper = entity_metadata_wrapper($index->entity_type, $entity); + $wrapper = $index->entityWrapper($item, FALSE); + $index = new SearchApiIndex(); - $url = entity_uri($index->entity_type, $entity); - $name = entity_label($index->entity_type, $entity); - - if ($index->entity_type == 'file') { - $url = array( - 'path' => file_create_url($url), - 'options' => array(), - ); - } + $url = $index->datasource()->getItemUrl($item); + $name = $index->datasource()->getItemLabel($item); if (!empty($variables['result']['excerpt'])) { $text = $variables['result']['excerpt']; diff --git a/contrib/search_api_views/includes/display_facet_block.inc b/contrib/search_api_views/includes/display_facet_block.inc index 1dc74b4..cecd95f 100644 --- a/contrib/search_api_views/includes/display_facet_block.inc +++ b/contrib/search_api_views/includes/display_facet_block.inc @@ -200,7 +200,7 @@ class SearchApiViewsFacetsBlockDisplay extends views_plugin_display_block { } $theme_suffix = ''; - $theme_suffix .= '__' . preg_replace('/\W+/', '_', $this->view->query->getIndex()->entity_type); + $theme_suffix .= '__' . preg_replace('/\W+/', '_', $this->view->query->getIndex()->item_type); $theme_suffix .= '__' . preg_replace('/\W+/', '_', $facet_field); $theme_suffix .= '__search_api_views_facets_block'; $info['content']['facets'] = array( diff --git a/contrib/search_api_views/includes/handler_argument_more_like_this.inc b/contrib/search_api_views/includes/handler_argument_more_like_this.inc index 9a8eeb6..be73ab0 100644 --- a/contrib/search_api_views/includes/handler_argument_more_like_this.inc +++ b/contrib/search_api_views/includes/handler_argument_more_like_this.inc @@ -56,7 +56,7 @@ class SearchApiViewsHandlerArgumentMoreLikeThis extends SearchApiViewsHandlerArg */ public function query($group_by = FALSE) { $server = $this->query->getIndex()->server(); - if (!$server->supportsFeature("search_api_mlt")) { + if (!$server->supportsFeature('search_api_mlt')) { $class = search_api_get_service_info($server->class); throw new SearchApiException(t('The search service "!class" does not offer "More like this" functionality.', array('!class' => $class['name']))); @@ -74,6 +74,6 @@ class SearchApiViewsHandlerArgumentMoreLikeThis extends SearchApiViewsHandlerArg 'id' => $this->argument, 'fields' => $fields, ); - $this->query->getSearchApiQuery()->setOption("search_api_mlt", $mlt); + $this->query->getSearchApiQuery()->setOption('search_api_mlt', $mlt); } } diff --git a/contrib/search_api_views/includes/handler_field.inc b/contrib/search_api_views/includes/handler_field.inc index 9627ed2..d3c0771 100644 --- a/contrib/search_api_views/includes/handler_field.inc +++ b/contrib/search_api_views/includes/handler_field.inc @@ -73,7 +73,7 @@ class SearchApiViewsHandlerField extends views_handler_field { } $form['link_to_entity'] = array( '#type' => 'checkbox', - '#title' => t('Link this field to its entity'), + '#title' => t('Link this field to the result item'), '#description' => t('This will override any other link you have set.'), '#default_value' => $this->options['link_to_entity'], ); @@ -169,16 +169,16 @@ class SearchApiViewsHandlerField extends views_handler_field { if (!$this->options['link_to_entity']) { return $render; } - $type = isset($values->search_api_views_entity_type) ? $values->search_api_views_entity_type : $this->query->getIndex()->entity_type; - $entity = $values->entity; + $index = $this->query->getIndex(); + $item = $values->entity; // $values->entity can either be the fully loaded entity, or just its ID. - if (!is_object($entity)) { - $entity = entity_load($type, array($entity)); - if ($entity) { - $entity = reset($entity); + if (!is_object($item)) { + $items = $index->loadItems(array($item)); + if ($items) { + $item = reset($items); } } - if (is_object($entity) && ($url = entity_uri($type, $entity))) { + if (is_object($item) && ($url = $index->datasource()->getItemUrl($item))) { return l($render, $url['path'], array('html' => TRUE) + $url['options']); } return $render; diff --git a/contrib/search_api_views/includes/handler_field_entity.inc b/contrib/search_api_views/includes/handler_field_entity.inc index 6cc11f4..a600dad 100644 --- a/contrib/search_api_views/includes/handler_field_entity.inc +++ b/contrib/search_api_views/includes/handler_field_entity.inc @@ -33,6 +33,7 @@ class SearchApiViewsHandlerFieldEntity extends SearchApiViewsHandlerField { */ public function options_form(&$form, &$form_state) { parent::options_form($form, $form_state); + $form['link_to_entity']['#description'] = t('Link this field to its entity'); $form['format_name'] = array( '#title' => t('Display label instead of ID'), diff --git a/contrib/search_api_views/includes/query.inc b/contrib/search_api_views/includes/query.inc index 4ec65d2..46a6e31 100644 --- a/contrib/search_api_views/includes/query.inc +++ b/contrib/search_api_views/includes/query.inc @@ -157,16 +157,15 @@ class SearchApiViewsQuery extends views_plugin_query { protected function addResults(array $results, $view) { $rows = array(); $missing = array(); - $entities = array(); - $entity_type = $this->index->entity_type; + $items = array(); // First off, we try to gather as much field values as possible without - // loading any entities. + // loading any items. foreach ($results as $id => $result) { - $row = array('search_api_views_entity_type' => $entity_type); + $row = array(); - // Include the loaded entity for this result row, if present, or the - // entity ID. + // Include the loaded item for this result row, if present, or the item + // ID. if (!empty($result['entity'])) { $row['entity'] = $result['entity']; } @@ -184,12 +183,12 @@ class SearchApiViewsQuery extends views_plugin_query { $row += $result['fields']; } - // Check whether we need to extract any properties from the Drupal entity. + // Check whether we need to extract any properties from the result item. $missing_fields = array_diff_key($this->fields, $row); - if (!empty($missing_fields)) { + if ($missing_fields) { $missing[$id] = $missing_fields; if (is_object($row['entity'])) { - $entities[$id] = $row['entity']; + $items[$id] = $row['entity']; } else { $ids[] = $id; @@ -200,16 +199,16 @@ class SearchApiViewsQuery extends views_plugin_query { $rows[$id] = $row; } - // Load entities of those rows which haven't got all field values, yet. + // Load items of those rows which haven't got all field values, yet. if (!empty($ids)) { - $entities += entity_load($entity_type, $ids); - // $entities now includes loaded entities, and those already passed in the + $items += $this->index->loadItems($ids); + // $items now includes loaded items, and those already passed in the // search results. - foreach ($entities as $id => $entity) { - // Extract entity properties. - $wrapper = entity_metadata_wrapper($entity_type, $entity); + foreach ($items as $id => $item) { + // Extract item properties. + $wrapper = $this->index->entityWrapper($item, FALSE); $rows[$id] += $this->extractFields($wrapper, $missing[$id]); - $rows[$id]['entity'] = $entity; + $rows[$id]['entity'] = $item; } } @@ -220,7 +219,7 @@ class SearchApiViewsQuery extends views_plugin_query { } /** - * Helper function for extracting all necessary fields from an entity. + * Helper function for extracting all necessary fields from a result item. */ // @todo Optimize protected function extractFields(EntityMetadataWrapper $wrapper, array $all_fields) { diff --git a/contrib/search_api_views/search_api_views.views.inc b/contrib/search_api_views/search_api_views.views.inc index a591c34..7c6799d 100644 --- a/contrib/search_api_views/search_api_views.views.inc +++ b/contrib/search_api_views/search_api_views.views.inc @@ -10,29 +10,34 @@ function search_api_views_views_data() { // Base data $key = 'search_api_index_' . $index->machine_name; $table = &$data[$key]; - $entity_info = entity_get_info($index->entity_type); - $table['table']['group'] = $entity_info['label']; + $type_info = search_api_get_item_type_info($index->item_type); + $table['table']['group'] = $type_info['name']; $table['table']['base'] = array( 'field' => 'search_api_id', 'index' => $index->machine_name, 'title' => $index->name, 'help' => t('Use the %name search index for filtering and retrieving data.', array('%name' => $index->name)), 'query class' => 'search_api_views_query', - 'entity type' => $index->entity_type, - 'skip entity load' => TRUE, ); + if (isset($entity_types[$index->item_type])) { + $table['table']['base'] += array( + 'entity type' => $index->item_type, + 'skip entity load' => TRUE, + ); + } // Add all available fields // This is largely copied from _search_api_admin_get_fields(). $max_depth = variable_get('search_api_views_max_fields_depth', 2); - $orig_wrapper = $index->entityWrapper(); + $orig_wrapper = $index->entityWrapper(NULL, FALSE); $fields = empty($index->options['fields']) ? array() : $index->options['fields']; - // A wrapper for a specific field name prefix, e.g. 'user:' mapped to the user wrapper + // A wrapper for a specific field name prefix, e.g. 'user:' mapped to the + // user wrapper. $wrappers = array('' => $orig_wrapper); - // Display names for the prefixes + // Display names for the prefixes. $prefix_names = array('' => ''); - // The list nesting level for entities with a certain prefix + // The list nesting level for structures with a certain prefix. $nesting_levels = array('' => 0); $types = search_api_field_types(); @@ -41,7 +46,7 @@ function search_api_views_views_data() { foreach ($wrappers as $prefix => $wrapper) { $prefix_name = $prefix_names[$prefix]; $depth = substr_count($prefix, ':'); - // Deal with lists of entities. + // Deal with lists of items. $nesting_level = $nesting_levels[$prefix]; $type_prefix = str_repeat('list<', $nesting_level); $type_suffix = str_repeat('>', $nesting_level); @@ -240,7 +245,7 @@ function _search_api_views_add_handlers($field, $wrapper) { $ret = array(); - if (empty($field['entity_type']) && $options = $wrapper->optionsList('view')) { + if ($options = $wrapper->optionsList('view')) { $ret['filter']['handler'] = 'SearchApiViewsHandlerFilterOptions'; $ret['filter']['options'] = $options; } diff --git a/includes/callback_add_url.inc b/includes/callback_add_url.inc index 281858a..097fd41 100644 --- a/includes/callback_add_url.inc +++ b/includes/callback_add_url.inc @@ -6,17 +6,8 @@ class SearchApiAlterAddUrl extends SearchApiAbstractAlterCallback { public function alterItems(array &$items) { - $type = $this->index->entity_type; foreach ($items as $id => &$item) { - if ($type == 'file') { - $url = array( - 'path' => file_create_url($url), - 'options' => array(), - ); - } - else { - $url = entity_uri($type, $item); - } + $url = $this->index->datasource()->getItemUrl($item); if (!$url) { $item->search_api_url = NULL; continue; diff --git a/includes/callback_add_viewed_entity.inc b/includes/callback_add_viewed_entity.inc index 78731cb..bd0f8d8 100644 --- a/includes/callback_add_viewed_entity.inc +++ b/includes/callback_add_viewed_entity.inc @@ -5,8 +5,17 @@ */ 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->entity_type); + $info = entity_get_info($this->index->item_type); $view_modes = array(); foreach ($info['view modes'] as $key => $mode) { $view_modes[$key] = $mode['label']; @@ -51,7 +60,7 @@ class SearchApiAlterAddViewedEntity extends SearchApiAbstractAlterCallback { $original_user = $GLOBALS['user']; $GLOBALS['user'] = drupal_anonymous_user(); - $type = $this->index->entity_type; + $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(), diff --git a/includes/callback_bundle_filter.inc b/includes/callback_bundle_filter.inc index f19deb4..8e39200 100644 --- a/includes/callback_bundle_filter.inc +++ b/includes/callback_bundle_filter.inc @@ -7,7 +7,7 @@ class SearchApiAlterBundleFilter extends SearchApiAbstractAlterCallback { public function supportsIndex(SearchApiIndex $index) { - return self::hasBundles(entity_get_info($index->entity_type)); + return self::hasBundles(entity_get_info($index->item_type)); } public function alterItems(array &$items) { diff --git a/includes/datasource.inc b/includes/datasource.inc new file mode 100644 index 0000000..3b702ea --- /dev/null +++ b/includes/datasource.inc @@ -0,0 +1,649 @@ +") 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". + * + * @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. + * + * @throws SearchApiDataSourceException + * If any of the indexes doesn't use the same item type as this controller. + */ + public function trackItemChange($item_ids, array $indexes); + + /** + * 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) { + // Types that set "track index status" to FALSE should override this method + // in their data source controller and provide their own logic, if possible. + 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) { + // Types that set "track index status" to FALSE should override this method + // in their data source controller and provide their own logic, if possible. + 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) { + // Types that set "track index status" to FALSE should override this method + // in their data source controller and provide their own logic, if possible. + if (!$this->table) { + return; + } + $insert = db_insert($this->table) + ->fields(array($this->itemIdColumn, $this->indexIdColumn, $this->changedColumn)); + foreach ($item_ids 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". + * + * @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. + * + * @throws SearchApiDataSourceException + * If any of the indexes doesn't use the same item type as this controller. + */ + public function trackItemChange($item_ids, array $indexes) { + // Types that set "track index status" to FALSE should override this method + // in their data source controller and provide their own logic, if possible. + 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); + if (is_array($item_ids)) { + $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) { + // Types that set "track index status" to FALSE should override this method + // in their data source controller and provide their own logic, if possible. + 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') + ->condition($this->changedColumn, 0) + ->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->itemIdColumn, '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) { + // Types that set "track index status" to FALSE should override this method + // in their data source controller and provide their own logic, if possible. + 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); + } + } + +} diff --git a/includes/datasource_entity.inc b/includes/datasource_entity.inc new file mode 100644 index 0000000..c3203dd --- /dev/null +++ b/includes/datasource_entity.inc @@ -0,0 +1,200 @@ +") 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) { + return entity_load($this->type, $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()) { + 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)); + } + +} diff --git a/includes/datasource_external.inc b/includes/datasource_external.inc new file mode 100644 index 0000000..480466d --- /dev/null +++ b/includes/datasource_external.inc @@ -0,0 +1,266 @@ +") 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['properties']['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. + * + * @throws SearchApiDataSourceException + * If any of the indexes doesn't use the same item type as this controller. + */ + public function trackItemChange($item_ids, array $indexes) { + 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, + ); + } + +} diff --git a/includes/exception.inc b/includes/exception.inc index 80e5c7e..4e7a0c8 100644 --- a/includes/exception.inc +++ b/includes/exception.inc @@ -20,3 +20,10 @@ class SearchApiException extends Exception { } } + +/** + * Represents an exception that occurred in a data source controller. + */ +class SearchApiDataSourceException extends SearchApiException { + +} diff --git a/includes/index_entity.inc b/includes/index_entity.inc index fd9ded5..08630ea 100644 --- a/includes/index_entity.inc +++ b/includes/index_entity.inc @@ -5,6 +5,16 @@ */ 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(). * @@ -13,56 +23,82 @@ class SearchApiIndex extends Entity { 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 + // 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 $entity_type; + public $item_type; /** * An array of options for configuring this index. The layout is as follows: @@ -100,17 +136,21 @@ class SearchApiIndex extends Entity { * * @var array */ - public $options; + public $options = array(); /** + * A flag indicating whether this index is enabled. + * * @var integer */ - public $enabled; + public $enabled = 1; /** + * A flag indicating whether to write to this index. + * * @var integer */ - public $read_only; + public $read_only = 0; /** * Constructor as a helper to the parent constructor. @@ -141,8 +181,7 @@ class SearchApiIndex extends Entity { } /** - * Execute necessary tasks when index is either deleted from the database or - * not defined in code anymore. + * Execute necessary tasks when the index is removed from the database. */ public function postDelete() { if ($server = $this->server()) { @@ -164,50 +203,8 @@ class SearchApiIndex extends Entity { * Record entities to index. */ public function queueItems() { - $this->dequeueItems(); - if (!$this->read_only) { - $entity_info = entity_get_info($this->entity_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']; - - // 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' => $this->id)); - $query->addExpression('1', 'changed'); - - // INSERT ... SELECT ... - db_insert('search_api_item') - ->from($query) - ->execute(); - } - else { - // In the absence of a 'base table', use the slow entity_load(). - - // Get an array of all entities using entity_load(). - $entities = entity_load($this->entity_type, FALSE); - - $query = db_insert('search_api_item') - ->fields(array('item_id', 'index_id', 'changed')); - - // Add each entity to the query. - foreach ($entities as $item_id => $entity) { - $query->values(array( - 'item_id' => $item_id, - 'index_id' => $this->id, - 'changed' => 1, - )); - } - - $query->execute(); - } + $this->datasource()->startTracking(array($this)); } } @@ -215,9 +212,7 @@ class SearchApiIndex extends Entity { * Remove all records of entities to index. */ public function dequeueItems() { - $query = db_delete('search_api_item') - ->condition('index_id', $this->id) - ->execute(); + $this->datasource()->stopTracking(array($this)); } /** @@ -285,10 +280,8 @@ class SearchApiIndex extends Entity { if (!$this->server || $this->read_only) { return TRUE; } - $ret = _search_api_index_reindex($this->id); - if($ret) { - module_invoke_all('search_api_index_reindex', $this, FALSE); - } + _search_api_index_reindex($this); + module_invoke_all('search_api_index_reindex', $this, FALSE); return TRUE; } @@ -310,19 +303,16 @@ class SearchApiIndex extends Entity { 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->id]) - || (array_search('add', $tasks[$server->machine_name][$this->id]) === FALSE - && array_search('clear', $tasks[$server->machine_name][$this->id]) === FALSE)) { - $tasks[$server->machine_name][$this->id][] = 'clear'; + 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); } } - $ret = _search_api_index_reindex($this->id); - if($ret) { - module_invoke_all('search_api_index_reindex', $this, TRUE); - } - + _search_api_index_reindex($this); + module_invoke_all('search_api_index_reindex', $this, TRUE); return TRUE; } @@ -336,11 +326,27 @@ class SearchApiIndex extends Entity { */ public function __sleep() { $ret = get_object_vars($this); - unset($ret['server_object'], $ret['processors'], $ret['added_properties'], $ret['fulltext_fields']); + 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 @@ -388,11 +394,11 @@ class SearchApiIndex extends Entity { /** * Indexes items on this index. Will return an array of IDs of items that - * should be marked as indexed – i.e. items that were either rejected by a + * 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 entities to index. + * An array of items to index. * * @return array * An array of the IDs of all items that should be marked as indexed. @@ -687,15 +693,37 @@ class SearchApiIndex extends Entity { * 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 entity type of this index, optionally loaded with the - * given data, and having fields according to the data alterations of this - * index. + * 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) { - $info['property info alter'] = array($this, 'propertyInfoAlter'); + 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 entity_metadata_wrapper($this->entity_type, $item, $info); + 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); } } diff --git a/includes/processor.inc b/includes/processor.inc index 3b4985f..288e8f5 100644 --- a/includes/processor.inc +++ b/includes/processor.inc @@ -29,7 +29,7 @@ interface SearchApiProcessorInterface { * * 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 entity type. Also, since this is only used for UI purposes, you + * 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. diff --git a/includes/query.inc b/includes/query.inc index 0c3f9aa..958f303 100644 --- a/includes/query.inc +++ b/includes/query.inc @@ -127,7 +127,7 @@ interface SearchApiQueryInterface { * * @param $field * The field to sort by. The special fields 'search_api_relevance' (sort by - * relevance) and 'search_api_id' (sort by entity id) may be used. + * relevance) and 'search_api_id' (sort by item id) may be used. * @param $order * The order to sort items in - either 'ASC' or 'DESC'. * @@ -170,10 +170,10 @@ interface SearchApiQueryInterface { * already ready-to-use. This allows search engines (or postprocessors) * to store extracted fields so other modules don't have to extract them * again. This fields should always be checked by modules that want to - * use field contents of the result entities. - * - entity: (optional) If set, the fully loaded entity. This field should - * always be used by modules using search results, to avoid duplicate - * entity loads. + * use field contents of the result items. + * - entity: (optional) If set, the fully loaded result item. This field + * should always be used by modules using search results, to avoid + * duplicate item loads. * - excerpt: (optional) If set, an HTML text containing highlighted * portions of the fulltext that match the query. * - warnings: A numeric array of translated warning messages that may be @@ -571,7 +571,7 @@ class SearchApiQuery implements SearchApiQueryInterface { * * @param $field * The field to sort by. The special fields 'search_api_relevance' (sort by - * relevance) and 'search_api_id' (sort by entity id) may be used. + * relevance) and 'search_api_id' (sort by item id) may be used. * @param $order * The order to sort items in - either 'ASC' or 'DESC'. * @@ -635,10 +635,10 @@ class SearchApiQuery implements SearchApiQueryInterface { * already ready-to-use. This allows search engines (or postprocessors) * to store extracted fields so other modules don't have to extract them * again. This fields should always be checked by modules that want to - * use field contents of the result entities. - * - entity: (optional) If set, the fully loaded entity. This field should - * always be used by modules using search results, to avoid duplicate - * entity loads. + * use field contents of the result items. + * - entity: (optional) If set, the fully loaded result item. This field + * should always be used by modules using search results, to avoid + * duplicate item loads. * - excerpt: (optional) If set, an HTML text containing highlighted * portions of the fulltext that match the query. * - warnings: A numeric array of translated warning messages that may be diff --git a/includes/server_entity.inc b/includes/server_entity.inc index d52c8b5..6db4229 100644 --- a/includes/server_entity.inc +++ b/includes/server_entity.inc @@ -11,36 +11,50 @@ 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; diff --git a/includes/service.inc b/includes/service.inc index ae7d494..9736d07 100644 --- a/includes/service.inc +++ b/includes/service.inc @@ -155,8 +155,8 @@ interface SearchApiServiceInterface { * 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 through a - * hook_entity_property_info(). + * - 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 diff --git a/search_api.admin.inc b/search_api.admin.inc index 0d298a2..14bb0ca 100644 --- a/search_api.admin.inc +++ b/search_api.admin.inc @@ -508,6 +508,7 @@ function search_api_admin_add_index(array $form, array &$form_state) { drupal_set_title(t('Add index')); $form['#attached']['css'][] = drupal_get_path('module', 'search_api') . '/search_api.admin.css'; + $form['#tree'] = TRUE; $form['name'] = array( '#type' => 'textfield', '#title' => t('Index name'), @@ -523,18 +524,16 @@ function search_api_admin_add_index(array $form, array &$form_state) { ), ); - $form['entity_type'] = array( + $form['item_type'] = array( '#type' => 'select', - '#title' => t('Entity type'), - '#description' => t('Select the type of entity that will be indexed in this index. ' . + '#title' => t('Item type'), + '#description' => t('Select the type of items that will be indexed in this index. ' . 'This setting cannot be changed afterwards.'), '#options' => array(), '#required' => TRUE, ); - foreach (entity_get_info() as $name => $entity) { - if (entity_get_property_info($name)) { - $form['entity_type']['#options'][$name] = $entity['label']; - } + foreach (search_api_get_item_type_info() as $type => $info) { + $form['item_type']['#options'][$type] = $info['name']; } $form['enabled'] = array( '#type' => 'checkbox', @@ -565,14 +564,14 @@ function search_api_admin_add_index(array $form, array &$form_state) { $form['server']['#options'][$server->machine_name] = t('@server_name (disabled)', array('@server_name' => $server->name)); } } - $form['index_directly'] = array( + $form['options']['index_directly'] = array( '#type' => 'checkbox', '#title' => t('Index items immediately'), '#description' => t('Immediately index new or updated items instead of waiting for the next cron run. ' . 'This might have serious performance drawbacks and is generally not advised for larger sites.'), '#default_value' => FALSE, ); - $form['cron_limit'] = array( + $form['options']['cron_limit'] = array( '#type' => 'textfield', '#title' => t('Cron limit'), '#description' => t('Set how many items will be indexed at most during each run of cron. ' . @@ -599,10 +598,10 @@ function search_api_admin_add_index_validate(array $form, array &$form_state) { form_set_error('machine_name', t('The machine name must not be a pure number.')); } - $cron_limit = $form_state['values']['cron_limit']; + $cron_limit = $form_state['values']['options']['cron_limit']; if ($cron_limit != '' . ((int) $cron_limit)) { // We don't enforce stricter rules and treat all negative values as -1. - form_set_error('cron_limit', t('The cron limit must be an integer.')); + form_set_error('options[cron_limit]', t('The cron limit must be an integer.')); } } @@ -613,14 +612,10 @@ function search_api_admin_add_index_submit(array $form, array &$form_state) { form_state_values_clean($form_state); $values = $form_state['values']; - $values['options'] = array( - 'cron_limit' => $values['cron_limit'], - 'index_directly' => $values['index_directly'], - ); - unset($values['cron_limit']); - // Trying to create an enabled index on a disabled server is handled elsewhere - $id = search_api_index_insert($values); + // Validation of whether the server of an enabled index is also enabled is + // done in the *_insert() function. + search_api_index_insert($values); drupal_set_message(t('The index was successfully created. Please set up its indexed fields now.')); $form_state['redirect'] = 'admin/config/search/search_api/index/' . $values['machine_name'] . '/fields'; @@ -661,7 +656,7 @@ function search_api_admin_index_view(SearchApiIndex $index = NULL, $action = NUL '#name' => $index->name, '#machine_name' => $index->machine_name, '#description' => $index->description, - '#entity_type' => $index->entity_type, + '#item_type' => $index->item_type, '#enabled' => $index->enabled, '#server' => $index->server(), '#options' => $index->options, @@ -681,7 +676,7 @@ function search_api_admin_index_view(SearchApiIndex $index = NULL, $action = NUL * - name: The index' name. * - machine_name: The index' machine name. * - description: The index' description. - * - entity_type: The type of entities stored in this index. + * - item_type: The type of items stored in this index. * - enabled: Boolean indicating whether the index is enabled. * - server: The server this index currently rests on, if any. * - options: The index' options, like cron limit. @@ -717,9 +712,9 @@ function theme_search_api_index(array $variables) { $output .= '
' . t('Machine name') . '
' . "\n"; $output .= '
' . check_plain($machine_name) . '
' . "\n"; - $output .= '
' . t('Entity type') . '
' . "\n"; - $type = entity_get_info($entity_type); - $type = $type['label']; + $output .= '
' . t('Item type') . '
' . "\n"; + $type = search_api_get_item_type_info($item_type); + $type = $type['name']; $output .= '
' . check_plain($type) . '
' . "\n"; if (!empty($description)) { @@ -989,6 +984,7 @@ function search_api_admin_index_edit(array $form, array &$form_state, SearchApiI $form_state['index'] = $index; $form['#attached']['css'][] = drupal_get_path('module', 'search_api') . '/search_api.admin.css'; + $form['#tree'] = TRUE; $form['name'] = array( '#type' => 'textfield', '#title' => t('Index name'), @@ -1033,7 +1029,7 @@ function search_api_admin_index_edit(array $form, array &$form_state, SearchApiI '#description' => t('Do not write to this index or track ids of entities in this index.'), '#default_value' => $index->read_only, ); - $form['index_directly'] = array( + $form['options']['index_directly'] = array( '#type' => 'checkbox', '#title' => t('Index items immediately'), '#description' => t('Immediately index new or updated items instead of waiting for the next cron run. ' . @@ -1043,7 +1039,7 @@ function search_api_admin_index_edit(array $form, array &$form_state, SearchApiI 'invisible' => array(':input[name="read_only"]' => array('checked' => TRUE)), ), ); - $form['cron_limit'] = array( + $form['options']['cron_limit'] = array( '#type' => 'textfield', '#title' => t('Cron limit'), '#description' => t('Set how many items will be indexed at most during each run of cron. ' . @@ -1073,11 +1069,7 @@ function search_api_admin_index_edit_submit(array $form, array &$form_state) { $values = $form_state['values']; $index = $form_state['index']; - $values['options'] = $index->options; - $values['options']['cron_limit'] = $values['cron_limit']; - unset($values['cron_limit']); - $values['options']['index_directly'] = $values['index_directly']; - unset($values['index_directly']); + $values['options'] += $index->options; $ret = $index->update($values); $form_state['redirect'] = 'admin/config/search/search_api/index/' . $index->machine_name; @@ -1467,7 +1459,7 @@ function search_api_admin_index_fields(array $form, array &$form_state, SearchAp $form['description'] = array( '#type' => 'item', '#title' => t('Select fields to index'), - '#description' => t('

The datatype of fields determines, how they can be searched or filtered upon. ' . + '#description' => t('

The datatype of a field determines how it can be used for searching and filtering. ' . 'The boost is used to give additional weight to certain fields, e.g. titles or tags. It only takes effect for fulltext fields.

' . '

Whether detailled field types are supported depends on the type of server this index resides on. ' . 'In any case, fields of type "Fulltext" will always be fulltext-searchable.

'), diff --git a/search_api.api.php b/search_api.api.php index 9cd3608..7339c57 100644 --- a/search_api.api.php +++ b/search_api.api.php @@ -65,6 +65,79 @@ function hook_search_api_service_info_alter(array &$service_info) { } /** + * Define new types of items that can be searched. + * + * This hook allows modules to define their own item types, for which indexes + * can then be created. (Note that the Search API natively provides support for + * all entity types that specify property information, so they should not be + * added here. You should therefore also not use an existing entity type as the + * identifier of a new item type.) + * + * The main part of defining a new item type is implementing its data source + * controller class, which is responsible for loading items, providing metadata + * and tracking existing items. The module defining a certain item type is also + * responsible for observing creations, updates and deletions of items of that + * type and notifying the Search API of them by calling + * search_api_track_item_insert(), search_api_track_item_change() and + * search_api_track_item_delete(), as appropriate. + * The only other restriction for item types is that they have to have a single + * item ID field, with a scalar value. This is, e.g., used to track indexed + * items. + * + * Note, however, that you can also define item types where some of these + * conditions are not met, as long as you are aware that some functionality of + * the Search API and related modules might then not be available for that type. + * + * @return array + * An associative array keyed by item type identifier, and containing type + * information arrays with at least the following keys: + * - name: A human-readable name for the type. + * - datasource controller: A class implementing the + * SearchApiDataSourceControllerInterface interface which will be used as + * the data source controller for this type. + * Other, datasource-specific settings might also be placed here. These should + * be specified with the data source controller in question. + * + * @see hook_search_api_item_type_info_alter() + */ +function hook_search_api_item_type_info() { + // Copied from search_api_search_api_item_type_info(). + $types = array(); + + foreach (entity_get_property_info() as $type => $property_info) { + if ($info = entity_get_info($type)) { + $types[$type] = array( + 'name' => $info['label'], + 'datasource controller' => 'SearchApiEntityDataSourceController', + ); + } + } + + return $types; +} + +/** + * Alter the item type info. + * + * Modules may implement this hook to alter the information that defines an + * item type. All properties that are available in + * hook_search_api_item_type_info() can be altered here. + * + * @param array $infos + * The item type info array, keyed by type identifier. + * + * @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) { + $info['is_entity'] = (bool) entity_get_info($type); + } +} + +/** * 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. diff --git a/search_api.drush.inc b/search_api.drush.inc index b6d9858..dfc5304 100644 --- a/search_api.drush.inc +++ b/search_api.drush.inc @@ -103,17 +103,21 @@ function drush_search_api_list() { dt('Index'), dt('Server'), dt('Type'), - dt('Enabled'), + dt('Status'), dt('Limit'), ); foreach ($indexes as $index) { + $type = search_api_get_item_type_info($index->item_type); + $type = isset($type['name']) ? $type['name'] : $index->item_type; + $server = $index->server(); + $server = $server ? $server->name : '(' . t('none') . ')'; $row = array( $index->id, $index->name, $index->machine_name, - $index->server, - $index->entity_type, - $index->enabled, + $server, + $type, + $index->enabled ? t('enabled') : t('disabled'), $index->options['cron_limit'], ); $rows[] = $row; diff --git a/search_api.info b/search_api.info index 440825b..dd5d70b 100644 --- a/search_api.info +++ b/search_api.info @@ -11,6 +11,9 @@ files[] = includes/callback_add_aggregation.inc files[] = includes/callback_add_url.inc files[] = includes/callback_add_viewed_entity.inc files[] = includes/callback_bundle_filter.inc +files[] = includes/datasource.inc +files[] = includes/datasource_entity.inc +files[] = includes/datasource_external.inc files[] = includes/exception.inc files[] = includes/index_entity.inc files[] = includes/processor.inc diff --git a/search_api.install b/search_api.install index e4ad0f2..285bd7f 100644 --- a/search_api.install +++ b/search_api.install @@ -109,8 +109,8 @@ function search_api_schema() { 'length' => 50, 'not null' => FALSE, ), - 'entity_type' => array( - 'description' => 'The entity type of items stored in this index.', + 'item_type' => array( + 'description' => 'The type of items stored in this index.', 'type' => 'varchar', 'length' => 50, 'not null' => TRUE, @@ -150,7 +150,7 @@ function search_api_schema() { ), ), 'indexes' => array( - 'entity_type' => array('entity_type'), + 'item_type' => array('item_type'), 'server' => array('server'), 'enabled' => array('enabled'), ), @@ -210,7 +210,7 @@ function search_api_install() { 'machine_name' => preg_replace('/[^a-z0-9]+/', '_', drupal_strtolower($name)), 'description' => t('An automatically created search index for indexing node data. Might be configured to specific needs.'), 'server' => NULL, - 'entity_type' => 'node', + 'item_type' => 'node', 'options' => array( 'cron_limit' => '50', 'data_alter_callbacks' => array( @@ -816,3 +816,24 @@ function search_api_update_7108() { function search_api_update_7109() { cache_clear_all('entity_info:', 'cache', TRUE); } + +/** + * Rename the "entity_type" field to "item_type" in the {search_api_index} table. + */ +function search_api_update_7110() { + $table = 'search_api_index'; + // This index isn't used anymore. + db_drop_index($table, 'entity_type'); + // Rename the "item_type" field (and change the description). + $item_type = array( + 'description' => 'The type of items stored in this index.', + 'type' => 'varchar', + 'length' => 50, + 'not null' => TRUE, + ); + // Also add the new "item_type" index, while we're at it. + $keys_new['indexes']['item_type'] = array('item_type'); + db_change_field($table, 'entity_type', 'item_type', $item_type, $keys_new); + // Clear entity info caches. + cache_clear_all('*', 'cache', TRUE); +} diff --git a/search_api.module b/search_api.module index 4dbbdcc..102fc99 100644 --- a/search_api.module +++ b/search_api.module @@ -177,7 +177,7 @@ function search_api_theme() { 'name' => '', 'machine_name' => '', 'description' => NULL, - 'entity_type' => NULL, + 'item_type' => NULL, 'enabled' => NULL, 'server' => NULL, 'options' => array(), @@ -283,35 +283,42 @@ function search_api_entity_property_info() { 'label' => t('ID'), 'type' => 'integer', 'description' => t('The primary identifier for a server.'), + 'schema field' => 'id', + 'validation callback' => 'entity_metadata_validate_integer_positive', ), 'name' => array( 'label' => t('Name'), 'type' => 'text', 'description' => t('The displayed name for a server.'), + 'schema field' => 'name', 'required' => TRUE, ), 'machine_name' => array( 'label' => t('Machine name'), 'type' => 'token', 'description' => t('The internally used machine name for a server.'), + 'schema field' => 'machine_name', 'required' => TRUE, ), 'description' => array( 'label' => t('Description'), 'type' => 'text', 'description' => t('The displayed description for a server.'), + 'schema field' => 'description', 'sanitize' => 'filter_xss', ), 'class' => array( 'label' => t('Service class'), 'type' => 'text', 'description' => t('The ID of the service class to use for this server.'), + 'schema field' => 'class', 'required' => TRUE, ), 'enabled' => array( 'label' => t('Enabled'), 'type' => 'boolean', 'description' => t('A flag indicating whether the server is enabled.'), + 'schema field' => 'enabled', ), ); $info['search_api_index']['properties'] = array( @@ -319,29 +326,35 @@ function search_api_entity_property_info() { 'label' => t('ID'), 'type' => 'integer', 'description' => t('An integer identifying the index.'), + 'schema field' => 'id', + 'validation callback' => 'entity_metadata_validate_integer_positive', ), 'name' => array( 'label' => t('Name'), 'type' => 'text', 'description' => t('A name to be displayed for the index.'), + 'schema field' => 'name', 'required' => TRUE, ), 'machine_name' => array( 'label' => t('Machine name'), 'type' => 'token', 'description' => t('The internally used machine name for an index.'), + 'schema field' => 'machine_name', 'required' => TRUE, ), 'description' => array( 'label' => t('Description'), 'type' => 'text', 'description' => t("A string describing the index' use to users."), + 'schema field' => 'description', 'sanitize' => 'filter_xss', ), 'server' => array( 'label' => t('Server ID'), 'type' => 'token', 'description' => t('The machine name of the search_api_server with which data should be indexed.'), + 'schema field' => 'server', ), 'server_entity' => array( 'label' => t('Server'), @@ -349,21 +362,24 @@ function search_api_entity_property_info() { 'description' => t('The search_api_server with which data should be indexed.'), 'getter callback' => 'search_api_index_get_server', ), - 'entity_type' => array( - 'label' => t('Entity type'), - 'type' => 'text', - 'description' => t('The entity type of items stored in this index.'), + 'item_type' => array( + 'label' => t('Item type'), + 'type' => 'token', + 'description' => t('The type of items stored in this index.'), + 'schema field' => 'item_type', 'required' => TRUE, ), 'enabled' => array( 'label' => t('Enabled'), 'type' => 'boolean', 'description' => t('A flag indicating whether the index is enabled.'), + 'schema field' => 'enabled', ), 'read_only' => array( 'label' => t('Read only'), 'type' => 'boolean', 'description' => t('A flag indicating whether the index is read-only.'), + 'schema field' => 'read_only', ), ); @@ -412,7 +428,7 @@ function search_api_search_api_server_update(SearchApiServer $server) { break; case 'fields': if ($server->fieldsUpdated($index)) { - _search_api_index_reindex($index->id); + _search_api_index_reindex($index); } break; case 'remove': @@ -509,7 +525,7 @@ function search_api_search_api_index_update(SearchApiIndex $index) { } // We also have to re-index all content - _search_api_index_reindex($index->id); + _search_api_index_reindex($index); } $old_fields = $index->original->options + array('fields' => array()); @@ -518,12 +534,12 @@ function search_api_search_api_index_update(SearchApiIndex $index) { $new_fields = $new_fields['fields']; if ($old_fields != $new_fields) { if ($index->server && $index->server()->fieldsUpdated($index)) { - _search_api_index_reindex($index->id); + _search_api_index_reindex($index); } } // If the index's enabled or read-only status is being changed, queue or - // dequeue entities for indexing. + // dequeue items for indexing. if (!$index->read_only && $index->enabled != $index->original->enabled) { if ($index->enabled) { $index->queueItems(); @@ -563,31 +579,15 @@ function search_api_search_api_index_delete(SearchApiIndex $index) { * The entity's type. */ function search_api_entity_insert($entity, $type) { - if ($type != 'search_api_index') { - // When inserting a new search index, the new index was already inserted into search_api_item. - // This would lead to a duplicate-key issue, if we would continue. - $info = entity_get_info($type); - $id = $info['entity keys']['id']; - $id = $entity->$id; - - $query = db_select('search_api_index', 'i') - ->condition('entity_type', $type) - ->condition('enabled', 1) - ->condition('read_only', 0); - $query->addField('i', 'id', 'index_id'); - $query->addExpression(':item_id', 'item_id', array(':item_id' => $id)); - $query->addExpression(':changed', 'changed', array(':changed' => 1)); - - db_insert('search_api_item') - ->from($query) - ->execute(); + // When inserting a new search index, the new index was already inserted into + // search_api_item. This would lead to a duplicate-key issue, if we would + // continue. + if ($type == 'search_api_index') { + return; } - - foreach (search_api_index_load_multiple(FALSE, array('enabled' => 1, 'entity_type' => $type, 'read_only' => 0)) as $index) { - if (!empty($index->options['index_directly'])) { - $item = clone $entity; - search_api_index_specific_items($index, array($id => $item)); - } + list($id) = entity_extract_ids($type, $entity); + if (isset($id)) { + search_api_track_item_insert($type, array($id)); } } @@ -602,17 +602,16 @@ function search_api_entity_insert($entity, $type) { * The entity's type. */ function search_api_entity_update($entity, $type) { - $info = entity_get_info($type); - $id = $info['entity keys']['id']; - $id = $entity->$id; - - search_api_mark_dirty($type, array($id)); + list($id) = entity_extract_ids($type, $entity); + if (isset($id)) { + search_api_track_item_change($type, array($id)); + } } /** * Implements hook_entity_delete(). * - * Removes the item from {search_api_item} and deletes it from all indexes. + * Removes the item from the tracking table and deletes it from all indexes. * * @param $entity * The updated entity. @@ -620,26 +619,30 @@ function search_api_entity_update($entity, $type) { * The entity's type. */ function search_api_entity_delete($entity, $type) { - $info = entity_get_info($type); - $id_field = $info['entity keys']['id']; - $id = $entity->$id_field; - foreach (search_api_index_load_multiple(FALSE, array('entity_type' => $type, 'read_only' => 0)) as $index) { - db_delete('search_api_item') - ->condition('item_id', $id) - ->condition('index_id', $index->id) - ->execute(); - if ($index->server) { - $server = $index->server(); - if ($server->enabled) { - $server->deleteItems(array($id), $index); - } - else { - $tasks = variable_get('search_api_tasks', array()); - $tasks[$server->machine_name][$index->machine_name][] = 'delete-' . $id; - variable_set('search_api_tasks', $tasks); - } + list($id) = entity_extract_ids($type, $entity); + if (isset($id)) { + search_api_track_item_delete($type, array($id)); + } +} + +/** + * Implements hook_search_api_item_type_info(). + * + * Adds item types for all entity types with property information. + */ +function search_api_search_api_item_type_info() { + $types = array(); + + foreach (entity_get_property_info() as $type => $property_info) { + if ($info = entity_get_info($type)) { + $types[$type] = array( + 'name' => $info['label'], + 'datasource controller' => 'SearchApiEntityDataSourceController', + ); } } + + return $types; } /** @@ -707,49 +710,108 @@ function search_api_search_api_processor_info() { } /** - * Mark the entities with the specified IDs as "dirty", i.e., as needing to be reindexed. + * Inserts new unindexed items for all indexes on the specified type. + * + * @param $type + * The item type of the new items. + * @param array $item_id + * The IDs of the new items. + */ +function search_api_track_item_insert($type, array $item_ids) { + $datasource = search_api_get_datasource_controller($type); + + $conditions = array( + 'enabled' => 1, + 'item_type' => $type, + 'read_only' => 0, + ); + $indexes = search_api_index_load_multiple(FALSE, $conditions); + if (!$indexes) { + return; + } + $datasource->trackItemInsert($item_ids, $indexes); + foreach ($indexes as $index) { + if (!empty($index->options['index_directly'])) { + $indexed = search_api_index_specific_items($index, $item_ids); + } + } +} + +/** + * Mark the items with the specified IDs as "dirty", i.e., as needing to be reindexed. * * For indexes for which items should be indexed immediately, the items are * indexed directly, instead. * - * @param $entity_type - * The type of entity, e.g., 'node'. - * @param array $ids - * The entity IDs of the entities to be marked dirty. + * @param $type + * The type of items, specific to the data source. + * @param array $item_ids + * The IDs of the items to be marked dirty. */ -function search_api_mark_dirty($entity_type, array $ids) { - $index_ids = array(); - foreach (search_api_index_load_multiple(FALSE, array('enabled' => 1, 'entity_type' => $entity_type, 'read_only' => 0)) as $index) { +function search_api_track_item_change($type, array $item_ids) { + $indexes = array(); + $datasource = search_api_get_datasource_controller($type); + $conditions = array( + 'enabled' => 1, + 'item_type' => $type, + 'read_only' => 0, + ); + foreach (search_api_index_load_multiple(FALSE, $conditions) as $index) { if (empty($index->options['index_directly'])) { - $index_ids[] = $index->id; + $indexes[] = $index; } else { - // For indexes with the index_directly set, index the items right away. - $items = entity_load($entity_type, $ids, array(), TRUE); - $indexed = search_api_index_specific_items($index, $items); - if (count($indexed) < count($ids)) { + // For indexes with the index_directly option set, index the items right + // away. + $indexed = array(); + try { + $indexed = search_api_index_specific_items($index, $item_ids); + } + catch (SearchApiException $e) { + watchdog('search_api', $e->getMessage(), NULL, WATCHDOG_ERROR); + } + if (count($indexed) < count($item_ids)) { // If indexing failed for some items, mark those as dirty. - $diff = array_diff($ids, $indexed); - db_update('search_api_item') - ->fields(array( - 'changed' => REQUEST_TIME, - )) - ->condition('item_id', $ids, 'IN') - ->condition('index_id', $index->id) - ->condition('changed', 0) - ->execute(); + $diff = array_diff($item_ids, $indexed); + $datasource->trackItemChange($diff, array($index)); } } } - if ($index_ids) { - db_update('search_api_item') - ->fields(array( - 'changed' => REQUEST_TIME, - )) - ->condition('item_id', $ids, 'IN') - ->condition('index_id', $index_ids, 'IN') - ->condition('changed', 0) - ->execute(); + if ($indexes) { + $datasource->trackItemChange($item_ids, $indexes); + } +} + +/** + * Marks items as successfully indexed for the specified index. + * + * @param SearchApiIndex $index + * The index on which items were indexed. + * @param array $item_ids + * The ids of the indexed items. + */ +function search_api_track_item_indexed(SearchApiIndex $index, array $item_ids) { + $index->datasource()->trackItemIndexed($item_ids, $index); +} + +/** + * Removes items from all indexes. + * + * @param $type + * The type of the items. + * @param array $item_ids + * The IDs of the deleted items. + */ +function search_api_track_item_delete($type, array $item_ids) { + $datasource = search_api_get_datasource_controller($type); + $conditions = array( + 'enabled' => 1, + 'item_type' => $type, + 'read_only' => 0, + ); + $indexes = search_api_index_load_multiple(FALSE, $conditions); + if ($indexes) { + $datasource->trackItemDelete($item_ids, $indexes); } } @@ -763,60 +825,52 @@ function search_api_mark_dirty($entity_type, array $ids) { * The number of items which should be indexed at most. -1 means no limit. * * @throws SearchApiException - * If the index' entity type is unknown or another error occurs during - * indexing. + * If any error occurs during indexing. * * @return * Number of successfully indexed items. */ function search_api_index_items(SearchApiIndex $index, $limit = -1) { - // Safety check if entity type is known (prevent failing of whole cron run/page request) - if (!entity_get_info($index->entity_type)) { - throw new SearchApiException(t("Couldn't index values for '!name' index (unknown entity type '!type')", array('!name' => $index->name, '!type' => $index->entity_type))); - } - // Don't try to index read-only indexes. if ($index->read_only) { return 0; } - $items = search_api_get_items_to_index($index, $limit); - if (!$items) { + $ids = search_api_get_items_to_index($index, $limit); + if (!$ids) { return 0; } - return count(search_api_index_specific_items($index, $items)); + return count(search_api_index_specific_items($index, $ids)); } /** - * Indexes the given items on the specified index. + * Indexes the specified items on the given index. * * Items which were successfully indexed are marked as such afterwards. * * @param SearchApiIndex $index * The index on which items should be indexed. - * @param array $items - * The items which should be indexed. Have to be entities of the appropriate - * type. + * @param array $ids + * The IDs of the items which should be indexed. * * @throws SearchApiException - * If the index' entity type is unknown or another error occurs during - * indexing. + * If any error occurs during indexing. * * @return * The IDs of all successfully indexed items. */ -function search_api_index_specific_items(SearchApiIndex $index, array $items) { +function search_api_index_specific_items(SearchApiIndex $index, array $ids) { + $items = $index->loadItems($ids); $indexed = $index->index($items); - if (!empty($indexed)) { - search_api_set_items_indexed($index, $indexed); + if ($indexed) { + search_api_track_item_indexed($index, $indexed); } return $indexed; } /** - * Returns a list of at most $limit items that need to be indexed for the - * specified index. + * Returns a list of items that need to be indexed for the specified index. * * @param SearchApiIndex $index * The index for which items should be retrieved. @@ -824,44 +878,13 @@ function search_api_index_specific_items(SearchApiIndex $index, array $items) { * The maximum number of items to retrieve. -1 means no limit. * * @return array - * An array of items (entities) that need to be indexed. + * An array of IDs of items that need to be indexed. */ function search_api_get_items_to_index(SearchApiIndex $index, $limit = -1) { if ($limit == 0) { return array(); } - $select = db_select('search_api_item', 'i'); - $select->addField('i', 'item_id'); - $select->condition('index_id', $index->id); - $select->condition('changed', 0, '<>'); - $select->orderBy('changed', 'ASC'); - if ($limit > 0) { - $select->range(0, $limit); - } - - $ids = $select->execute()->fetchCol(); - return entity_load($index->entity_type, $ids, array(), TRUE); -} - -/** - * Marks the items as successfully indexed for the specified index. - * - * @param SearchApiIndex $index - * The index on which items were indexed. - * @param array $ids - * The ids of the indexed items. - * - * @return - * The number of index entries changed. - */ -function search_api_set_items_indexed(SearchApiIndex $index, array $ids) { - return db_update('search_api_item') - ->fields(array( - 'changed' => 0, - )) - ->condition('index_id', $index->id) - ->condition('item_id', $ids, 'IN') - ->execute(); + return $index->datasource()->getChangedItems($index, $limit); } /** @@ -990,6 +1013,63 @@ function search_api_get_service_info($id = NULL) { } /** + * Returns information for either all item types, or a specific one. + * + * @param $type + * If set, the item type whose information should be returned. + * + * @return + * If $type is given, either an array containing the information of that item + * type, or NULL if it is unknown. Otherwise, an array keyed by type IDs + * containing the information for all item types. Item type information is + * formatted as specified by hook_search_api_item_type_info(), and has all + * optional fields filled with the defaults. + * + * @see hook_search_api_item_type_info() + */ +function search_api_get_item_type_info($type = NULL) { + $types = &drupal_static(__FUNCTION__); + + if (!isset($types)) { + $types = module_invoke_all('search_api_item_type_info'); + drupal_alter('search_api_item_type_info', $types); + } + + if (isset($type)) { + return isset($types[$type]) ? $types[$type] : NULL; + } + return $types; +} + +/** + * Get a data source controller object for the specified type. + * + * @param $type + * The type whose data source controller should be returned. + * + * @return SearchApiDataSourceControllerInterface + * The type's data source controller. + * + * @throws SearchApiException + * If the type is unknown or specifies an invalid data source controller. + */ +function search_api_get_datasource_controller($type) { + $datasources = &drupal_static(__FUNCTION__, array()); + if (empty($datasources[$type])) { + $info = search_api_get_item_type_info($type); + if (isset($info['datasource controller']) && class_exists($info['datasource controller'])) { + $datasources[$type] = new $info['datasource controller']($type); + } + if (empty($datasources[$type]) || !($datasources[$type] instanceof SearchApiDataSourceControllerInterface)) { + unset($datasources[$type]); + throw new SearchApiException(t('Unknown or invalid item type !type.', + array('!type' => $type))); + } + } + return $datasources[$type]; +} + +/** * Returns a list of all available data alter callbacks. * * @see hook_search_api_alter_callback_info() @@ -1007,8 +1087,6 @@ function search_api_get_alter_callbacks() { foreach ($callbacks as $id => $callback) { $callbacks[$id] += array('enabled' => TRUE, 'weight' => 0); } - - // @todo drupal_alter('search_api_alter_callback_info', $callbacks)? } return $callbacks; @@ -1032,8 +1110,6 @@ function search_api_get_processors() { foreach ($processors as $id => $processor) { $processors[$id] += array('enabled pre' => TRUE, 'enabled post' => TRUE, 'weight' => 0); } - - // @todo drupal_alter('search_api_processor_info', $processors)? } return $processors; @@ -1160,7 +1236,7 @@ function search_api_extract_fields(EntityMetadataWrapper $wrapper, array $fields unset($info); try { foreach ($wrapper as $i => $w) { - $nested_fields = search_api_extract_fields($w, $fields); + $nested_fields = search_api_extract_fields($w, $fields, $value_options); foreach ($nested_fields as $field => $info) { if (isset($info['value'])) { $fields[$field]['value'][] = $info['value']; @@ -1214,7 +1290,7 @@ function search_api_extract_fields(EntityMetadataWrapper $wrapper, array $fields foreach ($nested as $prefix => $nested_fields) { if (isset($wrapper->$prefix)) { - $nested_fields = search_api_extract_fields($wrapper->$prefix, $nested_fields); + $nested_fields = search_api_extract_fields($wrapper->$prefix, $nested_fields, $value_options); foreach ($nested_fields as $field => $info) { $fields["$prefix:$field"] = $info; } @@ -1270,50 +1346,6 @@ function _search_api_extract_entity_value(EntityMetadataWrapper $wrapper, $fullt } /** - * Returns a list of all (enabled) search servers. - * - * @deprecated - * This function doesn't take exported entities in code into account. Use - * entity_load() instead and sort manually, if you must. - * - * @param $only_enabled - * Whether to retrieve only enabled servers. - * @param $header - * A table header to sort by. - * - * @return array - * An array of objects representing all (or, if $enabled is TRUE, only - * enabled) search servers. - */ -// @todo Remove when changing the API. -function search_api_list_servers($only_enabled = TRUE, $header = NULL) { - $servers = &drupal_static(__FUNCTION__); - - $enabled = (int) $only_enabled; - if (!isset($servers[$enabled])) { - $select = db_select('search_api_server', 's', array('fetch' => 'SearchApiServer'))->fields('s'); - if ($enabled) { - $select->condition('s.enabled', 1); - } - if (!empty($header)) { - $select = $select->extend('TableSort')->orderByHeader($header); - } - - $results = $select->execute(); - $servers[$enabled] = array(); - foreach ($results as $row) { - $row->options = unserialize($row->options); - $servers[$enabled][$row->machine_name] = $row; - } - - module_invoke_all('search_api_server_load', $servers[$enabled]); - module_invoke_all('entity_load', $servers[$enabled], 'search_api_server'); - } - - return $servers[$enabled]; -} - -/** * Load the search server with the specified id. * * @param $id @@ -1470,57 +1502,6 @@ function search_api_server_delete($id) { } /** - * Returns a list of search indexes. - * - * @deprecated - * This function doesn't take exported entities in code into account. Use - * entity_load() instead and sort manually, if you must. - * - * @param $options - * An associative array of conditions on the returned indexes. - * - enabled: When set to TRUE, only enabled indexes will be returned. - * - server: Return only indexes on the server with the specified id. - * - entity_type: Return only indexes on the specified entity type. - * @param $header - * A table header to sort by. - * - * @return array - * An array of objects representing the search indexes that meet the - * specified criteria. - */ -// @todo Remove when changing the API. -function search_api_list_indexes(array $options = array(), $header = NULL) { - $server = empty($options['server']) ? NULL : $options['server']; - $type = empty($options['entity_type']) ? NULL : $options['entity_type']; - - $select = db_select('search_api_index', 'i', array('fetch' => 'SearchApiIndex'))->fields('i'); - if (!empty($options['enabled'])) { - $select->condition('i.enabled', 1); - } - if (!empty($server)) { - $select->condition('i.server', $server); - } - if (!empty($type)) { - $select->condition('i.entity_type', $type); - } - if (!empty($header)) { - $select = $select->extend('TableSort')->orderByHeader($header); - } - - $results = $select->execute(); - $indexes = array(); - foreach ($results as $row) { - $row->options = unserialize($row->options); - $indexes[$row->machine_name] = $row; - } - - module_invoke_all('search_api_index_load', $indexes); - module_invoke_all('entity_load', $indexes, 'search_api_index'); - - return $indexes; -} - -/** * Loads the Search API index with the specified id. * * @param $id @@ -1563,28 +1544,16 @@ function search_api_index_load_multiple($ids = array(), $conditions = array(), $ /** * Determines a search index' indexing status. * - * @param $index - * Either an index object, or the index' machine name. + * @param SearchApiIndex $index + * The index whose indexing status should be determined. * * @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. */ -function search_api_index_status($index) { - $id = is_object($index) ? $index->id : $index; - $indexed = db_select('search_api_item', 'i') - ->condition('index_id', $id) - ->condition('changed', 0) - ->countQuery() - ->execute() - ->fetchField(); - $total = db_select('search_api_item', 'i') - ->condition('index_id', $id) - ->countQuery() - ->execute() - ->fetchField(); - return array('indexed' => $indexed, 'total' => $total); +function search_api_index_status(SearchApiIndex $index) { + return $index->datasource()->getIndexStatus($index); } /** @@ -1712,18 +1681,11 @@ function search_api_index_reindex($id) { /** * Helper method for marking all items on an index as needing re-indexing. * - * @param $id - * The numeric ID of the index to re-index. - * - * @return - * The number of items affected. - */ -function _search_api_index_reindex($id) { - return db_update('search_api_item') - ->fields(array('changed' => REQUEST_TIME)) - ->condition('changed', 0) - ->condition('index_id', $id) - ->execute(); + * @param SearchApiIndex $index + * The index whose items should be re-indexed. + */ +function _search_api_index_reindex(SearchApiIndex $index) { + $index->datasource()->trackItemChange(FALSE, array($index)); } /** diff --git a/search_api.test b/search_api.test index 46adf78..8f82bb3 100644 --- a/search_api.test +++ b/search_api.test @@ -38,7 +38,7 @@ class SearchApiWebTest extends DrupalWebTestCase { public function testFramework() { $this->drupalLogin($this->drupalCreateUser(array('administer search_api'))); - // @todo Why is there no default index. + // @todo Why is there no default index? //$this->deleteDefaultIndex(); $this->insertItems(); $this->checkOverview1(); @@ -108,25 +108,25 @@ class SearchApiWebTest extends DrupalWebTestCase { protected function createIndex() { $values = array( 'name' => '', - 'entity_type' => '', + 'item_type' => '', 'enabled' => 1, 'description' => 'An index used for testing.', 'server' => '', - 'cron_limit' => 5, + 'options[cron_limit]' => 5, ); $this->drupalPost('admin/config/search/search_api/add_index', $values, t('Create index')); $this->assertText(t('!name field is required.', array('!name' => t('Index name')))); - $this->assertText(t('!name field is required.', array('!name' => t('Entity type')))); + $this->assertText(t('!name field is required.', array('!name' => t('Item type')))); $this->index_id = $id = 'test_index'; $values = array( 'name' => 'Search API test index', 'machine_name' => $id, - 'entity_type' => 'search_api_test', + 'item_type' => 'search_api_test', 'enabled' => 1, 'description' => 'An index used for testing.', 'server' => '', - 'cron_limit' => 5, + 'options[cron_limit]' => 5, ); $this->drupalPost(NULL, $values, t('Create index')); @@ -135,11 +135,11 @@ class SearchApiWebTest extends DrupalWebTestCase { $this->assertTrue($found, t('Correct redirect.')); $index = search_api_index_load($id, TRUE); $this->assertEqual($index->name, $values['name'], t('Name correctly inserted.')); - $this->assertEqual($index->entity_type, $values['entity_type'], t('Index entity type correctly inserted.')); + $this->assertEqual($index->item_type, $values['item_type'], t('Index item type correctly inserted.')); $this->assertFalse($index->enabled, t('Status correctly inserted.')); $this->assertEqual($index->description, $values['description'], t('Description correctly inserted.')); $this->assertNull($index->server, t('Index server correctly inserted.')); - $this->assertEqual($index->options['cron_limit'], $values['cron_limit'], t('Cron limit correctly inserted.')); + $this->assertEqual($index->options['cron_limit'], $values['options[cron_limit]'], t('Cron limit correctly inserted.')); $values = array( 'additional[field]' => 'parent', @@ -212,7 +212,7 @@ class SearchApiWebTest extends DrupalWebTestCase { $this->drupalGet("admin/config/search/search_api/index/$id"); $this->assertTitle('Search API test index | Drupal', t('Correct title when viewing index.')); $this->assertText('An index used for testing.', t('!field displayed.', array('!field' => t('Description')))); - $this->assertText('Search API test entity', t('!field displayed.', array('!field' => t('Entity type')))); + $this->assertText('Search API test entity', t('!field displayed.', array('!field' => t('Item type')))); $this->assertText(format_plural(5, '1 item per cron run.', '@count items per cron run.'), t('!field displayed.', array('!field' => t('Cron limit')))); $this->drupalGet("admin/config/search/search_api/index/$id/status"); @@ -409,7 +409,7 @@ class SearchApiUnitTest extends DrupalWebTestCase { 'id' => 1, 'name' => 'test', 'enabled' => 1, - 'entity_type' => 'user', + 'item_type' => 'user', 'options' => array( 'fields' => array( 'name' => array(