@vegan-friendly/strapi-plugin-elasticsearch 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +105 -0
- package/dist/package.json +1 -1
- package/dist/server/bootstrap.js +6 -3
- package/dist/server/index.d.ts +1 -1
- package/dist/server/services/index.d.ts +1 -1
- package/dist/server/services/perform-indexing.js +9 -8
- package/dist/server/services/schedule-indexing.d.ts +1 -1
- package/dist/server/services/schedule-indexing.js +19 -2
- package/package.json +1 -1
package/README.md
CHANGED
@@ -257,6 +257,111 @@ module.exports = (plugin) => {
|
|
257
257
|
- This will create a new route `/api/elasticsearch/enhanced-search` being served by the function defined above.
|
258
258
|
- You can add / modify the routes and controllers as necessary.
|
259
259
|
|
260
|
+
## Virtual Collections
|
261
|
+
|
262
|
+
**Virtual Collections** allow you to index and search data that does not directly map to a single Strapi collection type. This is useful for aggregating, transforming, or combining data from multiple sources before indexing it in Elasticsearch.
|
263
|
+
|
264
|
+
### What is a Virtual Collection?
|
265
|
+
|
266
|
+
A virtual collection is a logical grouping of data that you define, which can be indexed into Elasticsearch as if it were a regular Strapi collection. You control how the data is extracted, transformed, and indexed.
|
267
|
+
|
268
|
+
For example -
|
269
|
+
Let's say that you have two collections defined in Strapi:
|
270
|
+
|
271
|
+
- Restaurant
|
272
|
+
- Chain
|
273
|
+
|
274
|
+
* Each chain is related to one or more restaurants. Some restaurants don't have a chain.
|
275
|
+
|
276
|
+
Now let's say you want to index the restaurants, but in each restaurant you'd like to include some information about the restaurant, and you also want to merge some fields (e.g: if a Restaurant doesn't have a `description` - it inherits it from its chain ).
|
277
|
+
To do that, you need to create a virtual-collection in this plugin configuration, based on your existing restaurant collection.
|
278
|
+
|
279
|
+
### How to Register a Virtual Collection
|
280
|
+
|
281
|
+
To register a virtual collection, you need to provide a configuration object that implements the `VirtualCollectionConfig` interface. This is typically done in your plugin or project code.
|
282
|
+
|
283
|
+
**Example:**
|
284
|
+
|
285
|
+
```typescript
|
286
|
+
// src/extensions/elasticsearch/virtual-collections/my-virtual-collection.ts
|
287
|
+
|
288
|
+
import { VirtualCollectionConfig } from 'strapi-plugin-elasticsearch/server/types/virtual-collections.type';
|
289
|
+
|
290
|
+
const myVirtualCollection: VirtualCollectionConfig = {
|
291
|
+
collectionName: 'virtual::my-virtual-collection',
|
292
|
+
indexAlias: 'my_virtual_collection_index',
|
293
|
+
extractData: async (page, pageSize) => {
|
294
|
+
// Fetch and return an array of entities for the given page
|
295
|
+
// Example: aggregate data from multiple collections
|
296
|
+
return [];
|
297
|
+
},
|
298
|
+
extractByIds: async (ids) => {
|
299
|
+
// Fetch and return entities by their IDs
|
300
|
+
return [];
|
301
|
+
},
|
302
|
+
getIndexItemId: (itemId, collectionName) => `${collectionName}::${itemId}`,
|
303
|
+
triggers: [
|
304
|
+
{
|
305
|
+
collection: 'api::some-collection.some-collection',
|
306
|
+
getIdsToReindex: (event) => {
|
307
|
+
// Return an array of virtual collection item IDs to reindex
|
308
|
+
return [event.result.id];
|
309
|
+
},
|
310
|
+
},
|
311
|
+
],
|
312
|
+
mappings: {
|
313
|
+
// Optional: Elasticsearch mappings for this collection
|
314
|
+
},
|
315
|
+
};
|
316
|
+
|
317
|
+
export default myVirtualCollection;
|
318
|
+
```
|
319
|
+
|
320
|
+
### Key Properties
|
321
|
+
|
322
|
+
- **collectionName**: Unique name for your virtual collection.
|
323
|
+
- **indexAlias**: (Optional) Alias for the Elasticsearch index.
|
324
|
+
- **extractData(page, pageSize)**: Function to fetch paginated data for indexing.
|
325
|
+
- **extractByIds(ids)**: Function to fetch specific items by ID.
|
326
|
+
- **getIndexItemId(itemId, collectionName)**: (Optional) Function to generate unique Elasticsearch document IDs.
|
327
|
+
- **triggers**: Array of triggers that specify which Strapi collections should cause this virtual collection to reindex.
|
328
|
+
- **mappings**: (Optional) Elasticsearch mappings for the index.
|
329
|
+
|
330
|
+
### How Triggers Work
|
331
|
+
|
332
|
+
Each trigger listens to changes on a specified Strapi collection. When a change occurs, the `getIdsToReindex` function is called with the event data, and should return the IDs of the virtual collection items that need to be reindexed.
|
333
|
+
These items are then reindexed - they are fetched using `extractByIds` and are sent to ElasticSearch. If an item cannot be found using `extractByIds` - it is deleted from ElasticSearch.
|
334
|
+
|
335
|
+
### Registering the Virtual Collection
|
336
|
+
|
337
|
+
Register your virtual collection by specifying the `virtualCollections` property in this plugin's configuration, in `config/plugins.js`.
|
338
|
+
|
339
|
+
**Example:**
|
340
|
+
|
341
|
+
```javascript
|
342
|
+
// config/plugins.js
|
343
|
+
|
344
|
+
import myVirtualCollection from './virtual-collections/my-virtual-collection';
|
345
|
+
|
346
|
+
module.exports = async ({ env }) => {
|
347
|
+
return {
|
348
|
+
elasticsearch :{
|
349
|
+
enabled: true,
|
350
|
+
config: {
|
351
|
+
...
|
352
|
+
virtualCollections: [
|
353
|
+
myVirtualCollection,
|
354
|
+
]
|
355
|
+
}
|
356
|
+
}
|
357
|
+
}
|
358
|
+
};
|
359
|
+
```
|
360
|
+
|
361
|
+
### Indexing and Searching
|
362
|
+
|
363
|
+
Once registered, your virtual collection will be indexed according to the triggers and can be searched like any other indexed collection in Elasticsearch.
|
364
|
+
|
260
365
|
## Bugs
|
261
366
|
For any bugs, please create an issue [here](https://github.com/geeky-biz/strapi-plugin-elasticsearch/issues).
|
262
367
|
|
package/dist/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@vegan-friendly/strapi-plugin-elasticsearch",
|
3
|
-
"version": "0.3.
|
3
|
+
"version": "0.3.2",
|
4
4
|
"description": "A Strapi plugin to enable using Elasticsearch with Strapi CMS.",
|
5
5
|
"homepage": "https://github.com/vegan-friendly/strapi-plugin-elasticsearch",
|
6
6
|
"strapi": {
|
package/dist/server/bootstrap.js
CHANGED
@@ -146,16 +146,19 @@ exports.default = async ({ strapi }) => {
|
|
146
146
|
strapi.db.lifecycles.subscribe({
|
147
147
|
models: [collectionUID],
|
148
148
|
afterCreate: async (event) => {
|
149
|
-
|
149
|
+
virtualCollectionIndexer.handleTriggerEvent(event);
|
150
150
|
},
|
151
151
|
afterUpdate: async (event) => {
|
152
|
-
|
152
|
+
virtualCollectionIndexer.handleTriggerEvent(event);
|
153
153
|
},
|
154
154
|
afterDelete: async (event) => {
|
155
|
-
|
155
|
+
virtualCollectionIndexer.handleTriggerEvent(event);
|
156
156
|
},
|
157
157
|
});
|
158
158
|
});
|
159
|
+
// clean up old indexing tasks, as server is booting.
|
160
|
+
// allow 60 seconds, in case strapi is being run in cluster mode and this is the second instance
|
161
|
+
await scheduleIndexingService.getActiveFullIndexingTasks(60);
|
159
162
|
configureIndexingService.markInitialized();
|
160
163
|
}
|
161
164
|
catch (err) {
|
package/dist/server/index.d.ts
CHANGED
@@ -144,7 +144,7 @@ declare const _default: {
|
|
144
144
|
getItemsPendingToBeIndexed(): Promise<any>;
|
145
145
|
markIndexingTaskComplete(recId: any, error?: string | null): Promise<void>;
|
146
146
|
markIndexingTaskInProgress(recId: any): Promise<void>;
|
147
|
-
|
147
|
+
getActiveFullIndexingTasks(staleTaskThresholdSeconds?: number): Promise<any[]>;
|
148
148
|
};
|
149
149
|
esInterface: ({ strapi }: {
|
150
150
|
strapi: any;
|
@@ -40,7 +40,7 @@ declare const _default: {
|
|
40
40
|
getItemsPendingToBeIndexed(): Promise<any>;
|
41
41
|
markIndexingTaskComplete(recId: any, error?: string | null): Promise<void>;
|
42
42
|
markIndexingTaskInProgress(recId: any): Promise<void>;
|
43
|
-
|
43
|
+
getActiveFullIndexingTasks(staleTaskThresholdSeconds?: number): Promise<any[]>;
|
44
44
|
};
|
45
45
|
esInterface: ({ strapi }: {
|
46
46
|
strapi: any;
|
@@ -2,17 +2,18 @@
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
3
3
|
exports.default = ({ strapi }) => ({
|
4
4
|
async rebuildIndex(task = null) {
|
5
|
-
const
|
6
|
-
const
|
7
|
-
const
|
8
|
-
const
|
9
|
-
const
|
10
|
-
const
|
11
|
-
const
|
5
|
+
const pluginServices = strapi.plugins['elasticsearch'].services;
|
6
|
+
const helper = pluginServices.helper;
|
7
|
+
const esInterface = pluginServices.esInterface;
|
8
|
+
const scheduleIndexingService = pluginServices.scheduleIndexing;
|
9
|
+
const configureIndexingService = pluginServices.configureIndexing;
|
10
|
+
const logIndexingService = pluginServices.logIndexing;
|
11
|
+
const virtualCollectionsIndexer = pluginServices.virtualCollectionsIndexer;
|
12
|
+
const virtualCollectionsRegistry = pluginServices.virtualCollectionsRegistry;
|
12
13
|
let taskError = null;
|
13
14
|
try {
|
14
15
|
console.log('strapi-plugin-elasticsearch : Request to rebuild the index received.');
|
15
|
-
const fullIndexingInProgress = await scheduleIndexingService.
|
16
|
+
const fullIndexingInProgress = await scheduleIndexingService.getActiveFullIndexingTasks();
|
16
17
|
if (fullIndexingInProgress.length > 0) {
|
17
18
|
const msg = `Indexing is already in progress - see tasks ${fullIndexingInProgress.map((t) => t.id)}. This request is ignored and marked as failed.`;
|
18
19
|
console.log('strapi-plugin-elasticsearch : ' + msg);
|
@@ -16,6 +16,6 @@ declare const _default: ({ strapi }: {
|
|
16
16
|
getItemsPendingToBeIndexed(): Promise<any>;
|
17
17
|
markIndexingTaskComplete(recId: any, error?: string | null): Promise<void>;
|
18
18
|
markIndexingTaskInProgress(recId: any): Promise<void>;
|
19
|
-
|
19
|
+
getActiveFullIndexingTasks(staleTaskThresholdSeconds?: number): Promise<any[]>;
|
20
20
|
};
|
21
21
|
export default _default;
|
@@ -1,5 +1,6 @@
|
|
1
1
|
"use strict";
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
const STUCK_THRESHOLD_SECONDS = 60 * 60; // 1 hour
|
3
4
|
exports.default = ({ strapi }) => ({
|
4
5
|
async addFullSiteIndexingTask() {
|
5
6
|
const data = await strapi.entityService.create('plugin::elasticsearch.task', {
|
@@ -68,13 +69,29 @@ exports.default = ({ strapi }) => ({
|
|
68
69
|
},
|
69
70
|
});
|
70
71
|
},
|
71
|
-
async
|
72
|
+
async getActiveFullIndexingTasks(staleTaskThresholdSeconds) {
|
73
|
+
if (!staleTaskThresholdSeconds) {
|
74
|
+
staleTaskThresholdSeconds = Number(strapi.config.get('plugin.elasticsearch').staleTaskThresholdSeconds) || STUCK_THRESHOLD_SECONDS;
|
75
|
+
}
|
76
|
+
staleTaskThresholdSeconds *= 1000;
|
72
77
|
const entries = await strapi.entityService.findMany('plugin::elasticsearch.task', {
|
73
78
|
filters: {
|
74
79
|
indexing_status: 'in-progress',
|
75
80
|
full_site_indexing: true,
|
76
81
|
},
|
77
82
|
});
|
78
|
-
|
83
|
+
const now = Date.now();
|
84
|
+
const activeTasks = [];
|
85
|
+
for (const t of entries) {
|
86
|
+
const updatedAt = new Date(t.updatedAt || t.createdAt).getTime();
|
87
|
+
if (now - updatedAt > staleTaskThresholdSeconds) {
|
88
|
+
await strapi.plugins['elasticsearch'].services.logIndexing.recordIndexingFail(`Task ${t.id} was stuck in-progress for too long. Marking as failed.`);
|
89
|
+
await this.markIndexingTaskComplete(t.id, 'Stuck in-progress');
|
90
|
+
}
|
91
|
+
else {
|
92
|
+
activeTasks.push(t);
|
93
|
+
}
|
94
|
+
}
|
95
|
+
return activeTasks;
|
79
96
|
},
|
80
97
|
});
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@vegan-friendly/strapi-plugin-elasticsearch",
|
3
|
-
"version": "0.3.
|
3
|
+
"version": "0.3.2",
|
4
4
|
"description": "A Strapi plugin to enable using Elasticsearch with Strapi CMS.",
|
5
5
|
"homepage": "https://github.com/vegan-friendly/strapi-plugin-elasticsearch",
|
6
6
|
"strapi": {
|