5

I have a product catalog running on Search API and Search API SOLR Search. Now I was hoping that when a customer flags a product (with the flag module), there would be a way to boost the search score of that particular product. So that when it is flagged as favorite it will float to the top, but only for that specific user.

How to alter Search API search scores so flagged items will have a higher score?

4

You could have a view that shows flagged nodes by user and a view attachment for the index filtering out nodes that the user has flagged.

  • That would be more like two views on top of each other right? So theoretically the same product could appear in both Views. I'd like to prevent that if possible. – Neograph734 Feb 28 '16 at 9:48
  • Filtering out nodes the user has flagged would take care of that. Add a relationship and then in filter, do a negative on it. I used search api ages ago but I think it's possible. – Niall Murphy Feb 28 '16 at 11:12
  • That might work, upvote. But I'd still like to know if there is a better way so I'll keep the question for a little longer. – Neograph734 Feb 28 '16 at 11:33
2

TLDR; Use the Boost Query (bq) parameter

function mymodule_search_api_solr_query_alter(array &$call_args, SearchApiQueryInterface $query) {
  $call_args['params']['bq'][] = 'im_field_product$flag_favorite_user:"' . $uid . '"^10';
}

There are several things at play that made this question difficult to solve.

At first I was looking at hook_search_api_query_alter() which I didn't understand, only to find out later (thanks to LeDrupaliste) there is also a hook_search_api_solr_query_alter().

Then there was the challenge to sort the results based on the flag. I wanted to return all relevant content, but have favorite (flagged) products returned first. Mostly on the shop landing page (empty search query). So I needed to sort the results, giving the results with the matching flag and user higher priority. Too bad flag's integration with Search API exposes it as a multi-value field and SOLR is not capable to sort on mutli-value fields.

So in the end the client and I came to agree that we only display flagged products on the empty query page, which was implemented like this:

function mymodule_search_api_solr_query_alter(array &$call_args, SearchApiQueryInterface $query) {
  if (empty($call_args['query'])) {
    $call_args['query'] = 'im_field_product$flag_favorite_user:7^20 OR *:*';
  }
}

If the query is empty, query for the indexed flag field im_field_product$flag_favorite_user where the value matches the users uid 7 in this case and boost the score 20 times to ensure high relevance. To prevent the query from hiding all other results, also include all other non-empty fields *:*.

It is not yet exactly what I wanted, but it is very close. Now I only need to find a way to append this a regular search. Returning all items matching the keywords, but with the favorite onces on top.

$call_args['query'] = 'im_field_product$flag_favorite_user:7^20 OR ' . $call_args['query'];

Above returns the flagged items always first even if no field matches the rest of the query. Undesirable as it provides irrelevant search results. (i get a yellow product even though I search for a red one)

$call_args['query'] = 'im_field_product$flag_favorite_user:7^20 AND ' . $call_args['query'];

Above returns only results if the product is flagged and contains the correct query fields. No other products matching the query will be returned. (only red products that are marked favorite are returned)

$call_args['query'] = 'im_field_product$flag_favorite_user:7^20 ' . $call_args['query'];

Same as with AND.

I will update the answer once I've found a way to do this properly.

ps. I've found the field name through the SOLR web interface, but Search API SOLR probably provides it somehow.

UPDATE

In the end I solved it like this:

if (empty($call_args['query'])) {
  $call_args['query'] .= 'im_field_product$flag_favorite_user:' . $uid . '^9 OR *';
}
else {
  $call_args['query'] .= ' AND (im_field_product$flag_favorite_user:' . $uid . '^9 OR *)';
}

It adds a single filter to the query if no query exists, showing all products, but giving priority to the flagged ones. If there is an existing query, we use the AND clause to again select all products and give priority to the flagged ones, but also match it against the existing query so it honors the provided search terms. It is important to match everything (*) in the AND clause otherwise only flagged products are shown.

UPDATE2

I just discovered the Boost Query parameter.

The bq parameter specifies an additional, optional, query clause that will be added to the user's main query to influence the score.

So the code has been updated to:

function mymodule_search_api_solr_query_alter(array &$call_args, SearchApiQueryInterface $query) {
  global $user;
  $uid = $user->uid;

  $call_args['params']['bq'][] = 'im_field_product$flag_favorite_user:"' . $uid . '"^10';
}
1

Sounds to me like you would need to add this flag to the index. Given that bit of knowledge, it was only about 5 minutes to find this Drupal issue:

Search API: Flag module integration

Which does have a reasonable snippet of code that helps bring out to the index a count of "flags" per node.

<?php
/**
 * Get the flag count for a given node.
 */
function mymodule_get_count($entity, $options, $name, $entity_type, &$info) {
  // Requiring type node since we're relying on $entity->nid,
  // but this could be used for user objects too.
  if ($entity_type == 'node') {
    $query = db_select('flag_counts' ,'fc');
    $query->fields('fc', array('count'));
    $query->condition('fc.fid', $info['data']['flag']->fid);
    $query->condition('fc.content_type', 'node');
    $query->condition('fc.content_id', $entity->nid);
    $count = $query->execute()->fetchColumn();
  }
  return !empty($count) ? $count : 0;
}

/**
* Implements hook_entity_property_info_alter().
*/
function mymodule_entity_property_info_alter(&$info) {
  if (isset($info['node']['bundles'])) {
    // For each content type.
    foreach ($info['node']['bundles'] as $bundle_type => $bundle) {
      // Find all applicable flags for this content type.
      $flags = flag_get_flags('node', $bundle_type);
      // For each applicable flag.
      foreach ($flags as $fid => $flag) {
        $info['node']['bundles'][$bundle_type]['properties']['flag_' . $flag->name . '_count'] = array(
          'label' => t('@title Flag Count', array('@title' => $flag->title)),
          'description' => t('The total number of @title flags for this node.', array('@title' => $flag->title)),
          'type' => 'integer',
          'getter callback' => 'mymodule_get_count',
          'computed' => TRUE,
          'data' => array('flag' => $flag),
        );
      }
    }
  }
}
?>

But this isn't what you would need. You would need a module like commerce_search_api that provides a referenced entity index (commerce uses a product reference field, similar to the way flag references content entities on flag entities). So, your best bet is to:

  1. Start with the commerce_search_api module
  2. Modify the module for flag
  3. Then you should be able to add the user of the flag to your index

Note: I believe Flag is the reverse relationship which might pose an additional challenge to making a modified module do what you need to do.

  • The flag is already indexed. This example adds a newly created field containing the flag count to the index. It would have been really useful if I wanted to sort on total number of favorite flags, but I need to match it for the current user only. – Neograph734 Mar 3 '16 at 0:06
0

You can try hook_search_api_solr_search_results_alter (or hook_search_api_solr_multi_search_results_alter if you are doing multi index search)

I can't test for now if changing score here alters the results order but you can use your own sort function if necessary (with usort)

  • Thanks, but that seems to be giving problems with pagination (on Views). My View only returns a limited amount of products on the first page and I only get that amount of items in the $results array. So if a featured item is on the second (or other) page I would not be able to access it. – Neograph734 Feb 29 '16 at 12:25

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.