by Jeff Eaton on November 26, 2007 // Short URL

Hiding content from Drupal's search system

Drupal offers a variety of ways to integrate with the built-in search system, from connecting with third-party search systems to adding information to standard node content. In addition, Drupal's search system respects the access permissions on each piece of content -- users who can't access a particular node will never see it in the results of their searches.

What happens, though, if you want users to be able to access some content (user bio nodes, for example) if they navigate to it directly, but don't want that content to appear in search results? Drupal doesn't offer any way to do that by default, but your custom module can use the same behind-the-scenes hooks used by the security system to control exactly what search results are presented to users.

The magic happens in hook_db_rewrite_sql(). Whenever Drupal code calls the db_rewrite_sql() function to pull information from the database, other modules can use hook_db_rewrite_sql() to intercept the SQL call and add additional filters to the query. That's how modules like Organic Groups restrict access to content based on group membership: when queries pull information from the node table, it compares the groups the node is associated with to the groups the current user belongs to.

Intercepting the queries used by the search system takes a bit of extra work, though. We'll take a look at some example code and see how the funky bits work.

function your_module_db_rewrite_sql($query, $primary_table, $primary_field, $args) {
  if ($query == '' && $primary_table == 'n' && $primary_field == 'nid' && empty($args)) {
    $excluded_types = variable_get('your_module_types', array());
    if (!empty($excluded_types)) {
      $where = " n.type NOT IN ('". join("','", $excluded_types) ."') ";
      return array('where' => $where);
    }
  }
}

Modules that implement hook_db_rewrite_sql() receive a couple important pieces of information about each query. The most important is the 'primary table' parameter -- you don't want to add a SQL WHERE filter intended for nodes when the primary table is 'user', for example. Due to some curious code inside the node module, however, the query that's passed in is treated as empty. While that's a pretty big violation of Drupal's own coding standards, it makes it easy to intercept just the node search queries.

So, we first check to see whether the incoming query is empty, the table is 'n', and the primary field is 'nid'. If those conditions are matched, the rest is easy: we grab a list of node types that we want to hide from the search results and build a WHERE condition that hides them. That's it!

To make things a bit cleaner, we can add some configuration options.

function your_module_search($op = 'search') {
  if ('admin' == $op) {
     $form = array();
     $form['your_module_types'] = array(
       '#type'           => 'select',
       '#multiple'       => TRUE,
       '#title'          => t('Exclude Node Types'),
       '#default_value'  => variable_get('your_module_types', array()),
       '#options'        => node_get_types('names'),
       '#size'           => 9,
       '#description'    => t('Node types to exclude from search results.'),
     );
     return $form;
  }
}

function your_module_form_alter($form_id, &$form) {
  if ('search_form' == $form_id) {
    $excluded_types = variable_get('your_module_types', array());
    $types = array_map('check_plain', node_get_types('names'));
    foreach($excluded_types as $excluded_type) {
      unset($types[$excluded_type]);
    }
    $form['advanced']['type']['#options'] = $types;
  }
}

What do the two code snippets above do? The first one -- hook_search() -- adds an extra form field to the search administrative settings page. It allows site admins to choose which kinds of content should be hidden. The original snippet we used to do the SQL rewrite will use that list of types to build its WHERE query.

The second snippet alters the advanced form that users see when they search for content on your site. Normally, it shows a list of all the site's content types and lets users choose what they want to see. This implementation of hook_form_alter(), though, removes options from that list if the admin has listed the content types as hidden. That ensures that users will never see options that are impossible to use.

That's it! The same technique can be used to hide content from specific users, content posted on Wednesdays, or any other criteria that's needed.

Jeff Eaton

Senior Digital Strategist

Want Jeff Eaton to speak at your event? Contact us with the details and we’ll be in touch soon.

Comments

Grugnog2

Views_fastsearch

An alternative to this which is probably simpler in a lot of cases (although not quite as cunning!) is to use views_fastsearch module. This is faster than the regular search, and allows you to add all the regular views cleverness, like node type filters and so on. You can use exposed filters in place of advanced search too (and arguments for 'section' searches).

That said, I can't wait for the day when we have views2 in core, and a hoo_query_alter that works with a nice, self documenting query array (or object!)...thenthe 'query altering' approach will become much easier and insanely powerful!

Reply

canen

Search config

The search config module allows you to do this by removing content types from the search index. Using hook_db_rewrite_sql() seems like a nice option too.

Reply

eaton

