Похожие материалы в Друпал 7 с помощью представлений.

Для поисковой оптимизации важна правильная внутренняя перелинковка. Одним из логичных способов автоматизации внутренней перелинковки является блок "похожих" материалов, где показываются статьи, помеченные такими же тегами, как текущая. Для Друпала есть специальный модуль - Similar by Terms, решающий эту задачу, однако версия этого модуля для седьмого Друпала полна ошибок и недоработок, у меня не получилось её использовать в том виде, в каком она существует на данный момент. Я решил пойти моим любимым путём - использованием стандартных средств и широко распространённых модулей, а именно Views (этот модуль должен уже стоять на любом мало-мальски сложном сайте, сделанном на Друпале).

К сожалению, мои навыки настройки этого модуля ещё далеки от совершенства и у меня не получилось сходу скроить представление, удовлетворяющее моим потребностям. На моё счастье, Google уже давно служит заменой частям головного мозга, ответственным за изобретение велосипеда, и я довольно быстро нашёл готовые решения, основанные на фильтрах и связях. На первый взгляд, проблема была решена, однако при ближайшем рассмотрении оказалось, что "похожие" материалы сортировались случайным образом, а вовсе не по количеству общих терминов таксономии, как я ожидал. По моим представлениям о программировании в целом, для знающего специалиста отсортировать результаты в правильном порядке - дело пяти минут. Ну хорошо, не пяти минут, а получаса - пока туда-сюда подключиться, разобраться. Умножаем на два (базовое правило оценки сроков), получаем час работы. Когда я в последний раз работал программистом, день моей работы стоил примерно сто долларов, значит час - что-то около десяти. Десять долларов не жалко, открыл проект на freelance.ru, стоимость, на всякий случай, не указал. Бесполезная трата времени - пришли три желающих один другого краше.

Первый пишет:

Срок 5Дней, Готов выполнить
Работаем на совесть, любим длинные сроки. В конце подводим мы хорошие итоги! Не отказываемся от предоплат, Всегда мы в ожидании зарплат. Создать, сверстать нам не проблема. От сайт-визитки до 3D Max-а интерьера. Рисуем логопиты креативно. Выходит вроде бы все стильно!!!

Я долго пытался понять, что он предлагает, но оказалось, что корявые стихи - это не часть предложения, а подпись. Второй был краток:

Хорошо знаю drupal сделаю.

Срок исполнения лучше - "всего" два дня. Третий начал по-татарски и послал к себе на сайт:

Ассаламу алейкум,
у меня есть опыт работы с drupal, я могу выполнить ваше задание,
стоимость работы 133 руб/час, возможна платная оценка сроков и стоимости,
на сайте http://echo2012.us.to/ вы найдете исчерпывающую информацию,
для продолжения общения, пожалуйста, воспользуйтесь контактами на сайте,
Благодарю за внимание.

Что ж, такой подход имеет право на существование, однако при переходе на сайт начинает играть настолько ужасный музон, что его сразу хочется закрыть, что я благополучно и сделал, тем более, что платная оценка сроков и стоимости не входила в мой бюджет. Те исполнители, которые готовы были оценить стоимость работы, дружно называли цифру 3000 рублей - в десять раз больше запланированной.

