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 .= '- ' . theme('search_api_page_result', array('index' => $index, 'result' => $item, 'entity' => isset($entities[$item['id']]) ? $entities[$item['id']] : NULL, 'keys' => $keys)) . '
';
+ $output .= '- ' . theme('search_api_page_result', array('index' => $index, 'result' => $item, 'item' => isset($items[$item['id']]) ? $items[$item['id']] : NULL, 'keys' => $keys)) . '
';
}
$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(