123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197 |
- <?php
- /**
- * Search service class using the database for storing index information.
- */
- class SearchApiDbService extends SearchApiAbstractService {
- protected $previous_db;
- protected $query_options = array();
- protected $ignored = array();
- protected $warnings = array();
- public function configurationForm(array $form, array &$form_state) {
- if (empty($this->options)) {
- global $databases;
- foreach ($databases as $key => $targets) {
- foreach ($targets as $target => $info) {
- $options[$key]["$key:$target"] = "$key > $target";
- }
- }
- if (count($options) > 1 || count(reset($options)) > 1) {
- $form['database'] = array(
- '#type' => 'select',
- '#title' => t('Database'),
- '#description' => t('Select the database key and target to use for storing indexing information in. ' .
- 'Cannot be changed after creation.'),
- '#options' => $options,
- '#default_value' => 'default:default',
- '#required' => TRUE,
- );
- }
- else {
- $form['database'] = array(
- '#type' => 'value',
- '#value' => "$key:$target",
- );
- }
- $form['min_chars'] = array(
- '#type' => 'select',
- '#title' => t('Minimum word length'),
- '#description' => t('The minimum number of characters a word must consist of to be indexed.'),
- '#options' => drupal_map_assoc(array(1, 2, 3, 4, 5, 6)),
- '#default_value' => 1,
- );
- }
- else {
- $form = array(
- 'database' => array(
- '#type' => 'value',
- '#title' => t('Database'), // Slight hack for the "View server" page.
- '#value' => $this->options['database'],
- ),
- 'database_text' => array(
- '#type' => 'item',
- '#title' => t('Database'),
- '#markup' => check_plain(str_replace(':', ' > ', $this->options['database'])),
- ),
- 'min_chars' => array(
- '#type' => 'select',
- '#title' => t('Minimum word length'),
- '#description' => t('The minimum number of characters a word must consist of to be indexed.'),
- '#options' => drupal_map_assoc(array(1, 2, 3, 4, 5, 6)),
- '#default_value' => $this->options['min_chars'],
- ),
- );
- }
- return $form;
- }
- public function supportsFeature($feature) {
- return $feature == 'search_api_facets';
- }
- public function postUpdate() {
- return $this->server->options != $this->server->original->options;
- }
- public function preDelete() {
- if (empty($this->options['indexes'])) {
- return;
- }
- foreach ($this->options['indexes'] as $index) {
- foreach ($index as $field) {
- db_drop_table($field['table']);
- }
- }
- }
- public function addIndex(SearchApiIndex $index) {
- $this->options += array('indexes' => array());
- $indexes = &$this->options['indexes'];
- if (isset($indexes[$index->machine_name])) {
- // Easiest and safest method to ensure all of the index' data is properly re-added.
- $this->removeIndex($index);
- }
- if (empty($index->options['fields']) || !is_array($index->options['fields'])) {
- // No fields, no work.
- $indexes[$index->machine_name] = array();
- $this->server->save();
- return $this;
- }
- $prefix = 'search_api_db_' . $index->machine_name. '_';
- $indexes[$index->machine_name] = array();
- foreach ($index->getFields() as $name => $field) {
- $table = $this->findFreeTable($prefix, $name);
- $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'];
- }
- $this->server->save();
- }
- /**
- * Helper method for finding free table names for fields.
- *
- * MySQL 5.0 imposes a 64 characters length limit for table names, PostgreSQL
- * 8.3 only allows 63 characters. Therefore, always return a name at most 63
- * characters long.
- */
- protected function findFreeTable($prefix, $name) {
- // A DB prefix might further reduce the maximum length of the table name.
- $maxlen = 63;
- list($key, $target) = explode(':', $this->options['database'], 2);
- if ($db_prefix = Database::getConnection($target, $key)->tablePrefix()) {
- $maxlen -= drupal_strlen($db_prefix);
- }
- $base = $table = drupal_substr($prefix . drupal_strtolower(preg_replace('/[^a-z0-9]/i', '_', $name)), 0, $maxlen);
- $i = 0;
- while (db_table_exists($table)) {
- $suffix = '_' . ++$i;
- $table = drupal_substr($base, 0, $maxlen - drupal_strlen($suffix)) . $suffix;
- }
- return $table;
- }
- /**
- * Helper method for creating the table for a field.
- */
- 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 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(
- 'description' => 'The text of the indexed token.',
- 'type' => 'varchar',
- 'length' => 50,
- 'not null' => TRUE,
- );
- $table['fields']['score'] = array(
- 'description' => 'The score associated with this token.',
- 'type' => 'float',
- 'not null' => TRUE,
- );
- $table['primary key'] = array('item_id', 'word');
- $table['indexes']['word'] = array(array('word', 10));
- }
- else {
- $table['fields']['value'] = $this->sqlType($type);
- $table['fields']['value'] += array('description' => "The field's value for this item.");
- if ($type != $field['type']) {
- // This is a list type.
- $table['fields']['value']['not null'] = TRUE;
- $table['primary key'] = array('item_id', 'value');
- }
- else {
- $table['primary key'] = array('item_id');
- }
- $table['indexes']['value'] = $table['fields']['value'] == 'varchar' ? array(array('value', 10)) : array('value');
- }
- $set = $this->setDb();
- db_create_table($name, $table);
- if ($set) {
- $this->resetDb();
- }
- }
- protected function sqlType($type) {
- $type = search_api_extract_inner_type($type);
- switch ($type) {
- case 'string':
- case 'uri':
- return array('type' => 'varchar', 'length' => 255);
- case 'integer':
- case 'duration':
- case 'date': // 'datetime' sucks. This way, we just convert every input into a timestamp.
- return array('type' => 'int');
- case 'decimal':
- return array('type' => 'float');
- case 'boolean':
- return array('type' => 'int', 'size' => 'tiny');
- default:
- throw new SearchApiException(t('Unknown field type @type. Database search module might be out of sync with Search API.', array('@type' => $type)));
- }
- }
- public function fieldsUpdated(SearchApiIndex $index) {
- $fields = &$this->options['indexes'][$index->machine_name];
- $new_fields = $index->getFields();
- $reindex = FALSE;
- $cleared = FALSE;
- $set = $this->setDb();
- foreach ($fields as $name => $field) {
- if (!isset($new_fields[$name])) {
- db_drop_table($field['table']);
- unset($fields[$name]);
- continue;
- }
- $old_type = $field['type'];
- $new_type = $new_fields[$name]['type'];
- $fields[$name]['type'] = $new_type;
- $fields[$name]['boost'] = $new_fields[$name]['boost'];
- $old_inner_type = search_api_extract_inner_type($old_type);
- $new_inner_type = search_api_extract_inner_type($new_type);
- if ($old_type != $new_type) {
- if ($old_inner_type == 'text' || $new_inner_type == 'text'
- || search_api_list_nesting_level($old_type) != search_api_list_nesting_level($new_type)) {
- // A change in fulltext or list status necessitates completely
- // clearing the index.
- $reindex = TRUE;
- if (!$cleared) {
- $cleared = TRUE;
- $this->deleteItems('all', $index);
- }
- db_drop_table($field['table']);
- $this->createFieldTable($index, $new_fields[$name], $field['table']);
- }
- elseif ($this->sqlType($old_inner_type) != $this->sqlType($new_inner_type)) {
- // There is a change in SQL type. We don't have to clear the index, since types can be converted.
- db_change_field($field['table'], 'value', 'value', $this->sqlType($new_type) + array('description' => "The field's value for this item."));
- $reindex = TRUE;
- }
- elseif ($old_inner_type == 'date' || $new_inner_type == 'date') {
- // Even though the SQL type stays the same, we have to reindex since conversion rules change.
- $reindex = TRUE;
- }
- }
- elseif (!$reindex && $new_inner_type == 'text' && $field['boost'] != $new_fields[$name]['boost']) {
- $multiplier = $new_fields[$name]['boost'] / $field['boost'];
- db_update($field['table'], $this->query_options)
- ->expression('score', 'score * :mult', array(':mult' => $multiplier))
- ->execute();
- }
- unset($new_fields[$name]);
- }
- $prefix = 'search_api_db_' . $index->machine_name. '_';
- // These are new fields that were previously not indexed.
- foreach ($new_fields as $name => $field) {
- $reindex = TRUE;
- $table = $this->findFreeTable($prefix, $name);
- $this->createFieldTable($index, $field, $table);
- $fields[$name]['table'] = $table;
- $fields[$name]['type'] = $field['type'];
- $fields[$name]['boost'] = $field['boost'];
- }
- if ($set) {
- $this->resetDb();
- }
- $this->server->save();
- return $reindex;
- }
- public function removeIndex($index) {
- $id = is_object($index) ? $index->machine_name : $index;
- if (!isset($this->options['indexes'][$id])) {
- return;
- }
- $set = $this->setDb();
- foreach ($this->options['indexes'][$id] as $field) {
- db_drop_table($field['table']);
- }
- if ($set) {
- $this->resetDb();
- }
- unset($this->options['indexes'][$id]);
- $this->server->save();
- }
- public function indexItems(SearchApiIndex $index, array $items) {
- if (empty($this->options['indexes'][$index->machine_name])) {
- throw new SearchApiException(t('No field settings for index with id @id.', array('@id' => $index->machine_name)));
- }
- $indexed = array();
- $set = $this->setDb();
- foreach ($items as $id => $item) {
- try {
- if ($this->indexItem($index, $id, $item)) {
- $indexed[] = $id;
- }
- }
- catch (Exception $e) {
- // We just log the error, hoping we can index the other items.
- watchdog('search_api_db', check_plain($e->getMessage()), NULL, WATCHDOG_WARNING);
- }
- }
- if ($set) {
- $this->resetDb();
- }
- return $indexed;
- }
- protected function indexItem(SearchApiIndex $index, $id, array $item) {
- $fields = $this->options['indexes'][$index->machine_name];
- $txn = db_transaction('search_api_indexing', $this->query_options);
- try {
- foreach ($item as $name => $field) {
- $table = $fields[$name]['table'];
- $boost = $fields[$name]['boost'];
- db_delete($table, $this->query_options)
- ->condition('item_id', $id)
- ->execute();
- // Don't index null values
- if($field['value'] === NULL) {
- continue;
- }
- $type = $field['type'];
- $value = $this->convert($field['value'], $type, $field['original_type'], $index);
- 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.
- $focus = min(1, .01 + 3.5 / (2 + count($words) * .015));
- $value = &$token['value'];
- if (is_numeric($value)) {
- $value = ltrim($value, '-0');
- }
- elseif (drupal_strlen($value) < $this->options['min_chars']) {
- continue;
- }
- $value = drupal_strtolower($value);
- $token['score'] *= $focus;
- if (!isset($words[$value])) {
- $words[$value] = $token;
- }
- else {
- $words[$value]['score'] += $token['score'];
- }
- }
- if ($words) {
- $query = db_insert($table, $this->query_options)
- ->fields(array('item_id', 'word', 'score'));
- foreach ($words as $word) {
- $query->values(array(
- 'item_id' => $id,
- 'word' => $word['value'],
- 'score' => $word['score'] * $boost,
- ));
- }
- $query->execute();
- }
- }
- elseif (search_api_is_list_type($type)) {
- $values = array();
- if (is_array($value)) {
- foreach ($value as $v) {
- if ($v !== NULL) {
- $values[$v] = TRUE;
- }
- }
- $values = array_keys($values);
- }
- else {
- $values[] = $value;
- }
- if ($values) {
- $insert = db_insert($table, $this->query_options)
- ->fields(array('item_id', 'value'));
- foreach ($values as $v) {
- $insert->values(array(
- 'item_id' => $id,
- 'value' => $v,
- ));
- }
- $insert->execute();
- }
- }
- elseif (isset($value)) {
- db_insert($table, $this->query_options)
- ->fields(array(
- 'item_id' => $id,
- 'value' => $value,
- ))
- ->execute();
- }
- }
- }
- catch (Exception $e) {
- $txn->rollback();
- throw $e;
- }
- return TRUE;
- }
- protected function convert($value, $type, $original_type, SearchApiIndex $index) {
- if (search_api_is_list_type($type)) {
- $type = substr($type, 5, -1);
- $original_type = search_api_extract_inner_type($original_type);
- $ret = array();
- if (is_array($value)) {
- foreach ($value as $v) {
- $v = $this->convert($v, $type, $original_type, $index);
- // Don't add NULL values to the return array. Also, adding an empty
- // array is, of course, a waste of time.
- if (isset($v) && $v !== array()) {
- $ret = array_merge($ret, is_array($v) ? $v : array($v));
- }
- }
- }
- return $ret;
- }
- if (!isset($value)) {
- // For text fields, we have to return an array even if the value is NULL.
- return search_api_is_text_type($type, array('text', 'tokens')) ? array() : NULL;
- }
- switch ($type) {
- case 'text':
- $ret = array();
- foreach (preg_split('/[^\p{L}\p{N}]+/u', $value, -1, PREG_SPLIT_NO_EMPTY) as $v) {
- if ($v) {
- $ret[] = array(
- 'value' => $v,
- 'score' => 1.0,
- );
- }
- }
- $value = $ret;
- // FALL-THROUGH!
- case 'tokens':
- while (TRUE) {
- foreach ($value as $i => $v) {
- // Check for over-long tokens.
- $score = $v['score'];
- $v = $v['value'];
- if (drupal_strlen($v) > 50) {
- $words = preg_split('/[^\p{L}\p{N}]+/u', $v, -1, PREG_SPLIT_NO_EMPTY);
- if (count($words) > 1 && max(array_map('drupal_strlen', $words)) <= 50) {
- // Overlong token is due to bad tokenizing.
- // Check for "Tokenizer" preprocessor on index.
- if (empty($index->options['processors']['search_api_tokenizer']['status'])) {
- watchdog('search_api_db', 'An overlong word (more than 50 characters) was encountered while indexing, due to bad tokenizing. ' .
- 'It is recommended to enable the "Tokenizer" preprocessor for indexes using database servers. ' .
- 'Otherwise, the service class has to use its own, fixed tokenizing.', array(), WATCHDOG_WARNING);
- }
- else {
- watchdog('search_api_db', 'An overlong word (more than 50 characters) was encountered while indexing, due to bad tokenizing. ' .
- 'Please check your settings for the "Tokenizer" preprocessor to ensure that data is tokenized correctly.',
- array(), WATCHDOG_WARNING);
- }
- }
- $tokens = array();
- foreach ($words as $word) {
- if (drupal_strlen($word) > 50) {
- watchdog('search_api_db', 'An overlong word (more than 50 characters) was encountered while indexing: %word.<br />' .
- 'Database search servers currently cannot index such words correctly – the word was therefore trimmed to the allowed length.',
- array('%word' => $word), WATCHDOG_WARNING);
- $word = drupal_substr($word, 0, 50);
- }
- $tokens[] = array(
- 'value' => $word,
- 'score' => $score,
- );
- }
- array_splice($value, $i, 1, $tokens);
- continue 2;
- }
- }
- break;
- }
- return $value;
- case 'string':
- case 'uri':
- // For non-dates, PHP can handle this well enough
- if ($original_type == 'date') {
- return date('%c', $value);
- }
- if (drupal_strlen($value) > 255) {
- throw new SearchApiException(t('A string value longer than 255 characters was encountered. ' .
- "Such values currently aren't supported by the database backend."));
- }
- return $value;
- case 'integer':
- case 'duration':
- case 'decimal':
- return 0 + $value;
- case 'boolean':
- return $value ? 1 : 0;
- case 'date':
- if (is_numeric($value) || !$value) {
- return 0 + $value;
- }
- return strtotime($value);
- default:
- throw new SearchApiException(t('Unknown field type @type. Database search module might be out of sync with Search API.', array('@type' => $type)));
- }
- }
- public function deleteItems($ids = 'all', SearchApiIndex $index = NULL) {
- if (!$index) {
- if (empty($this->options['indexes'])) {
- return;
- }
- $set = $this->setDb();
- foreach ($this->options['indexes'] as $index) {
- foreach ($index as $fields) {
- foreach ($fields as $field) {
- db_truncate($field['table'], $this->query_options)->execute();
- }
- }
- }
- if ($set) {
- $this->resetDb();
- }
- return;
- }
- if (empty($this->options['indexes'][$index->machine_name])) {
- return;
- }
- $set = $this->setDb();
- foreach ($this->options['indexes'][$index->machine_name] as $field) {
- if (is_array($ids)) {
- db_delete($field['table'], $this->query_options)
- ->condition('item_id', $ids, 'IN')
- ->execute();
- }
- else {
- db_truncate($field['table'], $this->query_options)->execute();
- }
- }
- if ($set) {
- $this->resetDb();
- }
- }
- public function search(SearchApiQueryInterface $query) {
- $time_method_called = microtime(TRUE);
- $set = $this->setDb();
- $index = $query->getIndex();
- if (empty($this->options['indexes'][$index->machine_name])) {
- throw new SearchApiException(t('Unknown index @id.', array('@id' => $index->machine_name)));
- }
- $fields = $this->options['indexes'][$index->machine_name];
- $keys = &$query->getKeys();
- $keys_set = (boolean) $keys;
- $keys = $this->prepareKeys($keys);
- if ($keys && !(is_array($keys) && count($keys) == 1)) {
- $fulltext_fields = $query->getFields();
- if ($fulltext_fields) {
- $_fulltext_fields = $fulltext_fields;
- $fulltext_fields = array();
- foreach ($_fulltext_fields as $name) {
- if (!isset($fields[$name])) {
- throw new SearchApiException(t('Unknown field @field specified as search target.', array('@field' => $name)));
- }
- if (!search_api_is_text_type($fields[$name]['type'])) {
- throw new SearchApiException(t('Cannot perform fulltext search on field @field of type @type.', array('@field' => $name, '@type' => $fields[$name]['type'])));
- }
- $fulltext_fields[$name] = $fields[$name];
- }
- $db_query = $this->createKeysQuery($keys, $fulltext_fields, $fields);
- if (is_array($keys) && !empty($keys['#negation'])) {
- $db_query->addExpression(':score', 'score', array(':score' => 1));
- }
- }
- else {
- $msg = t('Search keys are given but no fulltext fields are defined.');
- watchdog('search_api_db', $msg, NULL, WATCHDOG_WARNING);
- $this->warnings[$msg] = 1;
- }
- }
- elseif ($keys_set) {
- $msg = t('No valid search keys were present in the query.');
- $this->warnings[$msg] = 1;
- }
- if (!isset($db_query)) {
- $db_query = db_select($fields['search_api_language']['table'], 't', $this->query_options);
- $db_query->addField('t', 'item_id', 'item_id');
- $db_query->addExpression(':score', 'score', array(':score' => 1));
- }
- $filter = $query->getFilter();
- if ($filter->getFilters()) {
- $condition = $this->createFilterCondition($filter, $fields, $db_query);
- if ($condition) {
- $db_query->condition($condition);
- }
- }
- $db_query->addTag('search_api_db_search');
- $time_processing_done = microtime(TRUE);
- $results = array();
- $count_query = $db_query->countQuery();
- $results['result count'] = $count_query->execute()->fetchField();
- if ($results['result count']) {
- if ($query->getOption('search_api_facets')) {
- $results['search_api_facets'] = $this->getFacets($query, clone $db_query);
- }
- $query_options = $query->getOptions();
- if (isset($query_options['offset']) || isset($query_options['limit'])) {
- $offset = isset($query_options['offset']) ? $query_options['offset'] : 0;
- $limit = isset($query_options['limit']) ? $query_options['limit'] : 1000000;
- $db_query->range($offset, $limit);
- }
- $sort = $query->getSort();
- if ($sort) {
- foreach ($sort as $field_name => $order) {
- if ($order != 'ASC' && $order != 'DESC') {
- $msg = t('Unknown sort order @order. Assuming "ASC".', array('@order' => $order));
- $this->warnings[$msg] = $msg;
- $order = 'ASC';
- }
- if ($field_name == 'search_api_relevance') {
- $db_query->orderBy('score', $order);
- continue;
- }
- if ($field_name == 'search_api_id') {
- $db_query->orderBy('item_id', $order);
- continue;
- }
- if (!isset($fields[$field_name])) {
- throw new SearchApiException(t('Trying to sort on unknown field @field.', array('@field' => $field_name)));
- }
- $field = $fields[$field_name];
- if (search_api_is_list_type($field['type'])) {
- throw new SearchApiException(t('Cannot sort on field @field of a list type.', array('@field' => $field_name)));
- }
- if (search_api_is_text_type($field['type'])) {
- throw new SearchApiException(t('Cannot sort on fulltext field @field.', array('@field' => $field_name)));
- }
- $alias = $this->getTableAlias($field, $db_query);
- $db_query->orderBy($alias . '.value', $order);
- }
- }
- else {
- $db_query->orderBy('score', 'DESC');
- }
- $result = $db_query->execute();
- $time_queries_done = microtime(TRUE);
- foreach ($result as $row) {
- $results['results'][$row->item_id] = array(
- 'id' => $row->item_id,
- 'score' => $row->score,
- );
- }
- }
- else {
- $time_queries_done = microtime(TRUE);
- $results['results'] = array();
- }
- $results['warnings'] = array_keys($this->warnings);
- $results['ignored'] = array_keys($this->ignored);
- if ($set) {
- $this->resetDb();
- }
- $time_end = microtime(TRUE);
- $results['performance'] = array(
- 'complete' => $time_end - $time_method_called,
- 'preprocessing' => $time_processing_done - $time_method_called,
- 'execution' => $time_queries_done - $time_processing_done,
- 'postprocessing' => $time_end - $time_queries_done,
- );
- return $results;
- }
- /**
- * Helper method for removing unnecessary nested expressions from keys.
- */
- protected function prepareKeys($keys) {
- if (is_scalar($keys)) {
- $keys = $this->splitKeys($keys);
- return is_array($keys) ? $this->eliminateDuplicates($keys) : $keys;
- }
- elseif (!$keys) {
- return NULL;
- }
- $keys = $this->eliminateDuplicates($this->splitKeys($keys));
- $conj = $keys['#conjunction'];
- $neg = !empty($keys['#negation']);
- foreach ($keys as $i => &$nested) {
- if (is_array($nested)) {
- $nested = $this->prepareKeys($nested);
- if ($neg == !empty($nested['#negation'])) {
- if ($nested['#conjunction'] == $conj) {
- unset($nested['#conjunction'], $nested['#negation']);
- foreach ($nested as $renested) {
- $keys[] = $renested;
- }
- unset($keys[$i]);
- }
- }
- }
- }
- $keys = array_filter($keys);
- if (($count = count($keys)) <= 2) {
- if ($count < 2 || isset($keys['#negation'])) {
- $keys = NULL;
- }
- else {
- unset($keys['#conjunction']);
- $keys = array_shift($keys);
- }
- }
- return $keys;
- }
- /**
- * Helper method for splitting keys.
- */
- protected function splitKeys($keys) {
- if (is_scalar($keys)) {
- $proc = drupal_strtolower(trim($keys));
- if (is_numeric($proc)) {
- return ltrim($proc, '-0');
- }
- elseif (drupal_strlen($proc) < $this->options['min_chars']) {
- $this->ignored[$keys] = 1;
- return NULL;
- }
- $words = preg_split('/[^\p{L}\p{N}]+/u', $proc, -1, PREG_SPLIT_NO_EMPTY);
- if (count($words) > 1) {
- $proc = $this->splitKeys($words);
- $proc['#conjunction'] = 'AND';
- }
- return $proc;
- }
- foreach ($keys as $i => $key) {
- if (element_child($i)) {
- $keys[$i] = $this->splitKeys($key);
- }
- }
- return array_filter($keys);
- }
- /**
- * Helper method for eliminating duplicates from the search keys.
- */
- protected function eliminateDuplicates($keys, &$words = array()) {
- foreach ($keys as $i => $word) {
- if (!element_child($i)) {
- continue;
- }
- if (is_scalar($word)) {
- if (isset($words[$word])) {
- unset($keys[$i]);
- }
- else {
- $words[$word] = TRUE;
- }
- }
- else {
- $keys[$i] = $this->eliminateDuplicates($word, $words);
- }
- }
- return $keys;
- }
- /**
- * Helper method for creating a SELECT query for given search keys.
- *
- * @return SelectQueryInterface
- * A SELECT query returning item_id and score (or only item_id, if
- * $keys['#negation'] is set).
- */
- protected function createKeysQuery($keys, array $fields, array $all_fields) {
- if (!is_array($keys)) {
- $keys = array(
- '#conjunction' => 'AND',
- $keys,
- );
- }
- $or = db_or();
- $neg = !empty($keys['#negation']);
- $conj = $keys['#conjunction'];
- $words = array();
- $nested = array();
- $negated = array();
- $db_query = NULL;
- $mul_words = FALSE;
- $not_nested = FALSE;
- foreach ($keys as $i => $key) {
- if (!element_child($i)) {
- continue;
- }
- if (is_scalar($key)) {
- $words[] = $key;
- }
- elseif (empty($key['#negation'])) {
- if ($neg) {
- // If this query is negated, we also only need item_ids from
- // subqueries.
- $key['#negation'] = TRUE;
- }
- $nested[] = $key;
- }
- else {
- $negated[] = $key;
- }
- }
- $subs = count($words) + count($nested);
- $not_nested = ($subs <= 1 && count($fields) == 1) || ($neg && $conj == 'OR' && !$negated);
- if ($words) {
- if (count($words) > 1) {
- $mul_words = TRUE;
- foreach ($words as $word) {
- $or->condition('word', $word);
- }
- }
- else {
- $word = array_shift($words);
- }
- foreach ($fields as $name => $field) {
- $table = $field['table'];
- $query = db_select($table, 't', $this->query_options);
- if ($neg) {
- $query->fields('t', array('item_id'));
- }
- elseif ($not_nested) {
- $query->fields('t', array('item_id', 'score'));
- }
- else {
- $query->fields('t');
- }
- if ($mul_words) {
- $query->condition($or);
- }
- else {
- $query->condition('word', $word);
- }
- if (!isset($db_query)) {
- $db_query = $query;
- }
- elseif ($not_nested) {
- $db_query->union($query, 'UNION');
- }
- else {
- $db_query->union($query, 'UNION ALL');
- }
- }
- }
- if ($nested) {
- $word = '';
- foreach ($nested as $k) {
- $query = $this->createKeysQuery($k, $fields, $all_fields);
- if (!$neg) {
- $word .= ' ';
- $var = ':word' . strlen($word);
- $query->addExpression($var, 'word', array($var => $word));
- }
- if (!isset($db_query)) {
- $db_query = $query;
- }
- elseif ($not_nested) {
- $db_query->union($query, 'UNION');
- }
- else {
- $db_query->union($query, 'UNION ALL');
- }
- }
- }
- if (isset($db_query) && !$not_nested) {
- $db_query = db_select($db_query, 't', $this->query_options);
- $db_query->addField('t', 'item_id', 'item_id');
- if (!$neg) {
- $db_query->addExpression('SUM(t.score)', 'score');
- $db_query->groupBy('t.item_id');
- }
- if ($conj == 'AND' && $subs > 1) {
- $var = ':subs' . ((int) $subs);
- if (!$db_query->getGroupBy()) {
- $db_query->groupBy('t.item_id');
- }
- if ($mul_words) {
- $db_query->having('COUNT(DISTINCT t.word) >= ' . $var, array($var => $subs));
- }
- else {
- $db_query->having('COUNT(DISTINCT t.word) >= ' . $var, array($var => $subs));
- }
- }
- }
- if ($negated) {
- if (!isset($db_query) || $conj == 'OR') {
- if (isset($all_fields['search_api_language'])) {
- // We use this table because all items should be contained exactly once.
- $table = $all_fields['search_api_language']['table'];
- }
- else {
- $distinct = TRUE;
- foreach ($all_fields as $field) {
- $table = $field['table'];
- if (!search_api_is_list_type($field['type']) && !search_api_is_text_type($field['type'])) {
- unset($distinct);
- break;
- }
- }
- }
- if (isset($db_query)) {
- // We are in a rather bizarre case where the keys are something like "a OR (NOT b)".
- $old_query = $db_query;
- }
- $db_query = db_select($table, 't', $this->query_options);
- $db_query->addField('t', 'item_id', 'item_id');
- if (!$neg) {
- $db_query->addExpression(':score', 'score', array(':score' => 1));
- }
- if (isset($distinct)) {
- $db_query->distinct();
- }
- }
- if ($conj == 'AND') {
- foreach ($negated as $k) {
- $db_query->condition('t.item_id', $this->createKeysQuery($k, $fields, $all_fields), 'NOT IN');
- }
- }
- else {
- $or = db_or();
- foreach ($negated as $k) {
- $or->condition('t.item_id', $this->createKeysQuery($k, $fields, $all_fields), 'NOT IN');
- }
- if (isset($old_query)) {
- $or->condition('t.item_id', $old_query, 'NOT IN');
- }
- $db_query->condition($or);
- }
- }
- return $db_query;
- }
- /**
- * Helper method for finding any needed table for a filter query.
- */
- protected function findTable(array $filters, array $fields) {
- foreach ($filters as $filter) {
- if (is_array($filter)) {
- return $fields[$filter[0]]['table'];
- }
- }
- foreach ($filters as $filter) {
- if (is_object($filter)) {
- $ret = $this->findTable($filter->getFilters(), $fields);
- if ($ret) {
- return $ret;
- }
- }
- }
- }
- /**
- * Helper method for creating a condition for filtering search results.
- *
- * @return QueryConditionInterface
- */
- protected function createFilterCondition(SearchApiQueryFilterInterface $filter, array $fields, SelectQueryInterface $db_query) {
- $cond = db_condition($filter->getConjunction());
- $empty = TRUE;
- foreach ($filter->getFilters() as $f) {
- if (is_object($f)) {
- $c = $this->createFilterCondition($f, $fields, $db_query);
- if ($c) {
- $empty = FALSE;
- $cond->condition($c);
- }
- }
- else {
- $empty = FALSE;
- if (!isset($fields[$f[0]])) {
- throw new SearchApiException(t('Unknown field in filter clause: @field.', array('@field' => $f[0])));
- }
- if ($f[1] === NULL) {
- $query = db_select($fields[$f[0]]['table'], 't')
- ->fields('t', array('item_id'));
- $cond->condition('t.item_id', $query, $f[2] == '<>' || $f[2] == '!=' ? 'IN' : 'NOT IN');
- continue;
- }
- if (search_api_is_text_type($fields[$f[0]]['type'])) {
- // #negation is set here purely because we don't want any score.
- $keys = array('#conjunction' => 'AND', '#negation' => TRUE, $f[1]);
- $keys = $this->prepareKeys($keys);
- $query = $this->createKeysQuery($keys, array($fields[$f[0]]), $fields);
- $cond->condition('t.item_id', $query, $f[2] == '<>' || $f[2] == '!=' ? 'NOT IN' : 'IN');
- }
- else {
- $alias = $this->getTableAlias($fields[$f[0]], $db_query, search_api_is_list_type($fields[$f[0]]['type']));
- $cond->condition($alias . '.value', $f[1], $f[2]);
- }
- }
- }
- return $empty ? NULL : $cond;
- }
- /**
- * Helper method for adding a field's table to a database query.
- *
- * @param array $field
- * The field information array. The "table" key should contain the table
- * name to which a join should be made.
- * @param SelectQueryInterface $db_query
- * The database query used.
- * @param $newjoin
- * If TRUE, a join is done even if the table was already joined to in the
- * query.
- */
- protected function getTableAlias(array $field, SelectQueryInterface $db_query, $newjoin = FALSE) {
- if(!$newjoin) {
- foreach ($db_query->getTables() as $alias => $info) {
- $table = $info['table'];
- if (is_scalar($table) && $table == $field['table']) {
- return $alias;
- }
- }
- }
- return $db_query->join($field['table'], 't', 't.item_id = %alias.item_id');
- }
- /**
- * Helper method for getting the facet values for a query.
- */
- protected function getFacets(SearchApiQueryInterface $query, SelectQueryInterface $db_query) {
- // We only need the id field, not the score.
- $fields = &$db_query->getFields();
- unset($fields['score']);
- if (count($fields) != 1 || !isset($fields['item_id'])) {
- $this->warnings[] = t('Error while adding facets: only "item_id" field should be used, used are: @fields.',
- array('@fields' => implode(', ', array_keys($fields))));
- return array();
- }
- $expressions = &$db_query->getExpressions();
- $expressions = array();
- $group_by = &$db_query->getGroupBy();
- $group_by = array();
- $db_query->distinct();
- if (!$db_query->preExecute()) {
- return array();
- }
- $args = $db_query->getArguments();
- $table = db_query_temporary((string) $db_query, $args, $this->query_options);
- $fields = $this->options['indexes'][$query->getIndex()->machine_name];
- $ret = array();
- foreach ($query->getOption('search_api_facets') as $key => $facet) {
- if (empty($fields[$facet['field']])) {
- $this->warnings[] = t('Unknown facet field @field.', array('@field' => $facet['field']));
- continue;
- }
- $field = $fields[$facet['field']];
- $missing_count = 0;
- $select = db_select($table, 't');
- $alias = $this->getTableAlias($field, $select, TRUE);
- $select->addField($alias, search_api_is_text_type($field['type']) ? 'word' : 'value', 'value');
- $select->addExpression('COUNT(DISTINCT t.item_id)', 'num');
- $select->groupBy('value');
- $select->orderBy('num', 'DESC');
- $limit = $facet['limit'];
- if ((int) $limit > 0) {
- $select->range(0, $limit);
- }
- if ($facet['min_count'] > 1) {
- $select->having('num >= :count', array(':count' => $facet['min_count']));
- }
- if (((bool) $facet['missing']) === search_api_is_list_type($field['type'])) {
- // For multi-valued fields, the "missing" facet is defined by the table
- // not having any entries for those items. We therefore need to execute
- // an additional query, counting the items which do not have any entries
- // in the field table.
- if ($facet['missing']) {
- $inner_query = db_select($field['table'], 't1')
- ->fields('t1', array('item_id'));
- $missing_count = db_select($table, 't');
- $missing_count->addExpression('COUNT(item_id)');
- $missing_count->condition('item_id', $inner_query, 'NOT IN');
- $missing_count = $missing_count->execute()->fetchField();
- $missing_count = $missing_count >= $facet['min_count'] ? $missing_count : 0;
- }
- // For single-valued fields, there are explicit NULL values in the
- // table for "missing" entries. Therefore, we have to exclude NULL
- // values if we do not want a "missing" facet.
- else {
- $select->isNotNull('value');
- }
- }
- $terms = array();
- foreach ($select->execute() as $row) {
- if ($missing_count && $missing_count > $row->num) {
- $terms[] = array(
- 'count' => $missing_count,
- 'filter' => '!',
- );
- $missing_count = 0;
- if ($limit && count($terms) == $limit) {
- break;
- }
- }
- $terms[] = array(
- 'count' => $row->num,
- 'filter' => isset($row->value) ? '"' . $row->value . '"' : '!',
- );
- }
- if ($missing_count && (!$limit || count($terms) < $limit)) {
- $terms[] = array(
- 'count' => $missing_count,
- 'filter' => '!',
- );
- }
- $ret[$key] = $terms;
- }
- return $ret;
- }
- /**
- * Helper method for setting the database to the one selected by the user.
- */
- protected function setDb() {
- if (!isset($this->previous_db)) {
- list($key, $target) = explode(':', $this->options['database'], 2);
- $this->previous_db = db_set_active($key);
- if (!isset($this->query_options)) {
- $this->query_options = array('target' => $target);
- }
- return TRUE;
- }
- return FALSE;
- }
- /**
- * Helper method for resetting the original database.
- */
- protected function resetDb() {
- if (isset($this->previous_db)) {
- db_set_active($this->previous_db);
- $this->previous_db = NULL;
- return TRUE;
- }
- return FALSE;
- }
- }
|