Add Product Ratings Attribute to Shopify Products Index

I’m trying to display my product reviews in the search results. I’m using the Shopify plugin, but can’t figure out where I can add the ratings attribute to the rest of the product attributes. Any help would be appreciated.

1 Like

Any help here would be much appreciated.

This message will only apply if you’re using Shopify’s default review application.

While I’ve never used it and can’t confirm this works, it seems like the review information is stored in a metafield called spr.reviews.
You can, in the application, index this metafield from the Indexing tab.
Then you’ll need to edit the autocomplete product template and / or the instant search product templates to add this:

<div class="product-reviews">[[& meta.spr.reviews ]]</div>

You’ll find here the documentation which should help you get started with modifying our front-end code.

So I went ahead and tried it myself this time.
This meta field holds all of the information we need, but its content is not numbers, it’s the whole HTML field.


If you only want to display review summaries in the search results, it is definitely enough, you should simply be able to write a small JavaScript helper to convert this string value to an actual star rating that you’ll be able to display.


However, if you want to add a filter, that’s not useful, since we do not have an usable number, but instead a full string with HTML inside.

Let’s make a script that we’ll be able to run to change this. The idea here will be to have a script that we can run by hand (or even better, automatically), every day. This script’s role will be to get the content of the spr.reviews metafield, and extract its information in other metafields.
For this, we’ll use node.js (v9 for async / await support) and two libraries: shopify-api-node and cheerio (jQuery on the server).

Step 1: Let’s get API credentials

With it, you’ll get an API key and an app password.
This is what we’ll need in the next step.

Step 2: Make a script that can talk to your Shop using the API

package.json

{
  "name": "spr-import-metafields",
  "description": "Shopify Product Reviews Metafield Creator",
  "version": "1.0.0",
  "private": "true",
  "engines": {
    "node": "9.2.0"
  },
  "dependencies": {
    "shopify-api-node": "^2.10.0",
    "cheerio": "^1.0.0-rc.2"
  }
}

main.js

const Shopify = require('shopify-api-node');

(async () => {
  const shopify = new Shopify({
    shopName: 'algolia-test-matthieu',
    apiKey: process.env.SHOPIFY_APP_API_KEY,
    password: process.env.SHOPIFY_APP_PASSWORD
  });

  const products = await shopify.product.list({ limit: 250, page: 0 });
  console.log(products.map(({ id }) => id));
})();

process.on('unhandledRejection', reason => {
  console.error(reason);
});

Then call it with:

SHOPIFY_APP_API_KEY=xxx SHOPIFY_APP_PASSWORD=xxx node main.js

If everything runs fine, you should see a bunch of ids showing up.

Step 3: Let’s make actual work

main.js

const Shopify = require('shopify-api-node');
const cheerio = require('cheerio');

const METAFIELDS_CREATED = 5;
const CALLS_PER_SECOND = 2;

// To avoid rate limiting, we should often wait
function wait(delay) {
  return new Promise(resolve => setTimeout(resolve, delay));
}

// Split an array into chunks, needed for rate limiting
function chunks(array, chunkLength) {
  const res = [];
  for (let i = 0; i < array.length; i += chunkLength) {
    res.push(array.slice(i, i + chunkLength));
  }
  return res;
}