Последняя надежда на коллективный разум - задаю вопрос на StackOverflow, отдавая часть своей репутации в качестве приза ответившему. Моя первоначальная оценка объёма работ в пять минут была довольно точна. Привожу компиляцию ответа:

  1. Создайте новое представление типа "Блок";
  2. Добавьте контекстный фильтр -> Content: Nid -> Provide default value -> Content ID from URL;
  3. Добавьте связь -> Content: Taxonomy terms on node -> Включите нужный словарь таксономии;
  4. Добавьте связь -> Taxonomy Term: Content using <Словарь таксономии из предыдущего шага> -> Отметьте галкой "Require this relationship"'
  5. Включите агрегацию представления (Вот этот шаг я упускал в своих попытках самостоятельно решить задачу);
  6. Под заголовком "Format" нажмите на "Show" и выберите "Fields";
  7. В поле, которое выводится представлением, включите использование связи из шага 4.
  8. Добавьте новый критерий сортировки Content: Nid. В настройках агрегирования выберите "Count". Используйте связь из шага 4 и отсортируйте по убыванию. Удалите существующую сортировку по времени, если таковая присутствует
  9. Добавьте контекстный фильтр -> Content: Nid -> Используйте связь из шага 4 -> Provide default value -> Content ID from URL -> Прокрутите вниз и разверните "More", там отметьте "Exclude" (это исключит текущий документ из списка "похожих")
  10. В разделе "Filter Criteria" нажмите на "Content: Published" и выберите связь из шага 4, этим вы настроите представления для вывода только опубликованных похожих нодов

Настройка представления похожих документов в Друпал 7

Вот полный код этого представления для "голого" Друпала:

$view = new view();
$view->name = 'similar_nodes';
$view->description = '';
$view->tag = 'default';
$view->base_table = 'node';
$view->human_name = 'Similar nodes';
$view->core = 7;
$view->api_version = '3.0';
$view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */

/* Display: Master */
$handler = $view->new_display('default', 'Master', 'default');
$handler->display->display_options['title'] = 'Similar nodes';
$handler->display->display_options['use_more_always'] = FALSE;
$handler->display->display_options['group_by'] = TRUE;
$handler->display->display_options['access']['type'] = 'perm';
$handler->display->display_options['cache']['type'] = 'none';
$handler->display->display_options['query']['type'] = 'views_query';
$handler->display->display_options['exposed_form']['type'] = 'basic';
$handler->display->display_options['pager']['type'] = 'full';
$handler->display->display_options['pager']['options']['items_per_page'] = '10';
$handler->display->display_options['style_plugin'] = 'default';
$handler->display->display_options['row_plugin'] = 'fields';
$handler->display->display_options['row_options']['inline'] = array(
  'title' => 'title',
);
/* Relationship: Content: Taxonomy terms on node */
$handler->display->display_options['relationships']['term_node_tid']['id'] = 'term_node_tid';
$handler->display->display_options['relationships']['term_node_tid']['table'] = 'node';
$handler->display->display_options['relationships']['term_node_tid']['field'] = 'term_node_tid';
$handler->display->display_options['relationships']['term_node_tid']['vocabularies'] = array(
  'tags' => 'tags',
  'forums' => 0,
);
/* Relationship: Taxonomy term: Content using Tags */
$handler->display->display_options['relationships']['reverse_field_tags_node']['id'] = 'reverse_field_tags_node';
$handler->display->display_options['relationships']['reverse_field_tags_node']['table'] = 'taxonomy_term_data';
$handler->display->display_options['relationships']['reverse_field_tags_node']['field'] = 'reverse_field_tags_node';
$handler->display->display_options['relationships']['reverse_field_tags_node']['relationship'] = 'term_node_tid';
$handler->display->display_options['relationships']['reverse_field_tags_node']['required'] = TRUE;
/* Field: Content: Title */
$handler->display->display_options['fields']['title']['id'] = 'title';
$handler->display->display_options['fields']['title']['table'] = 'node';
$handler->display->display_options['fields']['title']['field'] = 'title';
$handler->display->display_options['fields']['title']['relationship'] = 'reverse_field_tags_node';
$handler->display->display_options['fields']['title']['label'] = '';
$handler->display->display_options['fields']['title']['alter']['word_boundary'] = FALSE;
$handler->display->display_options['fields']['title']['alter']['ellipsis'] = FALSE;
$handler->display->display_options['fields']['title']['element_label_colon'] = FALSE;
/* Sort criterion: COUNT(Content: Nid) */
$handler->display->display_options['sorts']['nid']['id'] = 'nid';
$handler->display->display_options['sorts']['nid']['table'] = 'node';
$handler->display->display_options['sorts']['nid']['field'] = 'nid';
$handler->display->display_options['sorts']['nid']['relationship'] = 'reverse_field_tags_node';
$handler->display->display_options['sorts']['nid']['group_type'] = 'count';
$handler->display->display_options['sorts']['nid']['order'] = 'DESC';
/* Contextual filter: Content: Nid */
$handler->display->display_options['arguments']['nid']['id'] = 'nid';
$handler->display->display_options['arguments']['nid']['table'] = 'node';
$handler->display->display_options['arguments']['nid']['field'] = 'nid';
$handler->display->display_options['arguments']['nid']['default_action'] = 'default';
$handler->display->display_options['arguments']['nid']['default_argument_type'] = 'node';
$handler->display->display_options['arguments']['nid']['summary']['number_of_records'] = '0';
$handler->display->display_options['arguments']['nid']['summary']['format'] = 'default_summary';
$handler->display->display_options['arguments']['nid']['summary_options']['items_per_page'] = '25';
/* Contextual filter: Content: Nid */
$handler->display->display_options['arguments']['nid_1']['id'] = 'nid_1';
$handler->display->display_options['arguments']['nid_1']['table'] = 'node';
$handler->display->display_options['arguments']['nid_1']['field'] = 'nid';
$handler->display->display_options['arguments']['nid_1']['relationship'] = 'reverse_field_tags_node';
$handler->display->display_options['arguments']['nid_1']['default_action'] = 'default';
$handler->display->display_options['arguments']['nid_1']['default_argument_type'] = 'node';
$handler->display->display_options['arguments']['nid_1']['summary']['number_of_records'] = '0';
$handler->display->display_options['arguments']['nid_1']['summary']['format'] = 'default_summary';
$handler->display->display_options['arguments']['nid_1']['summary_options']['items_per_page'] = '25';
$handler->display->display_options['arguments']['nid_1']['not'] = TRUE;
/* Filter criterion: Content: Published */
$handler->display->display_options['filters']['status']['id'] = 'status';
$handler->display->display_options['filters']['status']['table'] = 'node';
$handler->display->display_options['filters']['status']['field'] = 'status';
$handler->display->display_options['filters']['status']['relationship'] = 'reverse_field_tags_node';
$handler->display->display_options['filters']['status']['value'] = '1';
$handler->display->display_options['filters']['status']['group'] = 1;
$handler->display->display_options['filters']['status']['expose']['operator'] = FALSE;

