Open selected-pixel-jameson opened 1 year ago
I am asking myself the same question. There is a quite blocking bug opened since a couple of months which makes it quite unsuable, right? https://github.com/MattieBelt/mattie-strapi-bundle/issues/139
Same! Algolia has their own JS client, though, with v5 just having entered public beta, so I'm going to go with that instead. Pity--the Strapi v3 version worked very nicely.
@andrew-braun could you please tell us more about how to use the Algolia JS client to index from strapi?
@krankos Sure! Here's what I did:
services
folder of the plugin I added my own file, algolia-indexing.js
lifecycles.js
folder for each content type I wanted to send to Algolia. I added afterCreate
, afterUpdate
, and afterDelete
hooks that call the appropriate Algolia functions and send the data over. Remember that for afterCreate
and afterUpdate
you'll also have to pass in a populate
object that tells the service what data to go fetch so it can be sent to the Algolia index. I put all my populate objects in a separate file and called them dynamically based on the index name so I could mostly just copy-paste the functions.It's still fairly rough since it's just meant for use in this one project (this can probably be improved quite a bit; it's my first time making a Strapi plugin), but here's the basic idea:
plugins/strapi-algolia/server/services.js
"use strict";
function algoliaClient() {
// Initiate the Algolia client
const { algoliasearch } = require("algoliasearch");
const client = algoliasearch(process.env.ALGOLIA_ID, process.env.ALGOLIA_KEY);
return client;
}
module.exports = ({ strapi }) => ({
async addOrReplace(event, options = { index: "", populate: "*" }) {
/* This function can be called from a Strapi lifecycle hook--afterCreate or afterUpdate, most likely
** Call it with syntax: strapi.service("plugin::strapi-algolia.index").addOrReplace(event, { index: "indexname", populate: {populateObject} })
*/
try {
const { model } = event;
const { where } = event.params;
const { index, populate } = options;
const entryId =
event.action === "afterUpdate" ? where?.id : event?.result?.id;
console.log(
`${event.action === "afterCreate" ? "Adding" : "Updating"} ${
model.singularName
} ${entryId} ${
event.action === "afterCreate" ? "to" : "in"
} Algolia index`
);
const client = algoliaClient();
/* Populate the object with all of its relations by default
** If you want to customize this, you can pass in a populate array
** on the options object
*/
const populatedObject = await strapi.entityService.findOne(
model.uid,
entryId,
{
populate: populate ?? "*",
}
);
// console.log(populatedObject);
const { taskID } = await client.saveObject({
indexName: index,
body: { objectID: entryId, ...populatedObject },
});
const status = await client.waitForTask({ indexName: index, taskID });
console.log(`Algolia indexing status: ${status.status}`);
} catch (error) {
console.error(`Error while updating Algolia index: ${error}`);
}
},
async delete(event, options) {
try {
const { index, many } = options;
const objectIDs = many
? event?.params?.where?.["$and"][0]?.id["$in"]
: [event.params.where.id];
const client = algoliaClient();
console.log(
`Deleting object(s) with id(s) ${objectIDs.join(
", "
)} from Algolia ${index} index`
);
// Use Algolia client.batch API to handle either one or many delete requests
const requests = objectIDs.map((objectID) => {
return {
objectID,
action: "deleteObject",
};
});
const { taskID } = await client.batch({
indexName: index,
batchWriteParams: { requests },
});
const response = await client.waitForTask({ indexName: index, taskID });
console.log(
`Successfully deleted object(s) with id(s) ${objectIDs.join(
", "
)} from Algolia ${index} index`
);
} catch (error) {
console.error(`Error while deleting from Algolia index: ${error}`);
}
},
/api/article/lifecycles.js
"use strict";
/**
* Read the documentation (https://strapi.io/documentation/developer-docs/latest/development/backend-customization.html#lifecycle-hooks)
* to customize this model
*/
const index = "articles";
const {
articlePopulate: populate,
} = require("../../../../lib/data/algolia-populate-data");
module.exports = {
async afterCreate(event) {
const response = await strapi
.service("plugin::strapi-algolia.index")
.addOrReplace(event, { index, populate: populate });
},
async afterUpdate(event) {
const response = await strapi
.service("plugin::strapi-algolia.index")
.addOrReplace(event, { index, populate: populate });
},
async afterDelete(event) {
const response = await strapi
.service("plugin::strapi-algolia.index")
.delete(event, { index, many: false });
},
async afterDeleteMany(event) {
const response = await strapi
.service("plugin::strapi-algolia.index")
.delete(event, { index, many: true });
},
};
/lib/data/algolia-populate-data.js
module.exports = {
articlePopulate: {
author: {
fields: ["authorName", "profession", "bio"],
},
story_category: {
fields: ["category_name"],
},
country: {
fields: ["country", "countryName"],
},
region: {
fields: ["region", "regionName"],
},
ContentZone: true,
},
}
I also added a separate thing for bulk-updating indexes via a POST request that tells Strapi to go collect the data from the specified content type and batch update it, but it's even rougher than this is and honestly would just make it more confusing :D
It's a very simple solution, probably not the most efficient, and is definitely missing a lot of features/convenience that a more sophisticated plugin would provide, but it didn't take me long to make and it does the job I want--updating Algolia based on Strapi lifecycle hooks.
@andrew-braun Thank you for sharing! I'll try this approach with my team and we'll try to make it index only published entries.
@krankos Good luck! And that's actually a good note--I should probably add that to mine as well :D Should be simple enough to add a publicationState check.
@andrew-braun well, we finally made it work! After many attempts and errors, we found the solutions we sought. We decided to use Algolia v4. We followed your steps but did some modifications specially in the algolia-indexing.js
.
Here's our version with the index on publish feature. We just used the publishedAt field as a reference for the entry's state.
"use strict";
function algoliaClient() {
const algoliasearch = require('algoliasearch');
const client = algoliasearch(process.env.ALGOLIA_APP_ID, process.env.ALGOLIA_ADMIN_KEY);
return client;
}
module.exports = ({ strapi }) => ({
async addOrReplace(event, options = { index: "", populate: "*" }) {
try {
const { model } = event;
const { where } = event.params;
const { index, populate } = options;
const entryId =
event.action === "afterUpdate" ? where?.id : event?.result?.id;
console.log(
`${event.action === "afterCreate" ? "Adding" : "Updating"}
${model.singularName}
${entryId} ${event.action === "afterCreate" ? "to" : "in"} Algolia index ${index}`
);
const client = algoliaClient();
const populateObject = await strapi.entityService.findOne(model.uid, entryId, { populate: populate ?? "*", });
if (populateObject.publishedAt === null && event.action === "afterCreate") {
strapi.log.info("created draft")
}
if (populateObject.publishedAt !== null && event.action === "afterCreate") {
strapi.log.info("created publish")
console.log("1", { objectID: entryId, ...populateObject })
const { taskID } = await client.initIndex(index).saveObject({
objectID: entryId, ...populateObject
});
const status = await client.waitForTask({ indexName: index, taskID });
console.log(`Algolia indexing status: ${status.status}`);
}
if (populateObject.publishedAt === null && event.action === "afterUpdate") {
strapi.log.info("updated from published to draft")
const { taskID } = await client.initIndex(index).deleteObject(entryId);
// const status = await client.waitForTask({ indexName: index, taskID });
// console.log(`Algolia indexing status: ${status.status}`);
}
if (populateObject.publishedAt !== null && event.action === "afterUpdate") {
strapi.log.info("updated from draft to published")
console.log("2", { objectID: entryId, ...populateObject })
const { taskID } = await client.initIndex(index).saveObject({
objectID: entryId, ...populateObject
});
// const status = await client.waitForTask({ indexName: index, taskID });
// console.log(`Algolia indexing status: ${status.status}`);
}
}
catch (error) {
console.log(`Error while updating Algolia index: ${JSON.stringify(error)}`);
}
},
async delete(event, options) {
try {
const { index, many } = options;
const objectIDs = many ? event?.params?.where?.["$and"][0]?.id["$in"] : [event.params.where.id];
const client = algoliaClient();
console.log(`Deleting object(s) with ID(s) ${objectIDs.join(",")} from Algolia index ${index}`);
const requests = objectIDs.map((objectID) => {
return {
objectID,
action: "deleteObject",
};
});
// const { taskID } = await client.batch({ indexName: index, batchWriteParams: { requests }, });
const { taskID } = await client.initIndex(index).deleteObjects(objectIDs);
// const response = await client.waitForTask({ indexName: index, taskID });
console.log(`Successfully deleted object(s) with ID(s) ${objectIDs.join(",")} from Algolia index ${index}`);
} catch (error) {
console.log(`Error while deleting object(s) from Algolia index: ${error}`);
}
},
});
hey guys @krankos @andrew-braun, thanks for sharing your solutions here. Did you have any issues after publishing this custom plugin to production? I mean for me everything works great on localhost but when I try to use strpapi cloud instance, indexing doesn't work for some reason. Thanks in advance!
In our case it's working in prod. However, we're not using strapi cloud, we're self hosting since we've been using strapi before strapi cloud was launched. Check your environmental variables. The issue might be as simple as that.
@andrew-braun @krankos thanks for sharing the ideas and the code!
hey guys @krankos @andrew-braun, thanks for sharing your solutions here. Did you have any issues after publishing this custom plugin to production? I mean for me everything works great on localhost but when I try to use strpapi cloud instance, indexing doesn't work for some reason. Thanks in advance!
Nope, it works in all my environments. If you're still having issues, feel free to drop logs of any errors you might be getting and I'll see if I can spot anything!
Haven't seen any new releases for this project in over a year. Is this still being worked on?