Your idea originally!

The genesis of the article was the feature in node_search_exclude you wrote (I'm sure you recognized it from the discussion we had the other week). While the discussion we had made it apparent that it had to be solved via nodeapi on the client's site, the SQL rewrite solution seemed like it was worth exploring...

Reply

Andy

Brilliant

We've had this issue in our site's queue for a few months, and we just hadn't gotten around to looking into it. Thanks to Jeff for the insight into node search queries, and thanks to canen for pointing out the search_config module. Problem solved in 5 minutes!

Reply

Andrew Berry

I wrote a module to do this

It does it in a different way, but seems to be working for me and a few others. I need to rename the module, but other than that it's pretty stable.

--Andrew

Reply

Anonymous

hiding search from your content

great site which is much appreciated.

I missed the search at the top right as there is some kind of javascript? drop down to display the search box. Not sure why you use this as it doesn't save any screen real estate and it just makes it confusing for people coming to your site. Just an idea.

Regards

Andy
Edinburgh

Reply

cam8001

search_block

I've been using search_block on healthnews.com and it's worked very well. It's simple to manage and doesn't seem to break (touch wood).

Reply

Ariane

Hey, thanks for this--I was

Hey, thanks for this--I was so thrilled when a lullabot post came up in my search for "drupal search access control content" exactly what I needed to know :-)

Reply

mykle

call a hack a hack!

this fix may work, but it is weird voodoo that makes no sense. blank queries? nonexistent tables? no thanks, i'd rather hack core and find a solution that's legible.

what is my guarantee here that i'm only intercepting node search queries? how do i know that someday some other module won't want to use an unrelated query that gets rewritten here, causing a mysterious failure?

if we don't want a given node type to be searched, shouldn't we start with not indexing it?

Reply

dldege

Another idea

For us we didn't want this to be user controlled and wanted some content to never show up. A simple way is to delete rows from the search_index table in your cron handler (after the normal search cron handling).