/* Display: Page */
$handler = $view->new_display('page', 'Page', 'page');
$handler->display->display_options['path'] = 'similar-nodes';

/* Display: Block */
$handler = $view->new_display('block', 'Block', 'block');
$handler->display->display_options['defaults']['pager'] = FALSE;
$handler->display->display_options['pager']['type'] = 'some';
$handler->display->display_options['pager']['options']['items_per_page'] = '5';
$handler->display->display_options['defaults']['style_plugin'] = FALSE;
$handler->display->display_options['style_plugin'] = 'default';
$handler->display->display_options['defaults']['style_options'] = FALSE;
$handler->display->display_options['defaults']['row_plugin'] = FALSE;
$handler->display->display_options['row_plugin'] = 'fields';
$handler->display->display_options['defaults']['row_options'] = FALSE;
$translatables['similar_nodes'] = array(
  t('Master'),
  t('Similar nodes'),
  t('more'),
  t('Apply'),
  t('Reset'),
  t('Sort by'),
  t('Asc'),
  t('Desc'),
  t('Items per page'),
  t('- All -'),
  t('Offset'),
  t('« first'),
  t('‹ previous'),
  t('next ›'),
  t('last »'),
  t('term'),
  t('field_tags'),
  t('All'),
  t('Page'),
  t('Block'),
);