function importProducts({ shopify, products }) {
  return Promise.all(
    products.map(async ({ id }) => {
      // Get all metafields of this product
      const metafields = await shopify.metafield.list({
        limit: 250,
        metafield: { owner_resource: 'product', owner_id: id },
      });
      // Find the spr metafield
      const meta = metafields.find(
        ({ namespace, key }) => namespace === 'spr' && key === 'reviews'
      );
      // If we didn't find it, skip
      if (!meta) return;

      const $ = cheerio.load(meta.value);

      // Get values
      const bestRating = parseInt(
        $('meta[itemprop="bestRating"]').attr('content'),
        10
      );
      const worstRating = parseInt(
        $('meta[itemprop="worstRating"]').attr('content'),
        10
      );
      const reviewCount = parseInt(
        $('meta[itemprop="reviewCount"]').attr('content'),
        10
      );
      const ratingValue =
        Math.round(
          parseFloat($('meta[itemprop="ratingValue"]').attr('content')) * 100
        ) / 100;

      // With those values, let's create metafields
      // Update the constant METAFIELDS_CREATED if you change those
      await Promise.all([
        shopify.metafield.create({
          namespace: 'spr_import',
          value_type: 'integer',
          key: 'best_rating',
          value: bestRating,
          owner_resource: 'product',
          owner_id: id,
        }),
        shopify.metafield.create({
          namespace: 'spr_import',
          value_type: 'integer',
          key: 'worst_rating',
          value: worstRating,
          owner_resource: 'product',
          owner_id: id,
        }),
        shopify.metafield.create({
          namespace: 'spr_import',
          value_type: 'integer',
          key: 'review_count',
          value: reviewCount,
          owner_resource: 'product',
          owner_id: id,
        }),
        // Shopify doesn't support decimal metafields
        shopify.metafield.create({
          namespace: 'spr_import',
          value_type: 'integer',
          key: 'rating_integer',
          value: Math.round(ratingValue),
          owner_resource: 'product',
          owner_id: id,
        }),
        // This one can be converted to a number in the Algolia app
        shopify.metafield.create({
          namespace: 'spr_import',
          value_type: 'string',
          key: 'rating_decimal',
          value: ratingValue.toString(),
          owner_resource: 'product',
          owner_id: id,
        }),
      ]);
    })
  );
}

(async () => {
  const shopify = new Shopify({
    shopName: 'algolia-test-matthieu',
    apiKey: process.env.SHOPIFY_APP_API_KEY,
    password: process.env.SHOPIFY_APP_PASSWORD,
  });

  let productsImported = 0;
  let page = 0;
  let products = [];

  do {
    products = await shopify.product.list({ limit: 250, page });
    const productCount = await shopify.product.count();
    await wait(Math.round(1000 * 2 / CALLS_PER_SECOND));

    // Split the products in chunks to avoid hitting the rateLimit
    const callsPerProduct = METAFIELDS_CREATED + 1;
    const chunkLength = Math.floor(30 / callsPerProduct);
    const productChunks = chunks(products, chunkLength);

    for (let i = 0; i < productChunks.length; ++i) {
      const productChunk = productChunks[i];
      await importProducts({ shopify, products: productChunk });
      productsImported += productChunk.length;

      console.log(`Products imported : ${productsImported} / ${productCount}`);

      // Let's check the rateLimit and go back to 10
      const { remaining } = shopify.callLimits;
      const toWaitFor = Math.max(0, 30 - remaining);
      await wait(toWaitFor * Math.floor(1000 / CALLS_PER_SECOND));
    }

    page += 1;
  } while (products.length === 250);
})();

process.on('unhandledRejection', reason => {
  console.error(reason);
});

This script is going to take a long time to run. This is unfortunately the best we can do with Shopify’s rate limiting.
You might be able to have Shopify increase your rate limit to more than 2 calls per second on average. If you do, you can change the CALLS_PER_SECOND variable. Also, creating one metafield instead of five would make it run way faster, just remember to also change METAFIELDS_CREATED if you change this.

Step 4: Find somewhere to run it

As stated before, this will take a long time to run, so you might want to run it elsewhere than your machine. Heroku might be a good place, especially with their scheduler add-on set to run every 24 hours.
If you do, you might need to use node --optimize-for-size --max-old-space-size=420 --max-semi-space-size=16 to limit the amount of RAM the script takes, or Heroku might scream because we’ll end up creating a lot of objects, and node is sometimes not so smart in cleaning RAM it uses.

Step 5: Configure the app to use it

Now that you have metafields holding those values, to make use of it, you’ll need to:

  1. Add the metafield to the product metafields in the Indexing tab
  2. Wait for the reindexing to be over
  3. In the Search options tab, drag the newly appeared Metafield you want to use in the Facets list from Disabled to Enabled (and probably set it as a slider)
  4. Confirm that you see it in your search page.