delete from search_index where sid in (select nid from node where type in ('profile', 'folder', 'group', 'webform')

The ideal situation is to never have the hidden content in the index in the first place. I hope D7 has this built in as its a nice feature and a potential performance boost for the search.

Reply

Andy

Great article, thanks for making me think

I think this is a needed alternative to the popular approach (and probably most appropriate in most settings) of hosing the search index one way or another for content types that are to be non-searchable.

For example, I may have a content types that I want certain users to be able to search but not all users. Maybe because as an administrator I want content searching of all my content but I don't want anonymous users searching this content.

I can even seeing layers of permissions where different roles of users can search different sets of content types.

However, it just seems a little weird to rely on this:

Due to some curious code inside the node module, however, the query that's passed in is treated as empty. While that's a pretty big violation of Drupal's own coding standards, it makes it easy to intercept just the node search queries.

Do you happen to know if this is still the case in Drupal 6?

Is it just possible to use arg(0) == 'search' instead of $query == '' ?

I'll probably be playing around with this and figure it out but I wanted to share how useful your article was and really made me take a look at different options when considering content type search restrictions.

Reply

Anonymous

nothing for 6.x

annoying that nothing 6.x compatible yet to achieve this. I'm sure a lot of people need this functionality
Great article I should probably overcome my laziness and churn out a tiny module to exclude content types using this technique.
- bollywood fan

Reply

Mike M

Thank you and small correction

Thanks for a great and easy way to implement this! I did have to make one small correction of a typo in the if --

$primary_field = 'nid'

should be

$primary_field == 'nid'

Hope that helps someone else too!

Reply

Mack Hardy

Thanks!

Thanks Jeff - works great, works well with the section module, if you want to limit search results by section, but use the same index table

-M

Reply

Arnel Bornales

using drupal 6

this is my version in drupal 6, just replace modulename x with your own module
Hope this will help you guys,

<?php
function x_db_rewrite_sql($query, $primary_table, $primary_field, $args) {
  if (
$query == '' && $primary_table == 'n' && $primary_field = 'nid' && empty($args)) {
   
$excluded_types = variable_get('your_module_types', array());
    if (!empty(
$excluded_types)) {
     
$where = " n.type NOT IN ('". join("','", $excluded_types) ."') ";
      return array(
'where' => $where);
    }
  }
}
function
x_search($op = 'search') {
  if (
'admin' == $op) {
    
$form = array();
    
$form['your_module_types'] = array(
      
'#type'           => 'select',
      
'#multiple'       => TRUE,
      
'#title'          => t('Exclude Node Types'),
      
'#default_value'  => variable_get('your_module_types', array()),
      
'#options'        => node_get_types('names'),
      
'#size'           => 9,
      
'#description'    => t('Node types to exclude from search results.'),
     );
     return
$form;
  }
}
function
x_form_alterform_alter(&$form, $form_state, $form_id){
   switch (
$form_id) {
    case
'search_form':
     
$excluded_types = variable_get('your_module_types', array());
     
$types = array_map('check_plain', node_get_types('names'));
      foreach(
$excluded_types as $excluded_type) {
        unset(
$types[$excluded_type]);
      }
     
$form['advanced']['type']['#options'] = $types;
      break;
  }
}
?>
Reply

William Eaton

Modifications to the Drupal 6 version

Fantastic, great work to lullabot as well as Arnel Bornales for writing this, this saved me A LOT of time and all I had to do was copy and paste the code. Any thoughts of releasing this as an official module?

Just 2 things. You didnt change "=" to "==" on line 4 and also the function "x_form_alterform_alter" should be "x_form_alter"... typo :-p

Thanks
Will Eaton

Reply

Arnel Bornales

revision: using drupal 6

ok, here it is...sorry for the delayed repost ... XD

just replace module name x with your own module name

create a .info file, name it x.info since x is our module name

; $Id$
name = Restrict search by content types
description = Restrict search
core = 6.x
package = "x - custom modules"

now create another file x.module

<?php
function x_db_rewrite_sql($query, $primary_table, $primary_field, $args) {
if (
$query == '' && $primary_table == 'n' && $primary_field == 'nid' && empty($args)){
 
$excluded_types = variable_get('your_module_types', array());
    if (!empty(
$excluded_types)) {
     
$where = " n.type NOT IN ('". join("','", $excluded_types) ."') ";
      return array(
'where' => $where);
    }
  }
}
function
x_search($op = 'search') {
  if (
'admin' == $op) {
   
$form = array();
   
$form['your_module_types'] = array(
     
'#type' => 'select',
     
'#multiple' => TRUE,
     
'#title' => t('Exclude Node Types'),
     
'#default_value' => variable_get('your_module_types', array()),
     
'#options' => node_get_types('names'),
     
'#size' => 9,
     
'#description' => t('Node types to exclude from search results.'),
    );
    return
$form;
  }
}
function
x_form_alter(&$form, $form_state, $form_id){
  switch (
$form_id) {
    case
'search_form':
     
$excluded_types = variable_get('your_module_types', array());
     
$types = array_map('check_plain', node_get_types('names'));
     
     foreach(
$excluded_types as $excluded_type) {
       unset(
$types[$excluded_type]);
     }

    
$form['advanced']['type']['#options'] = $types;
     break;
  }
}
?>
Reply

ionz149

many thanks

This is really awesome guys! I was struggling with no D6 modules and here you go making it so easy (:

Reply

Anonymous

Atencion. Hay un modulo

Atencion. Hay un modulo search_block, acabo de descubrir, que destroza el cron en los sitios grandes, al parecer porque intenta procesar toda la BBDD de miles de nodos en el mismo cron. Así que el cron sale con "timeout".

Además, cuando estaba desinstalando, he visto, que tiene algunos problemas con limpieza, ya que había cosas muy viejas en las tablas (tipos de contenido que ya no existen, variables no borrados etc.)

No lo utiliceis.

El modulo propuesto en esta página, lo he probado, funciona bien y con cambios mínimos según necesidad puede reemplazar search_block con mucho éxito (yo, por ejemplo, necesitaba también controlar la visibilidad en búsqueda de nodos sueltos y por comodidad he conservado el mismo GUI que tenía search_block)

Además, con control sobre la consulta de los nodos "excluidos de búsqueda" obtenemos mucha flexibilidad

Reply

Anonymous

Oops, sorry for the spanish,

Oops, sorry for the spanish, guys. I wanted to say that dont use search_block, because cron times out, if your BBDD has a lot of nodes. It doesnt respect the node limit that is set in search parameters.

It is much more efficient escribir your own, like given above, adding to it whatever you think is necessary (I needed per node control)

Reply

Balikampung

Hide admin user (user1) in search results.

I am looking for the information on how to hide the user1 or superadmin in search results. i mean i do not want the super user to be displayed in any part of the website. Appreciate if any one know the way for this and guide me on how to achieve this.

Reply