├── .editorconfig ├── .eslintignore ├── .gitignore ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── admin ├── jsconfig.json └── src │ ├── components │ ├── Initializer.jsx │ ├── Loader.jsx │ ├── PluginIcon.jsx │ ├── SubNavigation.jsx │ └── TooltipIconButton.jsx │ ├── index.js │ ├── pages │ ├── App.jsx │ ├── ConfigureCollection.jsx │ ├── ConfigureCollectionList.jsx │ ├── HomePage.jsx │ └── ViewIndexingRunLog.jsx │ ├── pluginId.js │ ├── translations │ └── en.json │ └── utils │ ├── apiUrls.js │ └── getTranslation.js ├── package.json ├── server ├── jsconfig.json └── src │ ├── bootstrap.js │ ├── config │ └── index.js │ ├── content-types │ ├── index.js │ ├── indexing-logs.js │ └── tasks.js │ ├── controllers │ ├── configure-indexing.js │ ├── controller.js │ ├── index.js │ ├── log-indexing.js │ ├── perform-indexing.js │ ├── perform-search.js │ └── setup-info.js │ ├── destroy.js │ ├── index.js │ ├── middlewares │ └── index.js │ ├── policies │ └── index.js │ ├── register.js │ ├── routes │ ├── configure-indexing.js │ ├── content-api.js │ ├── index.js │ ├── perform-indexing.js │ ├── perform-search.js │ ├── run-log.js │ └── setup-info.js │ └── services │ ├── configure-indexing.js │ ├── es-interface.js │ ├── helper.js │ ├── index.js │ ├── log-indexing.js │ ├── perform-indexing.js │ ├── schedule-indexing.js │ ├── service.js │ └── transform-content.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [{package.json,*.yml}] 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | ############################ 4 | # OS X 5 | ############################ 6 | 7 | .DS_Store 8 | .AppleDouble 9 | .LSOverride 10 | Icon 11 | .Spotlight-V100 12 | .Trashes 13 | ._* 14 | 15 | 16 | ############################ 17 | # Linux 18 | ############################ 19 | 20 | *~ 21 | 22 | 23 | ############################ 24 | # Windows 25 | ############################ 26 | 27 | Thumbs.db 28 | ehthumbs.db 29 | Desktop.ini 30 | $RECYCLE.BIN/ 31 | *.cab 32 | *.msi 33 | *.msm 34 | *.msp 35 | 36 | 37 | ############################ 38 | # Packages 39 | ############################ 40 | 41 | *.7z 42 | *.csv 43 | *.dat 44 | *.dmg 45 | *.gz 46 | *.iso 47 | *.jar 48 | *.rar 49 | *.tar 50 | *.zip 51 | *.com 52 | *.class 53 | *.dll 54 | *.exe 55 | *.o 56 | *.seed 57 | *.so 58 | *.swo 59 | *.swp 60 | *.swn 61 | *.swm 62 | *.out 63 | *.pid 64 | 65 | 66 | ############################ 67 | # Logs and databases 68 | ############################ 69 | 70 | .tmp 71 | *.log 72 | *.sql 73 | *.sqlite 74 | *.sqlite3 75 | 76 | 77 | ############################ 78 | # Misc. 79 | ############################ 80 | 81 | *# 82 | ssl 83 | .idea 84 | nbproject 85 | .tsbuildinfo 86 | .eslintcache 87 | .env 88 | 89 | 90 | ############################ 91 | # Strapi 92 | ############################ 93 | 94 | public/uploads/* 95 | !public/uploads/.gitkeep 96 | 97 | 98 | ############################ 99 | # Build 100 | ############################ 101 | 102 | dist 103 | build 104 | 105 | 106 | ############################ 107 | # Node.js 108 | ############################ 109 | 110 | lib-cov 111 | lcov.info 112 | pids 113 | logs 114 | results 115 | node_modules 116 | .node_history 117 | 118 | 119 | ############################ 120 | # Package managers 121 | ############################ 122 | 123 | .yarn/* 124 | !.yarn/cache 125 | !.yarn/unplugged 126 | !.yarn/patches 127 | !.yarn/releases 128 | !.yarn/sdks 129 | !.yarn/versions 130 | .pnp.* 131 | yarn-error.log 132 | 133 | 134 | ############################ 135 | # Tests 136 | ############################ 137 | 138 | coverage 139 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | coverage 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "lf", 3 | "tabWidth": 2, 4 | "printWidth": 100, 5 | "singleQuote": true, 6 | "trailingComma": "es5" 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Punit Sethi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Strapi plugin strapi-plugin-elasticsearch 2 | 3 | A plugin to enable integrating Elasticsearch with Strapi CMS. 4 | 5 | ## Installation 6 | 7 | via npm: 8 | 9 | ``` 10 | npm i @geeky-biz/strapi-plugin-elasticsearch 11 | ``` 12 | 13 | via yarn: 14 | 15 | ``` 16 | yarn add @geeky-biz/strapi-plugin-elasticsearch 17 | ``` 18 | 19 | ## Supported Strapi version 20 | 21 | The latest version of this plugin works for Strapi v5. For Strapi v4, please install the version 0.0.8 of this plugin. 22 | 23 | ## Plugin Configuration 24 | 25 | Within your Strapi project's `config/plugin.js`, enable the plugin and provide the configuration details: 26 | 27 | ``` 28 | module.exports = { 29 | // ... 30 | 'elasticsearch': { 31 | enabled: true, 32 | config: { 33 | indexingCronSchedule: "", 34 | searchConnector: { 35 | host: "", 36 | username: "", 37 | password: "", 38 | certificate: "" 39 | }, 40 | indexAliasName: "" 41 | } 42 | }, 43 | // ... 44 | } 45 | ``` 46 | 47 | Example plugin configuration (with adequate `.env` variables set up): 48 | ``` 49 | module.exports = { 50 | // ... 51 | 'elasticsearch': { 52 | enabled: true, 53 | config: { 54 | indexingCronSchedule: "00 23 * * *", //run daily at 11:00 PM 55 | searchConnector: { 56 | host: process.env.ELASTIC_HOST, 57 | username: process.env.ELASTIC_USERNAME, 58 | password: process.env.ELASTIC_PASSWORD, 59 | certificate: path.join(__dirname, process.env.ELASTIC_CERT_NAME) 60 | }, 61 | indexAliasName: process.env.ELASTIC_ALIAS_NAME 62 | } 63 | }, 64 | // ... 65 | } 66 | ``` 67 | ## Ensuring connection to Elasticsearch 68 | When connected to Elasticsearch, the `Connected` field within the `Setup Information` screen shall display `true`. 69 | 70 | ![image](https://github.com/geeky-biz/strapi-plugin-elasticsearch/assets/17068206/a0fe0d6c-95e9-4c3c-95e1-46209db113c7) 71 | 72 | ## Configuring collections & attributes to be indexed 73 | The `Configure Collections` view displays the collections and the fields setup to be indexed. 74 | 75 | ![image](https://github.com/geeky-biz/strapi-plugin-elasticsearch/assets/17068206/13ad3a24-02a6-4c86-8da2-e015ba9c18ea) 76 | 77 | From this view, individual collection can be selected to modify configuration: 78 | 79 | ![image](https://github.com/geeky-biz/strapi-plugin-elasticsearch/assets/17068206/bdc1d674-a74f-4534-9b48-ad0e0eebaeea) 80 | 81 | ## Configuring indexing for dynamic zone or component attributes 82 | To enable indexing content for attributes of type `component` or `dynamiczone`, additional information needs to be provided via JSON in the following format: 83 | 84 | ``` 85 | { 86 | "subfields": [ 87 | { 88 | "component": "", 89 | "field": "" 90 | }, 91 | {...}, 92 | {...} 93 | ] 94 | } 95 | ``` 96 | ### Example 1: 97 | If we have an attribute called `seo_details` of type `component` like the following within our collection `schema.json`: 98 | ``` 99 | "seo_details": { 100 | "type": "component", 101 | "repeatable": false, 102 | "component": "metainfo.seo" 103 | }, 104 | ``` 105 | And, if we seek to index the contents of the `meta_description` field belonging to the component `seo`, our `subfields` configuration should be: 106 | ``` 107 | { 108 | "subfields": [ 109 | { 110 | "component": "metainfo.seo", 111 | "field": "meta_description" 112 | } 113 | ] 114 | } 115 | ``` 116 | ![image](https://github.com/geeky-biz/strapi-plugin-elasticsearch/assets/17068206/df1f7dba-2aa1-410e-a567-1de73156a020) 117 | 118 | ### Example 2: 119 | If we have an attribute called `sections` of type `dynamiczone` within our collection `schema.json`: 120 | ``` 121 | "sections": { 122 | "type": "dynamiczone", 123 | "components": [ 124 | "content.footer", 125 | "content.paragraph", 126 | "content.separator", 127 | "content.heading" 128 | ] 129 | }, 130 | ... 131 | ``` 132 | And, if we seek to index the contents of the fields `title` for `content.heading` and `details` as well as `subtext` for `content.paragraph`, our `subfields` configuration should be: 133 | ``` 134 | { 135 | "subfields": [ 136 | { 137 | "component": "content.paragraph", 138 | "field": "details" 139 | }, 140 | { 141 | "component": "content.paragraph", 142 | "field": "subtext" 143 | }, 144 | { 145 | "component": "content.heading", 146 | "field": "title" 147 | } 148 | ] 149 | } 150 | ``` 151 | The subfields JSON also supports multiple level of nesting: 152 | ``` 153 | { 154 | "subfields": [ 155 | { 156 | "component": "content.footer", 157 | "field": "footer_link", 158 | "subfields": [ 159 | { 160 | "component": "content.link", 161 | "field": "display_text" 162 | } 163 | ] 164 | } 165 | ] 166 | } 167 | ``` 168 | Note: Indexing of `relations` attributes isn't yet supported. 169 | 170 | ## Exporting and Importing indexing configuration 171 | To enable backing up the indexing configuration or transferring it between various environments, these can be Exported / Imported from the `Configure Collections` view. 172 | 173 | ![image](https://github.com/geeky-biz/strapi-plugin-elasticsearch/assets/17068206/6e099392-499e-4101-8f51-85b7eff8aa38) 174 | 175 | ## Scheduling Indexing 176 | Once the collection attributes are configured for indexing, any changes to the respective collections & attributes is marked for indexing. The cron job (configured via `indexingCronSchedule`) makes actual indexing requests to the connected Elasticsearch instance. 177 | 178 | - `Trigger Indexing` triggers the cron job immediately to perform the pending indexing tasks without waiting for the next scheduled run. 179 | - `Rebuild Indexing` completely rebuilds the index. It may be used if the Elasticsearch appears to be out of sync from the data within Strapi. 180 | 181 | ![image](https://github.com/geeky-biz/strapi-plugin-elasticsearch/assets/17068206/71df02a9-8513-4a91-8e23-2b5f34495c20) 182 | 183 | Whenever a collection is configured for indexing, it may already have data that needs to be indexed. To facilitate indexing of the past data, a collection can be scheduled for indexing in the next cron run from the `Configure Collections` view: 184 | 185 | ![image](https://github.com/geeky-biz/strapi-plugin-elasticsearch/assets/17068206/7f37453a-dc87-406a-8de0-0391018b7fb5) 186 | 187 | ## Searching 188 | You may directly use the Elasticsearch search API or you may use the Search API exposed by the plugin (at `/api/elasticsearch/search`). The plugin search API is just a wrapper around the Elasticsearch search API that passes the query parameter to the Elasticsearch search API and returns the results coming from Elasticsearch: 189 | 190 | For example, the below API call would result into the Elasticsearch search API being triggered with the query 191 | ``` 192 | `/api/elasticsearch/search?query=query%5Bbool%5D%5Bshould%5D%5B0%5D%5Bmatch%5D%5Bcity%5D=atlanta` 193 | ``` 194 | would result into the Elasticsearch search API being triggered with query 195 | ``` 196 | query[bool][should][0][match][city]=atlanta 197 | ``` 198 | The plugin's API would return the response from the Elasticsearch search API. 199 | 200 | Note: To use the `search` API (at `/api/elasticsearch/search`), you will have to provide access via `Settings` -> `Users & Permissions Plugin` -> `Roles` -> (Select adequate role) -> `Elasticsearch` -> `search`. 201 | 202 | ### Extending Search API 203 | The recommended was to enhance the Search API is to write your own route and controller. Below is an example of how this can be achieved (this example adds pagination capability to the search API): 204 | 205 | - Within your setup, create `src/extensions/elasticsearch/strapi-server.js` with the following contents: 206 | 207 | ``` 208 | const { Client } = require('@elastic/elasticsearch') 209 | const qs = require('qs'); 210 | 211 | let client = null; 212 | 213 | module.exports = (plugin) => { 214 | 215 | client = new Client({ 216 | node: plugin.config.searchConnector.host, 217 | auth: { 218 | username: plugin.config.searchConnector.username, 219 | password: plugin.config.searchConnector.password 220 | }, 221 | tls: { 222 | ca: plugin.config.searchConnector.certificate, 223 | rejectUnauthorized: false 224 | } 225 | }); 226 | 227 | plugin.controllers['performSearch'].enhancedSearch = async (ctx) => { 228 | try 229 | { 230 | const params = qs.parse(ctx.request.query) 231 | const query = params.search; 232 | const pagesize = params.pagesize; 233 | const from = params.from; 234 | const result= await client.search({ 235 | index: plugin.config.indexAliasName, 236 | query: { "bool" : { "should" : [ { "match": { "content": "dummy"} } ] } }, 237 | size: pagesize, 238 | from: from 239 | }); 240 | return result; 241 | } 242 | catch(err) 243 | { 244 | console.log('Search : elasticClient.enhancedSearch : Error encountered while making a search request to ElasticSearch.') 245 | throw err; 246 | } 247 | } 248 | 249 | plugin.routes['search'].routes.push({ 250 | method: 'GET', 251 | path: '/enhanced-search', 252 | handler: 'performSearch.enhancedSearch', 253 | }); 254 | 255 | 256 | return plugin; 257 | }; 258 | 259 | ``` 260 | 261 | - This will create a new route `/api/elasticsearch/enhanced-search` being served by the function defined above. 262 | - You can add / modify the routes and controllers as necessary. 263 | 264 | ## Bugs 265 | For any bugs, please create an issue [here](https://github.com/geeky-biz/strapi-plugin-elasticsearch/issues). 266 | 267 | ## About 268 | - This plugin is created by [Punit Sethi](https://punits.dev). 269 | - I'm an independent developer working on Strapi migrations, customizations, configuration projects (see [here](https://punits.dev/strapi-customizations/)). 270 | - For any Strapi implementation requirement, write to me at `punit@tezify.com`. 271 | -------------------------------------------------------------------------------- /admin/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "jsx": "react", 5 | "module": "esnext", 6 | "allowSyntheticDefaultImports": true, 7 | "esModuleInterop": true 8 | }, 9 | "include": ["./src/**/*.js", "./src/**/*.jsx"] 10 | } 11 | -------------------------------------------------------------------------------- /admin/src/components/Initializer.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | 3 | import PLUGIN_ID from '../pluginId'; 4 | 5 | /** 6 | * @type {import('react').FC<{ setPlugin: (id: string) => void }>} 7 | */ 8 | const Initializer = ({ setPlugin }) => { 9 | const ref = useRef(setPlugin); 10 | 11 | useEffect(() => { 12 | ref.current(PLUGIN_ID); 13 | }, []); 14 | 15 | return null; 16 | }; 17 | 18 | export { Initializer }; 19 | -------------------------------------------------------------------------------- /admin/src/components/Loader.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Loader } from '@strapi/design-system'; 3 | import { styled } from 'styled-components'; 4 | 5 | const FullScreenOverlay = styled.div` 6 | display: flex; 7 | background: rgba(255, 255, 255, 0.5); 8 | position: fixed; 9 | bottom: 0; 10 | left: 0; 11 | right: 0; 12 | top: 0; 13 | z-index: 9998; 14 | align-items: center; 15 | justify-content: center; 16 | `; 17 | 18 | const FullScreenLoader = () => { 19 | return ( 20 | 21 | Loading content... 22 | 23 | ); 24 | } 25 | export default FullScreenLoader; 26 | -------------------------------------------------------------------------------- /admin/src/components/PluginIcon.jsx: -------------------------------------------------------------------------------- 1 | import { Search } from '@strapi/icons'; 2 | 3 | const PluginIcon = () => ; 4 | 5 | export { PluginIcon }; 6 | -------------------------------------------------------------------------------- /admin/src/components/SubNavigation.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ChevronRight } from '@strapi/icons'; 3 | import { Box } from '@strapi/design-system'; 4 | import { 5 | SubNav, 6 | SubNavHeader, 7 | SubNavSection, 8 | SubNavSections, 9 | SubNavLink, 10 | } from '@strapi/design-system'; 11 | import { NavLink } from 'react-router-dom'; 12 | import pluginId from "../pluginId"; 13 | 14 | export const SubNavigation = ({activeUrl}) => { 15 | const links = [ { 16 | id: 1, 17 | label : 'Setup Information', 18 | href : `/plugins/${pluginId}/`, 19 | }, 20 | { 21 | id: 2, 22 | label : 'Configure Collections', 23 | href : `/plugins/${pluginId}/configure-collections`, 24 | }, 25 | { 26 | id: 3, 27 | label : 'Indexing Run Logs', 28 | href : `/plugins/${pluginId}/view-indexing-logs`, 29 | }]; 30 | return ( 33 | 34 | 35 | 36 | 37 | {links.map(link => 39 | {link.label} 40 | )} 41 | 42 | 43 | 44 | ); 45 | } -------------------------------------------------------------------------------- /admin/src/components/TooltipIconButton.jsx: -------------------------------------------------------------------------------- 1 | import * as Tooltip from '@radix-ui/react-tooltip'; 2 | import { Typography, Box, IconButton } from "@strapi/design-system"; 3 | 4 | 5 | const TooltipIconButton = ({ children, label, variant, onClick, disabled, showBorder = false }) => { 6 | if (!label) 7 | return ( 8 | 9 | {children} 10 | 11 | ); 12 | 13 | const tooltipContent = (showBorder) ? 14 | 17 | 18 | {label} 19 | 20 | 21 | : 22 | <> 23 | 24 | {label} 25 | 26 | 27 | ; 28 | 29 | return ( 30 | 31 | 32 | 33 | 34 | {children} 35 | 36 | 37 | 38 | 39 | {tooltipContent} 40 | 41 | 42 | 43 | 44 | ); 45 | } 46 | 47 | export default TooltipIconButton; -------------------------------------------------------------------------------- /admin/src/index.js: -------------------------------------------------------------------------------- 1 | import { getTranslation } from './utils/getTranslation'; 2 | import PLUGIN_ID from './pluginId'; 3 | import { Initializer } from './components/Initializer'; 4 | import { PluginIcon } from './components/PluginIcon'; 5 | 6 | export default { 7 | register(app) { 8 | app.addMenuLink({ 9 | to: `plugins/${PLUGIN_ID}`, 10 | icon: PluginIcon, 11 | intlLabel: { 12 | id: `${PLUGIN_ID}.plugin.name`, 13 | defaultMessage: PLUGIN_ID, 14 | }, 15 | Component: async () => { 16 | const { App } = await import('./pages/App'); 17 | 18 | return App; 19 | }, 20 | }); 21 | 22 | app.registerPlugin({ 23 | id: PLUGIN_ID, 24 | initializer: Initializer, 25 | isReady: false, 26 | name: PLUGIN_ID, 27 | }); 28 | }, 29 | 30 | async registerTrads({ locales }) { 31 | return Promise.all( 32 | locales.map(async (locale) => { 33 | try { 34 | const { default: data } = await import(`./translations/${locale}.json`); 35 | 36 | return { data, locale }; 37 | } catch { 38 | return { data: {}, locale }; 39 | } 40 | }) 41 | ); 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /admin/src/pages/App.jsx: -------------------------------------------------------------------------------- 1 | import { Page } from '@strapi/strapi/admin'; 2 | import { Routes, Route } from 'react-router-dom'; 3 | import pluginId from '../pluginId'; 4 | import HomePage from './HomePage'; 5 | import ConfigureCollectionList from './ConfigureCollectionList'; 6 | import ConfigureCollection from './ConfigureCollection'; 7 | import ViewIndexingRunLog from './ViewIndexingRunLog'; 8 | const App = () => { 9 | console.log('pluginId', pluginId); 10 | return ( 11 | 12 | } /> 13 | } exact /> 14 | } exact /> 15 | } /> 16 | } /> 17 | 18 | ); 19 | }; 20 | 21 | export { App }; 22 | -------------------------------------------------------------------------------- /admin/src/pages/ConfigureCollection.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Page } from '@strapi/admin/strapi-admin'; 3 | import { useFetchClient } from '@strapi/admin/strapi-admin'; 4 | import { useParams } from 'react-router-dom'; 5 | import { SubNavigation } from '../components/SubNavigation';; 6 | import { Box, Flex } from '@strapi/design-system'; 7 | import { Toggle } from '@strapi/design-system'; 8 | import { Link } from '@strapi/design-system'; 9 | import pluginId from '../pluginId'; 10 | import { apiGetCollectionConfig, apiSaveCollectionConfig } from "../utils/apiUrls"; 11 | import { Alert } from '@strapi/design-system'; 12 | import { Button } from '@strapi/design-system'; 13 | import { ArrowLeft } from '@strapi/icons'; 14 | import { Typography } from '@strapi/design-system'; 15 | import { Textarea, TextInput } from '@strapi/design-system'; 16 | import Loader from "../components/Loader"; 17 | 18 | 19 | 20 | const ConfigureField = ({config, index, setFieldConfig}) => { 21 | 22 | const validateSubfieldsConfig = (conf) => { 23 | if (conf && conf.length > 0) 24 | { 25 | try { 26 | JSON.parse(conf); 27 | return true; 28 | } catch (e) { 29 | return false; 30 | } 31 | } 32 | else 33 | return true; 34 | } 35 | 36 | const updateIndex = (checked) => { 37 | setFieldConfig({index, config: {...config, index: checked}}) 38 | } 39 | 40 | const updateSubfieldConfig = (subfields) => { 41 | const subfieldsConfigValid = validateSubfieldsConfig(subfields); 42 | setFieldConfig({index, config: {...config, subfields, subfieldsConfigValid}}) 43 | } 44 | 45 | const updateMappedFieldName = (mappedName) => { 46 | setFieldConfig({index, config: {...config, searchFieldName: mappedName}}) 47 | } 48 | 49 | return ( 50 | 52 | 53 | {config.name} 54 | 55 | 56 | updateIndex(e.target.checked)} /> 58 | 59 | 60 | updateMappedFieldName(e.target.value)} value={config.searchFieldName || ""} /> 61 | 62 | { 63 | config.index && config.type && config.type === "dynamiczone" ? ( 64 | 65 | 70 | 71 | ) : null 72 | } 73 | { 74 | config.index && config.type && config.type === "component" ? ( 75 | 76 | 81 | 82 | ) : null 83 | } 84 | 85 | ) 86 | } 87 | 88 | const ConfigureCollection = () => { 89 | const [isInProgress, setIsInProgress] = useState(false); 90 | const [selectedCollection, setSelectedCollection] = useState(null); 91 | const [collectionConfig, setCollectionConfig] = useState(null); 92 | 93 | const params = useParams(); 94 | const [alertContent, setAlertContent] = useState(null); 95 | const { get, post } = useFetchClient(); 96 | const showMessage = ({variant, title, text}) => { 97 | setAlertContent({variant, title, text}); 98 | setTimeout(() => { 99 | setAlertContent(null); 100 | }, 5000); 101 | }; 102 | const updateCollectionsConfig = ({index, config}) => { 103 | setCollectionConfig({ 104 | collectionName: collectionConfig.collectionName, 105 | attributes: collectionConfig.attributes.map((e, idx) => index === idx ? config : e) 106 | }); 107 | } 108 | 109 | const loadConfigForCollection = (collectionName) => { 110 | return get(apiGetCollectionConfig(collectionName)) 111 | .then((resp) => resp.data); 112 | } 113 | 114 | const saveConfigForCollection = (collectionName, data) => { 115 | return post(apiSaveCollectionConfig(collectionName), { 116 | data 117 | }) 118 | } 119 | 120 | const saveCollectionConfig = () => { 121 | if (collectionConfig && collectionConfig.collectionName) 122 | { 123 | const data = {} 124 | data[collectionConfig.collectionName] = {} 125 | for (let k=0; k { 133 | showMessage({ 134 | variant: "success", title: "The collection configuration is saved.", text: "The collection configuration is saved." 135 | }); 136 | window.scrollTo({ top: 0, behavior: 'smooth' }); 137 | }) 138 | .catch((err) => { 139 | showMessage({ 140 | variant: "warning", title: "An error was encountered.", text: err.message || "An error was encountered." 141 | }); 142 | window.scrollTo({ top: 0, behavior: 'smooth' }); 143 | console.log(err); 144 | }) 145 | .finally(() => setIsInProgress(false)); 146 | } 147 | } 148 | 149 | useEffect(() => { 150 | if (params && params.collectionName) 151 | setSelectedCollection(params.collectionName) 152 | }, [params]); 153 | 154 | useEffect(() => { 155 | if (selectedCollection) 156 | { 157 | loadConfigForCollection(selectedCollection) 158 | .then((resp) => { 159 | if (Object.keys(resp).length === 0) 160 | { 161 | showMessage({ 162 | variant: "warning", title: 'No collection with the selected name exists.', text: 'No collection with the selected name exists.' 163 | }); 164 | } 165 | else 166 | { 167 | const collectionName = Object.keys(resp)[0]; 168 | const attributeNames = Object.keys(resp[collectionName]); 169 | const attributes = []; 170 | for (let s = 0; s { 177 | showMessage({ 178 | variant: "warning", title: "An error was encountered.", text: err.message || "An error was encountered." 179 | }); 180 | window.scrollTo({ top: 0, behavior: 'smooth' }); 181 | console.log(err); 182 | }); 183 | } 184 | }, [selectedCollection]); 185 | 186 | if (collectionConfig === null) 187 | return ; 188 | else 189 | return ( 190 | 191 | Configure Collection {selectedCollection} 192 | 193 | 194 | 195 | 196 | } href={`/admin/plugins/${pluginId}/configure-collections`}> 197 | Back 198 | 199 | 200 | { 201 | selectedCollection && ( 202 | 203 | {selectedCollection} 204 | 205 | ) 206 | } 207 | { 208 | alertContent && 209 | {alertContent.text} 210 | } 211 | { 212 | collectionConfig && ( 213 | <> 214 | 215 | 216 | 217 | Attributes 218 | { 219 | collectionConfig.attributes.map((a, idx) => { 220 | return 222 | }) 223 | } 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | ) 232 | } 233 | 234 | 235 | 236 | ); 237 | }; 238 | 239 | export default ConfigureCollection; 240 | -------------------------------------------------------------------------------- /admin/src/pages/ConfigureCollectionList.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * HomePage 4 | * 5 | */ 6 | 7 | import React, { useState } from 'react'; 8 | import { Page } from '@strapi/admin/strapi-admin'; 9 | import { useFetchClient } from '@strapi/admin/strapi-admin'; 10 | import pluginId from '../pluginId'; 11 | import { SubNavigation } from '../components/SubNavigation'; 12 | import { Grid, Box, Flex } from '@strapi/design-system'; 13 | import { useEffect } from 'react'; 14 | import { apiGetContentConfig, apiRequestCollectionIndexing, 15 | apiImportContentConfig, apiExportContentConfig } from "../utils/apiUrls"; 16 | import { Alert } from '@strapi/design-system'; 17 | import { Table, Thead, Tbody, Tr, Td, Th } from '@strapi/design-system'; 18 | import { Typography } from '@strapi/design-system'; 19 | import { Pencil, Server } from '@strapi/icons'; 20 | import { useNavigate } from "react-router-dom"; 21 | import { Button } from '@strapi/design-system'; 22 | import { Modal } from '@strapi/design-system'; 23 | import { Textarea, Divider } from '@strapi/design-system'; 24 | import Loader from '../components/Loader'; 25 | import TooltipIconButton from '../components/TooltipIconButton'; 26 | 27 | 28 | const Configure = () => { 29 | const [alertContent, setAlertContent] = useState(null); 30 | const [isInProgress, setIsInProgress] = useState(false); 31 | const [displayImportModal, setDisplayImportModal] = useState(true); 32 | const [isEnteredJsonValid, setIsEnteredJsonValid] = useState(true); 33 | const [importJson, setImportJson] = useState(null); 34 | 35 | const [config, setConfig] = useState(null); 36 | const navigate = useNavigate(); 37 | const { get, post } = useFetchClient(); 38 | 39 | const showMessage = ({variant, title, text}) => { 40 | setAlertContent({variant, title, text}); 41 | setTimeout(() => { 42 | setAlertContent(null); 43 | }, 5000); 44 | }; 45 | 46 | const exportContentConfig = () => { 47 | 48 | return get(apiExportContentConfig, { 49 | responseType: 'blob' 50 | }) 51 | .then((response) => { 52 | const jsonString = JSON.stringify(response.data, null, 2); 53 | const blob = new Blob([jsonString], { type: 'application/json' }); 54 | const href = URL.createObjectURL(blob); 55 | const link = document.createElement('a'); 56 | link.href = href; 57 | link.setAttribute('download', 'strapi-plugin-elasticsearch-contentconfig.json'); 58 | document.body.appendChild(link); 59 | link.click(); 60 | document.body.removeChild(link); 61 | URL.revokeObjectURL(href); 62 | }); 63 | } 64 | 65 | const importContentConfig = (conf) => { 66 | return post(apiImportContentConfig, { 67 | data : conf 68 | }); 69 | } 70 | 71 | const loadContentConfig = () => { 72 | return get(apiGetContentConfig) 73 | .then((resp) => resp.data); 74 | } 75 | 76 | const scheduleCollectionIndexing = (collectionName) => { 77 | setIsInProgress(true); 78 | return get(apiRequestCollectionIndexing(collectionName)) 79 | .then(() => { 80 | showMessage({variant: "success", title: "Success", text: `Collection ${collectionName} scheduled for indexing.`}); 81 | }) 82 | .catch((err) => { 83 | showMessage({variant: "warning", title: "Error", text: "Scheduling collection indexing failed. An error was encountered."}); 84 | console.log(err); 85 | }) 86 | .finally(() => { 87 | window.scrollTo({ top: 0, behavior: 'smooth' }); 88 | setIsInProgress(false) 89 | }); 90 | } 91 | 92 | const performImport = () => { 93 | const conf = importJson 94 | console.log(conf && conf.length > 0) 95 | if (conf && conf.length > 0) { 96 | setIsInProgress(true); 97 | importContentConfig(conf) 98 | .then(() => { 99 | showMessage({variant: "success", title: "Success", text: "Collections configuration imported. Please refresh this view."}); 100 | }) 101 | .catch((err) => { 102 | showMessage({variant: "warning", title: "Error", text: "Importing collections configuration failed. An error was encountered."}); 103 | console.log(err); 104 | }) 105 | .finally(() => setIsInProgress(false)); 106 | } 107 | } 108 | const performExport = () => { 109 | setIsInProgress(true); 110 | exportContentConfig() 111 | .then(() => { 112 | showMessage({variant: "success", title: "Success", text: "Collections configuration exported."}); 113 | }) 114 | .catch((err) => { 115 | showMessage({variant: "warning", title: "Error", text: "Exporting collections configuration failed. An error was encountered."}); 116 | console.log(err); 117 | }) 118 | .finally(() => setIsInProgress(false)); 119 | } 120 | 121 | useEffect(() => { 122 | if (importJson && importJson.length > 0) 123 | { 124 | try { 125 | JSON.parse(importJson); 126 | setIsEnteredJsonValid(true); 127 | } catch (e) { 128 | setIsEnteredJsonValid(false); 129 | } 130 | } 131 | }, [importJson]); 132 | 133 | useEffect(() => { 134 | setIsInProgress(true); 135 | loadContentConfig() 136 | .then((resp) => { 137 | const displayConfig = []; 138 | for (let r=0; r < Object.keys(resp).length; r++) 139 | { 140 | const item = {collectionName: Object.keys(resp)[r], indexed: [], notIndexed: []}; 141 | const collectionName = item.collectionName; 142 | for (let k=0; k < Object.keys(resp[collectionName]).length; k++) 143 | { 144 | const attribs = resp[collectionName]; 145 | for (let s=0; s < Object.keys(attribs).length; s++) 146 | { 147 | const attrName = Object.keys(attribs)[s] 148 | const attr = attribs[attrName] 149 | if (attr.index === false && !item.notIndexed.includes(attrName)) 150 | item.notIndexed.push(attrName) 151 | else if (attr.index && !item.indexed.includes(attrName)) 152 | item.indexed.push(attrName) 153 | } 154 | } 155 | displayConfig.push(item); 156 | } 157 | setConfig(displayConfig); 158 | }) 159 | .catch((err) => { 160 | showMessage({variant: "warning", title: "Error", text: "An error was encountered while fetching the configuration."}); 161 | console.log(err); 162 | }) 163 | .finally (() => { 164 | setIsInProgress(false); 165 | }) 166 | }, []); 167 | if (config === null) 168 | return ; 169 | else 170 | { 171 | return ( 172 | 173 | Configure Collections 174 | 175 | 176 | 177 | 178 | Configure Collections 179 | 180 | {alertContent && ( 181 | 182 | {alertContent.text} 183 | 184 | )} 185 | {config && ( 186 | 187 | 188 | 189 | 190 | 191 | 194 | 197 | 200 | 203 | 204 | 205 | 206 | {config.map((collection, idx) => ( 207 | 208 | 211 | 218 | 225 | 246 | 247 | ))} 248 | 249 |
192 | Collection 193 | 195 | Index 196 | 198 | Do not Index 199 | 201 | Actions 202 |
209 | {collection.collectionName} 210 | 212 | {collection.indexed.map((i) => ( 213 | 214 | {i} 215 | 216 | ))} 217 | 219 | {collection.notIndexed.map((i) => ( 220 | 221 | {i} 222 | 223 | ))} 224 | 226 | 227 | navigate(`/plugins/${pluginId}/configure-collections/${collection.collectionName}`)} 230 | label="Edit collection configuration" 231 | noBorder 232 | > 233 | 234 | 235 | 236 | scheduleCollectionIndexing(collection.collectionName)} 239 | label="Schedule indexing for all items in this collection" 240 | noBorder 241 | > 242 | 243 | 244 | 245 |
250 |
251 | 252 | 253 | 254 | CONFIG ACTIONS 255 | 256 | 257 | 258 | 259 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | Import Search Configuration 271 | 272 | 273 | 280 | 281 | 282 | 283 | 284 | 285 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 |
300 | )} 301 |
302 |
303 |
304 | ); 305 | } 306 | }; 307 | 308 | export default Configure; 309 | -------------------------------------------------------------------------------- /admin/src/pages/HomePage.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Page } from '@strapi/admin/strapi-admin'; 3 | import { useFetchClient } from '@strapi/admin/strapi-admin'; 4 | import { Alert } from '@strapi/design-system'; 5 | import { SubNavigation } from '../components/SubNavigation'; 6 | import { Box, Flex, Tab } from '@strapi/design-system'; 7 | import { Typography } from '@strapi/design-system'; 8 | import { apiGetElasticsearchSetupInfo, apiRequestReIndexing, 9 | apiTriggerIndexing } from '../utils/apiUrls'; 10 | import { Table, Tr, Td } from '@strapi/design-system'; 11 | import { ArrowClockwise } from '@strapi/icons'; 12 | import { Button } from '@strapi/design-system'; 13 | import { Grid } from '@strapi/design-system'; 14 | import { Divider } from '@strapi/design-system'; 15 | import Loader from "../components/Loader"; 16 | import pluginId from '../pluginId'; 17 | import TooltipIconButton from '../components/TooltipIconButton'; 18 | const Homepage = () => { 19 | const [alertContent, setAlertContent] = useState(null); 20 | const [setupInfo, setSetupInfo] = useState(null); 21 | const [isInProgress, setIsInProgress] = useState(false); 22 | const { get } = useFetchClient(); 23 | 24 | const showMessage = ({variant, title, text}) => { 25 | setAlertContent({variant, title, text}); 26 | setTimeout(() => { 27 | setAlertContent(null); 28 | }, 5000); 29 | }; 30 | 31 | const displayLabels = {'connected' : 'Connected', 32 | 'elasticCertificate' : 'Certificate', 33 | 'elasticHost' : 'Elasticsearch host', 34 | 'elasticIndexAlias' : 'Elasticsearch index Alias name', 35 | 'elasticUserName' : 'Elasticsearch username', 36 | 'indexingCronSchedule' : 'Indexing cron schedule', 37 | 'initialized' : 'Elasticsearch configuration loaded'}; 38 | 39 | const loadElasticsearchSetupInfo = () => { 40 | return get(apiGetElasticsearchSetupInfo) 41 | .then((resp) => resp.data) 42 | .then((data) => { 43 | return data; 44 | }); 45 | }; 46 | 47 | const reloadElasticsearchSetupInfo = ({showNotification}) => { 48 | setIsInProgress(true); 49 | loadElasticsearchSetupInfo() 50 | .then(setSetupInfo) 51 | .then(() => { 52 | if (showNotification) 53 | showMessage({variant: 'success', title: 'Success', text: 'Elasticsearch setup information reloaded.'}) 54 | }) 55 | .catch(() => { 56 | showMessage({variant: 'danger', title: 'Error', text: 'An error was encountered. Please contact support.'}) 57 | }) 58 | .finally(() => setIsInProgress(false)); 59 | } 60 | 61 | const requestFullSiteReindexing = () => { 62 | setIsInProgress(true); 63 | return get(apiRequestReIndexing) 64 | .then(() => { 65 | showMessage({variant: 'success', title: 'Success', text: 'Rebuilding the index is triggered.'}) 66 | }) 67 | .catch(() => { 68 | showMessage({variant: 'warning', title: 'Error', text: 'An error was encountered.'}) 69 | }) 70 | .finally(() => setIsInProgress(false)); 71 | } 72 | 73 | const triggerIndexingRun = () => { 74 | setIsInProgress(true); 75 | return get(apiTriggerIndexing) 76 | .then(() => { 77 | showMessage({variant: 'success', title: 'Success', text: 'The indexing job to process the pending tasks is started.'}) 78 | }) 79 | .catch(() => { 80 | showMessage({variant: 'warning', title: 'Error', text: 'An error was encountered.'}) 81 | }) 82 | .finally(() => setIsInProgress(false)); 83 | } 84 | useEffect(() => { 85 | reloadElasticsearchSetupInfo({showNotification: false}); 86 | }, []); 87 | if (setupInfo === null) 88 | return (); 89 | else 90 | return ( 91 | 92 | ElasticSearch Setup Information 93 | 94 | 95 | 96 | 97 | Setup Information 98 | 99 | { 100 | alertContent && 101 | {alertContent.text} 102 | } 103 | 104 | 105 | 106 | 107 | 108 | { 109 | setupInfo && ( 110 | Object.keys(setupInfo).map((k, idx) => { 111 | return ( 112 | 113 | 116 | 150 | 151 | ); 152 | }) 153 | ) 154 | } 155 | 156 |
114 | {displayLabels[k]} : 115 | 117 | { 118 | k === 'connected' && setupInfo[k] === true && 119 | ( 120 | 121 | 122 | Yes 123 | 124 | 125 | reloadElasticsearchSetupInfo({showNotification: true})} label="Refresh"> 126 | 127 | 128 | ) 129 | } 130 | { 131 | k === 'connected' && setupInfo[k] === false && 132 | ( 133 | 134 | 135 | No 136 | 137 | 138 | reloadElasticsearchSetupInfo({showNotification: true})} label="Refresh"> 139 | 140 | 141 | ) 142 | } 143 | { 144 | k !== 'connected' && 145 | ( 146 | {String(setupInfo[k])} 147 | ) 148 | } 149 |
157 | 158 | 159 | For bugs or feature requests related to this plugin, please email on punit@tezify.com 160 | (or check https://punits.dev) 161 | 162 | 163 |
164 | 165 | 166 | 167 | ACTIONS 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 |
181 |
182 |
183 |
184 |
185 | ); 186 | }; 187 | 188 | 189 | export default Homepage; -------------------------------------------------------------------------------- /admin/src/pages/ViewIndexingRunLog.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Page } from '@strapi/admin/strapi-admin'; 3 | import { useFetchClient } from '@strapi/admin/strapi-admin'; 4 | import { SubNavigation } from '../components/SubNavigation';; 5 | import { Box, Flex } from '@strapi/design-system'; 6 | import { apiFetchRecentIndexingRunLog } from "../utils/apiUrls"; 7 | import { Table, Thead, Tbody, Tr, Td, Th } from '@strapi/design-system'; 8 | import { Typography } from '@strapi/design-system'; 9 | import Loader from "../components/Loader"; 10 | import pluginId from '../pluginId'; 11 | const ViewIndexingRunLog = () => { 12 | const [logTable, setLogTable] = useState(null); 13 | const { get } = useFetchClient(); 14 | const loadRecentIndexingRuns = () => { 15 | return get(apiFetchRecentIndexingRunLog) 16 | .then((resp) => resp.data) 17 | .then((data) => { 18 | return data; 19 | }); 20 | } 21 | 22 | const formattedDate = (dateString) => { 23 | const date = new Date(dateString); 24 | const options = { 25 | weekday: 'long', 26 | year: 'numeric', 27 | month: 'numeric', 28 | day: 'numeric', 29 | hour: 'numeric', 30 | minute: 'numeric', 31 | }; 32 | 33 | const dateTimeFormat = new Intl.DateTimeFormat('en-US', options); 34 | const parts = dateTimeFormat.formatToParts(date); 35 | 36 | let formattedDate = ''; 37 | parts.forEach((part) => { 38 | if (part.type === "weekday") 39 | formattedDate+= `${part.value}, `; 40 | if (part.type === "day") 41 | formattedDate+= `${part.value}/`; 42 | if (part.type === "month") 43 | formattedDate+= `${part.value}/`; 44 | if (part.type === "year") 45 | formattedDate+= `${part.value} `; 46 | if (part.type === "hour") 47 | formattedDate+= `${part.value}:`; 48 | if (part.type === "minute") 49 | formattedDate+= `${part.value}`; 50 | }); 51 | 52 | return formattedDate; 53 | } 54 | 55 | useEffect(() => { 56 | loadRecentIndexingRuns() 57 | .then(setLogTable) 58 | }, []); 59 | 60 | if (logTable === null) 61 | return 62 | else 63 | return ( 64 | 65 | Recent Indexing Run Logs 66 | 67 | 68 | 69 | 70 | Recent Indexing Run Logs 71 | 72 | { 73 | logTable && logTable.length > 0 && ( 74 | <> 75 | 76 | 77 | 78 | 81 | 84 | 87 | 88 | 89 | 90 | { 91 | logTable.map((data, index) => { 92 | return ( 93 | 94 | 97 | 100 | 103 | 104 | ); 105 | }) 106 | } 107 | 108 |
79 | Date 80 | 82 | Status 83 | 85 | Details 86 |
95 | {formattedDate(data.createdAt)} 96 | 98 | {data.status} 99 | 101 | {data.details} 102 |
109 | 110 | This view lists the details of the 50 recent-most indexing runs. 111 | 112 | 113 | ) 114 | } 115 |
116 |
117 |
118 | ); 119 | }; 120 | 121 | export default ViewIndexingRunLog; 122 | -------------------------------------------------------------------------------- /admin/src/pluginId.js: -------------------------------------------------------------------------------- 1 | import pluginPkg from '../../package.json'; 2 | 3 | const pluginId = pluginPkg.strapi.name; 4 | 5 | export default pluginId; 6 | -------------------------------------------------------------------------------- /admin/src/translations/en.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /admin/src/utils/apiUrls.js: -------------------------------------------------------------------------------- 1 | import pluginId from "../pluginId"; 2 | export const apiGetContentConfig = `/${pluginId}/content-config/` 3 | export const apiGetCollectionConfig = (collectionName) => `/${pluginId}/collection-config/${collectionName}` 4 | export const apiSaveCollectionConfig = (collectionName) => `/${pluginId}/collection-config/${collectionName}` 5 | export const apiGetElasticsearchSetupInfo = `/${pluginId}/setup-info` 6 | export const apiFetchRecentIndexingRunLog = `/${pluginId}/indexing-run-log` 7 | export const apiRequestReIndexing = `/${pluginId}/reindex` 8 | export const apiRequestCollectionIndexing = (collectionName) => `/${pluginId}/collection-reindex/${collectionName}` 9 | export const apiTriggerIndexing = `/${pluginId}/trigger-indexing/` 10 | 11 | export const apiExportContentConfig = `/${pluginId}/export-content-config/` 12 | export const apiImportContentConfig = `/${pluginId}/import-content-config/` 13 | 14 | -------------------------------------------------------------------------------- /admin/src/utils/getTranslation.js: -------------------------------------------------------------------------------- 1 | import PLUGIN_ID from '../pluginId'; 2 | 3 | const getTranslation = (id) => `${PLUGIN_ID}.${id}`; 4 | 5 | export { getTranslation }; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@geeky-biz/strapi-plugin-elasticsearch", 3 | "description": "A Strapi plugin to enable using Elasticsearch with Strapi CMS.", 4 | "license": "MIT", 5 | "author": "Punit Sethi ", 6 | "homepage": "https://github.com/geeky-biz/strapi-plugin-elasticsearch", 7 | "repository": { 8 | "type": "git", 9 | "url": "git://github.com/geeky-biz/strapi-plugin-elasticsearch.git" 10 | }, 11 | "maintainers": [ 12 | { 13 | "name": "Punit Sethi", 14 | "email": "punit@tezify.com", 15 | "url": "https://punits.dev" 16 | } 17 | ], 18 | "engines": { 19 | "node": ">=18.0.0 <=22.x.x", 20 | "npm": ">=6.0.0" 21 | }, 22 | "version": "0.0.10", 23 | "keywords": [], 24 | "type": "commonjs", 25 | "exports": { 26 | "./package.json": "./package.json", 27 | "./strapi-admin": { 28 | "source": "./admin/src/index.js", 29 | "import": "./dist/admin/index.mjs", 30 | "require": "./dist/admin/index.js", 31 | "default": "./dist/admin/index.js" 32 | }, 33 | "./strapi-server": { 34 | "source": "./server/src/index.js", 35 | "import": "./dist/server/index.mjs", 36 | "require": "./dist/server/index.js", 37 | "default": "./dist/server/index.js" 38 | } 39 | }, 40 | "files": [ 41 | "dist" 42 | ], 43 | "scripts": { 44 | "build": "strapi-plugin build", 45 | "watch": "strapi-plugin watch", 46 | "watch:link": "strapi-plugin watch:link", 47 | "verify": "strapi-plugin verify" 48 | }, 49 | "dependencies": { 50 | "@elastic/elasticsearch": "^8.17.1", 51 | "@radix-ui/react-tooltip": "^1.1.8", 52 | "@strapi/design-system": "^2.0.0-rc.18", 53 | "@strapi/icons": "^2.0.0-rc.18", 54 | "markdown-to-txt": "^2.0.1", 55 | "react-intl": "^7.1.9" 56 | }, 57 | "devDependencies": { 58 | "@strapi/sdk-plugin": "^5.3.2", 59 | "@strapi/strapi": "^5.11.3", 60 | "prettier": "^3.5.3", 61 | "react": "^18.3.1", 62 | "react-dom": "^18.3.1", 63 | "react-router-dom": "^6.30.0", 64 | "styled-components": "^6.1.16" 65 | }, 66 | "peerDependencies": { 67 | "@strapi/sdk-plugin": "^5.3.2", 68 | "@strapi/strapi": "^5.11.3", 69 | "react": "^18.3.1", 70 | "react-dom": "^18.3.1", 71 | "react-router-dom": "^6.30.0", 72 | "styled-components": "^6.1.16" 73 | }, 74 | "strapi": { 75 | "name": "elasticsearch", 76 | "description": "A plugin to enable using Elasticsearch with Strapi CMS.", 77 | "kind": "plugin", 78 | "displayName": "Strapi <-> Elasticsearch" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /server/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "allowSyntheticDefaultImports": true, 6 | "esModuleInterop": true 7 | }, 8 | "include": ["./src/**/*.js"], 9 | "exclude": ["node_modules"] 10 | } 11 | -------------------------------------------------------------------------------- /server/src/bootstrap.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async ({ strapi }) => { 4 | const pluginConfig = await strapi.config.get('plugin::elasticsearch'); 5 | const configureIndexingService = strapi.plugins['elasticsearch'].services.configureIndexing; 6 | const esInterface = strapi.plugins['elasticsearch'].services.esInterface; 7 | const indexer = strapi.plugins['elasticsearch'].services.indexer; 8 | const helper = strapi.plugins['elasticsearch'].services.helper; 9 | try 10 | { 11 | await configureIndexingService.initializeStrapiElasticsearch(); 12 | 13 | if (!Object.keys(pluginConfig).includes('indexingCronSchedule')) 14 | console.warn("The plugin strapi-plugin-elasticsearch is enabled but the indexingCronSchedule is not configured."); 15 | else if (!Object.keys(pluginConfig).includes('searchConnector')) 16 | console.warn("The plugin strapi-plugin-elasticsearch is enabled but the searchConnector is not configured."); 17 | else 18 | { 19 | const connector = pluginConfig['searchConnector']; 20 | await esInterface.initializeSearchEngine({host : connector.host, uname: connector.username, 21 | password: connector.password, cert: connector.certificate}); 22 | strapi.cron.add({ 23 | elasticsearchIndexing: { 24 | task: async ({ strapi }) => { 25 | await indexer.indexPendingData(); 26 | }, 27 | options: { 28 | rule: pluginConfig['indexingCronSchedule'], 29 | }, 30 | }, 31 | }); 32 | 33 | if (await esInterface.checkESConnection()) 34 | { 35 | //Attach the alias to the current index: 36 | const idxName = await helper.getCurrentIndexName(); 37 | await esInterface.attachAliasToIndex(idxName); 38 | } 39 | 40 | } 41 | configureIndexingService.markInitialized(); 42 | } 43 | catch(err) { 44 | console.error('An error was encountered while initializing the strapi-plugin-elasticsearch plugin.') 45 | console.error(err); 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /server/src/config/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | default: {}, 3 | validator() {}, 4 | }; 5 | -------------------------------------------------------------------------------- /server/src/content-types/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const task = require('./tasks'); 4 | const indexingLog = require('./indexing-logs'); 5 | 6 | module.exports = { 7 | 'task' : {schema : task}, 8 | 'indexing-log' : {schema: indexingLog} 9 | }; 10 | -------------------------------------------------------------------------------- /server/src/content-types/indexing-logs.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "kind": "collectionType", 3 | "collectionName": "indexing-log", 4 | "info": { 5 | "singularName": "indexing-log", 6 | "pluralName": "indexing-logs", 7 | "displayName": "Indexing Logs", 8 | "description": "Logged runs of the indexing cron job" 9 | }, 10 | "options": { 11 | "draftAndPublish": false 12 | }, 13 | "pluginOptions": { 14 | 'content-manager': { 15 | visible: false, 16 | }, 17 | 'content-type-builder': { 18 | visible: false, 19 | } 20 | }, 21 | "attributes": { 22 | "status": { 23 | "type": "enumeration", 24 | "enum": [ 25 | "pass", 26 | "fail" 27 | ], 28 | "required": true 29 | }, 30 | "details": { 31 | "type": "text" 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /server/src/content-types/tasks.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "kind": "collectionType", 3 | "collectionName": "task", 4 | "info": { 5 | "singularName": "task", 6 | "pluralName": "tasks", 7 | "displayName": "Task", 8 | "description": "Search indexing tasks" 9 | }, 10 | "options": { 11 | "draftAndPublish": false 12 | }, 13 | "pluginOptions": { 14 | 'content-manager': { 15 | visible: false, 16 | }, 17 | 'content-type-builder': { 18 | visible: false, 19 | } 20 | }, 21 | "attributes": { 22 | "collection_name": { 23 | "type": "string", 24 | "required": true 25 | }, 26 | "item_document_id": { 27 | "type": "string" 28 | }, 29 | "indexing_status": { 30 | "type": "enumeration", 31 | "enum": [ 32 | "to-be-done", 33 | "done" 34 | ], 35 | "required": true, 36 | "default": "to-be-done" 37 | }, 38 | "full_site_indexing": { 39 | "type": "boolean" 40 | }, 41 | "indexing_type": { 42 | "type": "enumeration", 43 | "enum": [ 44 | "add-to-index", 45 | "remove-from-index" 46 | ], 47 | "default": "add-to-index", 48 | "required": true 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /server/src/controllers/configure-indexing.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | 5 | module.exports = ({ strapi }) => { 6 | const configureIndexingService = strapi.plugins['elasticsearch'].services.configureIndexing; 7 | 8 | const getContentConfig = async (ctx) => { 9 | return configureIndexingService.getContentConfig(); 10 | }; 11 | 12 | const saveCollectionConfig = async (ctx) => { 13 | const { body } = ctx.request; 14 | try { 15 | const updatedConfig = await configureIndexingService.setContentConfig({collection: ctx.params.collectionname, config : body.data}); 16 | return updatedConfig; 17 | } catch (err) { 18 | ctx.throw(500, err); 19 | } 20 | }; 21 | 22 | const importContentConfig = async (ctx) => { 23 | const { body } = ctx.request; 24 | try { 25 | if (body['data']) 26 | { 27 | const updatedConfig = await configureIndexingService.importContentConfig({config : body['data']}); 28 | return updatedConfig; 29 | } 30 | else 31 | ctx.throw(400, 'Invalid parameters') 32 | } catch (err) { 33 | ctx.throw(500, err); 34 | } 35 | } 36 | 37 | const exportContentConfig = async (ctx) => { 38 | return configureIndexingService.getContentConfig(); 39 | } 40 | 41 | const setContentConfig = async (ctx) => { 42 | const { body } = ctx.request; 43 | try { 44 | const updatedConfig = await configureIndexingService.setContentConfig({config : body}); 45 | return updatedConfig; 46 | } catch (err) { 47 | ctx.throw(500, err); 48 | } 49 | } 50 | 51 | const getCollectionConfig = async (ctx) => { 52 | if (ctx.params.collectionname) 53 | return configureIndexingService.getCollectionConfig({collectionName: ctx.params.collectionname}) 54 | else 55 | return null; 56 | } 57 | 58 | return { 59 | getContentConfig, 60 | setContentConfig, 61 | getCollectionConfig, 62 | saveCollectionConfig, 63 | exportContentConfig, 64 | importContentConfig 65 | }; 66 | }; 67 | -------------------------------------------------------------------------------- /server/src/controllers/controller.js: -------------------------------------------------------------------------------- 1 | const controller = ({ strapi }) => ({ 2 | index(ctx) { 3 | ctx.body = strapi 4 | .plugin('strapi-plugin-elasticsearch') 5 | // the name of the service file & the method. 6 | .service('service') 7 | .getWelcomeMessage(); 8 | }, 9 | }); 10 | 11 | export default controller; 12 | -------------------------------------------------------------------------------- /server/src/controllers/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const configureIndexing = require('./configure-indexing'); 4 | const performSearch = require('./perform-search'); 5 | const logIndexing = require('./log-indexing'); 6 | const setupInfo = require('./setup-info'); 7 | const performIndexing = require('./perform-indexing'); 8 | 9 | module.exports = { 10 | configureIndexing, 11 | performSearch, 12 | logIndexing, 13 | setupInfo, 14 | performIndexing 15 | }; 16 | -------------------------------------------------------------------------------- /server/src/controllers/log-indexing.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = ({ strapi }) => { 3 | const logIndexingService = strapi.plugins['elasticsearch'].services.logIndexing; 4 | const fetchRecentRunsLog = async (ctx) => { 5 | return await logIndexingService.fetchIndexingLogs(); 6 | } 7 | 8 | return { 9 | fetchRecentRunsLog 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /server/src/controllers/perform-indexing.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | 5 | module.exports = ({ strapi }) => { 6 | const indexer = strapi.plugins['elasticsearch'].services.indexer; 7 | const scheduleIndexingService = strapi.plugins['elasticsearch'].services.scheduleIndexing; 8 | const rebuildIndex = async (ctx) => { 9 | return await indexer.rebuildIndex(); 10 | } 11 | 12 | const indexCollection = async (ctx) => { 13 | if (ctx.params.collectionname) 14 | return await scheduleIndexingService.addCollectionToIndex({collectionUid: ctx.params.collectionname}) 15 | else 16 | return null; 17 | } 18 | 19 | const triggerIndexingTask = async (ctx) => { 20 | return await indexer.indexPendingData() 21 | } 22 | 23 | return { 24 | rebuildIndex, 25 | indexCollection, 26 | triggerIndexingTask 27 | }; 28 | } -------------------------------------------------------------------------------- /server/src/controllers/perform-search.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const qs = require('qs'); 4 | 5 | module.exports = { 6 | search : async (ctx) => { 7 | try { 8 | const esInterface = strapi.plugins['elasticsearch'].services.esInterface; 9 | if (ctx.query.query) 10 | { 11 | const query = qs.parse(ctx.query.query); 12 | const resp = await esInterface.searchData(query); 13 | if (resp?.hits?.hits) 14 | { 15 | const filteredData = resp.hits.hits.filter(dt => dt._source !== null); 16 | const filteredMatches = filteredData.map((dt) => dt['_source']); 17 | ctx.body = filteredMatches; 18 | } 19 | else 20 | ctx.body = {} 21 | } 22 | else 23 | ctx.body = {} 24 | } catch (err) { 25 | ctx.response.status = 500; 26 | ctx.body = "An error was encountered while processing the search request." 27 | console.log('An error was encountered while processing the search request.') 28 | console.log(err); 29 | } 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /server/src/controllers/setup-info.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | 5 | module.exports = ({ strapi }) => { 6 | const helperService = strapi.plugins['elasticsearch'].services.helper; 7 | const getElasticsearchInfo = async (ctx) => { 8 | return helperService.getElasticsearchInfo(); 9 | } 10 | 11 | return { 12 | getElasticsearchInfo, 13 | }; 14 | } -------------------------------------------------------------------------------- /server/src/destroy.js: -------------------------------------------------------------------------------- 1 | const destroy = ({ strapi }) => { 2 | // destroy phase 3 | }; 4 | 5 | export default destroy; 6 | -------------------------------------------------------------------------------- /server/src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Application methods 3 | */ 4 | import bootstrap from './bootstrap'; 5 | import destroy from './destroy'; 6 | import register from './register'; 7 | 8 | /** 9 | * Plugin server methods 10 | */ 11 | import config from './config'; 12 | import contentTypes from './content-types'; 13 | import controllers from './controllers'; 14 | import middlewares from './middlewares'; 15 | import policies from './policies'; 16 | import routes from './routes'; 17 | import services from './services'; 18 | 19 | export default { 20 | bootstrap, 21 | destroy, 22 | register, 23 | 24 | config, 25 | controllers, 26 | contentTypes, 27 | middlewares, 28 | policies, 29 | routes, 30 | services, 31 | }; 32 | -------------------------------------------------------------------------------- /server/src/middlewares/index.js: -------------------------------------------------------------------------------- 1 | export default {}; 2 | -------------------------------------------------------------------------------- /server/src/policies/index.js: -------------------------------------------------------------------------------- 1 | export default {}; 2 | -------------------------------------------------------------------------------- /server/src/register.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const register = ({ strapi }) => { 4 | strapi.documents.use(async (context, next) => { 5 | const result = await next(); 6 | const scheduleIndexingService = strapi.plugins['elasticsearch'].services.scheduleIndexing; 7 | if (['create', 'update', 'delete', 'publish', 'unpublish'].includes(context.action) 8 | && strapi.elasticsearch.collections.includes(context.uid)) { 9 | console.log('Document services context : ', context.action, ' ', context.uid, ' ', context.params.documentId); 10 | if (context.contentType.options.draftAndPublish === true) { 11 | //publish, unpublish 12 | if (context.action === 'publish') { 13 | await scheduleIndexingService.addItemToIndex({ 14 | collectionUid: context.uid, 15 | recordId: context.params.documentId 16 | }); 17 | } 18 | else if (context.action === 'unpublish') { 19 | await scheduleIndexingService.removeItemFromIndex({ 20 | collectionUid: context.uid, 21 | recordId: context.params.documentId 22 | }); 23 | } 24 | } 25 | else { 26 | if (['create', 'update'].includes(context.action)) { 27 | await scheduleIndexingService.addItemToIndex({ 28 | collectionUid: context.uid, 29 | recordId: context.params.documentId 30 | }); 31 | } 32 | } 33 | if (context.action === 'delete') { 34 | await scheduleIndexingService.removeItemFromIndex({ 35 | collectionUid: context.uid, 36 | recordId: context.params.documentId 37 | }); 38 | } 39 | } 40 | return result; 41 | }); 42 | }; 43 | 44 | export default register; 45 | -------------------------------------------------------------------------------- /server/src/routes/configure-indexing.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // accessible only from admin UI 3 | type: 'admin', 4 | routes: [ 5 | { 6 | method: 'GET', 7 | path: '/content-config', 8 | handler: 'configureIndexing.getContentConfig', 9 | config: { policies: [] }, 10 | }, 11 | { 12 | method: 'GET', 13 | path: '/collection-config/:collectionname', 14 | handler: 'configureIndexing.getCollectionConfig', 15 | config: { policies: [] }, 16 | }, 17 | { 18 | method: 'POST', 19 | path: '/collection-config/:collectionname', 20 | handler: 'configureIndexing.saveCollectionConfig', 21 | config: { policies: [] }, 22 | }, 23 | { 24 | method: 'POST', 25 | path: '/content-config', 26 | handler: 'configureIndexing.setContentConfig', 27 | config: { policies: [] }, 28 | }, 29 | { 30 | method: 'GET', 31 | path: '/export-content-config', 32 | handler: 'configureIndexing.exportContentConfig', 33 | config: { policies: [] }, 34 | }, 35 | { 36 | method: 'POST', 37 | path: '/import-content-config', 38 | handler: 'configureIndexing.importContentConfig', 39 | config: { policies: [] }, 40 | }, 41 | ], 42 | }; -------------------------------------------------------------------------------- /server/src/routes/content-api.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | method: 'GET', 4 | path: '/', 5 | // name of the controller file & the method. 6 | handler: 'controller.index', 7 | config: { 8 | policies: [], 9 | }, 10 | }, 11 | ]; 12 | -------------------------------------------------------------------------------- /server/src/routes/index.js: -------------------------------------------------------------------------------- 1 | const configureIndexingRoutes = require('./configure-indexing'); 2 | const performSearch = require('./perform-search'); 3 | const runLog = require('./run-log'); 4 | const setupInfo = require('./setup-info'); 5 | const performIndexing = require('./perform-indexing'); 6 | 7 | module.exports = { 8 | config: configureIndexingRoutes, 9 | search: performSearch, 10 | runLog: runLog, 11 | setupInfo: setupInfo, 12 | performIndexing: performIndexing 13 | }; 14 | -------------------------------------------------------------------------------- /server/src/routes/perform-indexing.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // accessible only from admin UI 3 | type: 'admin', 4 | routes: [ 5 | { 6 | method: 'GET', 7 | path: '/reindex', 8 | handler: 'performIndexing.rebuildIndex', 9 | config: { policies: [] }, 10 | }, 11 | { 12 | method: 'GET', 13 | path: '/collection-reindex/:collectionname', 14 | handler: 'performIndexing.indexCollection', 15 | config: { policies: [] }, 16 | }, 17 | { 18 | method: 'GET', 19 | path: '/trigger-indexing/', 20 | handler: 'performIndexing.triggerIndexingTask', 21 | config: { policies: [] }, 22 | }, 23 | ], 24 | }; -------------------------------------------------------------------------------- /server/src/routes/perform-search.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // accessible only from admin UI 3 | type: 'content-api', 4 | routes: [ 5 | { 6 | method: 'GET', 7 | path: '/search', 8 | handler: 'performSearch.search', 9 | config: { 10 | policies: [] 11 | }, 12 | } 13 | ], 14 | }; -------------------------------------------------------------------------------- /server/src/routes/run-log.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // accessible only from admin UI 3 | type: 'admin', 4 | routes: [ 5 | { 6 | method: 'GET', 7 | path: '/indexing-run-log', 8 | handler: 'logIndexing.fetchRecentRunsLog', 9 | config: { policies: [] }, 10 | } 11 | ], 12 | }; -------------------------------------------------------------------------------- /server/src/routes/setup-info.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // accessible only from admin UI 3 | type: 'admin', 4 | routes: [ 5 | { 6 | method: 'GET', 7 | path: '/setup-info', 8 | handler: 'setupInfo.getElasticsearchInfo', 9 | config: { policies: [] }, 10 | } 11 | ], 12 | }; -------------------------------------------------------------------------------- /server/src/services/configure-indexing.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const getPluginStore = () => { 4 | return strapi.store({ 5 | environment: '', 6 | type: 'plugin', 7 | name: 'elasticsearch', 8 | }); 9 | } 10 | 11 | module.exports = ({ strapi }) => ({ 12 | async initializeStrapiElasticsearch() { 13 | await this.cacheConfig(); 14 | }, 15 | async markInitialized() { 16 | if (!strapi.elasticsearch) 17 | strapi.elasticsearch = {} 18 | strapi.elasticsearch.initialized = true; 19 | }, 20 | isInitialized() { 21 | return strapi.elasticsearch?.initialized || false 22 | }, 23 | async cacheConfig() { 24 | if (!strapi.elasticsearch) 25 | strapi.elasticsearch = {} 26 | strapi.elasticsearch.collectionsconfig = await this.getCollectionsConfiguredForIndexing(); 27 | strapi.elasticsearch.collections = await this.getCollectionsConfiguredForIndexing(); 28 | }, 29 | async getCollectionConfig({collectionName}) { 30 | const contentConfig = await this.getContentConfig(); 31 | if (Object.keys(contentConfig).includes(collectionName)) 32 | { 33 | const ob = {} 34 | ob[collectionName] = contentConfig[collectionName]; 35 | return ob; 36 | } 37 | else 38 | return null; 39 | }, 40 | async getCollectionsConfiguredForIndexing() { 41 | const contentConfig = await this.getContentConfig(); 42 | if (contentConfig) 43 | return Object.keys(contentConfig).filter((i) => { 44 | let hasAtleastOneIndexableAttribute = false; 45 | const attribs = Object.keys(contentConfig[i]) 46 | for (let k=0; k c.includes('api::')); 69 | const apiContentConfig = {}; 70 | for (let r = 0; r < apiContentTypes.length; r++) 71 | { 72 | apiContentConfig[apiContentTypes[r]] = {}; 73 | const collectionAttributes = contentTypes[apiContentTypes[r]].attributes 74 | const listOfAttributes = Object.keys(collectionAttributes).filter( 75 | (i) => fieldsToExclude.includes(i) === false 76 | ); 77 | 78 | for (let k = 0; k < listOfAttributes.length; k++) 79 | { 80 | const currentAttribute = listOfAttributes[k]; 81 | let attributeType = "regular"; 82 | if (typeof collectionAttributes[currentAttribute]["type"] !== "undefined" 83 | && collectionAttributes[currentAttribute]["type"] !== null) 84 | { 85 | if (collectionAttributes[currentAttribute]["type"] === "component") 86 | attributeType = "component" 87 | else if (collectionAttributes[currentAttribute]["type"] === "dynamiczone") 88 | attributeType = "dynamiczone" 89 | } 90 | apiContentConfig[apiContentTypes[r]][listOfAttributes[k]] = {index: false, 91 | type: attributeType} 92 | } 93 | 94 | } 95 | if (settings) 96 | { 97 | const objSettings = JSON.parse(settings); 98 | if (Object.keys(objSettings).includes('contentConfig')) 99 | { 100 | const collections = Object.keys(apiContentConfig); 101 | for (let r=0; r< collections.length; r++) 102 | { 103 | if (Object.keys(objSettings['contentConfig']).includes(collections[r])) 104 | { 105 | const attribsForCollection = Object.keys(apiContentConfig[collections[r]]) 106 | for (let s = 0; s < attribsForCollection.length; s++) 107 | { 108 | if (!Object.keys(objSettings['contentConfig'][collections[r]]).includes(attribsForCollection[s])) 109 | { 110 | objSettings['contentConfig'][collections[r]][attribsForCollection[s]] = {index: false, 111 | type: apiContentConfig[collections[r]][attribsForCollection[s]].type} 112 | } 113 | else 114 | { 115 | if (!Object.keys(objSettings['contentConfig'][collections[r]][attribsForCollection[s]]).includes('type')) 116 | objSettings['contentConfig'][collections[r]][attribsForCollection[s]]['type'] = apiContentConfig[collections[r]][attribsForCollection[s]].type 117 | } 118 | } 119 | } 120 | else 121 | objSettings['contentConfig'][collections[r]] = apiContentConfig[collections[r]] 122 | } 123 | return objSettings['contentConfig']; 124 | } 125 | else 126 | return apiContentConfig 127 | } 128 | else 129 | return apiContentConfig; 130 | }, 131 | async importContentConfig({config}){ 132 | const pluginStore = getPluginStore(); 133 | const settings = await pluginStore.get({ key: 'configsettings' }); 134 | if (settings) 135 | { 136 | const objSettings = JSON.parse(settings); 137 | objSettings['contentConfig'] = JSON.parse(config) 138 | const stringifySettings = JSON.stringify(objSettings); 139 | await pluginStore.set({ key: 'configsettings', value : stringifySettings }); 140 | } 141 | else 142 | { 143 | const newSettings = JSON.stringify({'contentConfig' : config}) 144 | await pluginStore.set({ key: 'configsettings', value : newSettings}); 145 | } 146 | const updatedSettings = await pluginStore.get({ key: 'configsettings' }); 147 | await this.cacheConfig(); 148 | if (updatedSettings && Object.keys(updatedSettings).includes('contentConfig')) 149 | return updatedSettings['contentConfig'] 150 | else 151 | return {}; 152 | }, 153 | async setContentConfig({collection, config}){ 154 | const pluginStore = getPluginStore(); 155 | const settings = await pluginStore.get({ key: 'configsettings' }); 156 | if (settings) 157 | { 158 | const objSettings = JSON.parse(settings); 159 | if (Object.keys(objSettings).includes('contentConfig')) 160 | { 161 | const prevConfig = objSettings['contentConfig']; 162 | const changedConfigKey = Object.keys(config)[0]; 163 | const newConfig = prevConfig; 164 | newConfig[changedConfigKey] = config[changedConfigKey] 165 | objSettings['contentConfig'] = newConfig 166 | } 167 | else 168 | objSettings['contentConfig'] = config; 169 | const stringifySettings = JSON.stringify(objSettings); 170 | await pluginStore.set({ key: 'configsettings', value : stringifySettings }); 171 | } 172 | else 173 | { 174 | const newSettings = JSON.stringify({'contentConfig' : config}) 175 | await pluginStore.set({ key: 'configsettings', value : newSettings}); 176 | } 177 | const updatedSettings = await pluginStore.get({ key: 'configsettings' }); 178 | await this.cacheConfig(); 179 | if (updatedSettings && Object.keys(updatedSettings).includes('contentConfig')) 180 | return updatedSettings['contentConfig'] 181 | else 182 | return {}; 183 | }, 184 | }); 185 | -------------------------------------------------------------------------------- /server/src/services/es-interface.js: -------------------------------------------------------------------------------- 1 | const { Client } = require('@elastic/elasticsearch') 2 | const fs = require('fs') 3 | const path = require('path'); 4 | 5 | 6 | 7 | let client = null; 8 | 9 | module.exports = ({ strapi }) => ({ 10 | async initializeSearchEngine({host, uname, password, cert}){ 11 | try 12 | { 13 | client = new Client({ 14 | node: host, 15 | auth: { 16 | username: uname, 17 | password: password 18 | }, 19 | tls: { 20 | ca: cert, 21 | rejectUnauthorized: false 22 | } 23 | }); 24 | } 25 | catch (err) 26 | { 27 | if (err.message.includes('ECONNREFUSED')) 28 | { 29 | console.error('strapi-plugin-elasticsearch : Connection to ElasticSearch at ', host, ' refused.') 30 | console.error(err); 31 | } 32 | else 33 | { 34 | console.error('strapi-plugin-elasticsearch : Error while initializing connection to ElasticSearch.') 35 | console.error(err); 36 | } 37 | throw(err); 38 | } 39 | }, 40 | async createIndex(indexName){ 41 | try{ 42 | const exists = await client.indices.exists({index: indexName}); 43 | if (!exists) 44 | { 45 | console.log('strapi-plugin-elasticsearch : Search index ', indexName, ' does not exist. Creating index.'); 46 | 47 | await client.indices.create({ 48 | index: indexName, 49 | }); 50 | } 51 | } 52 | catch (err) 53 | { 54 | if (err.message.includes('ECONNREFUSED')) 55 | { 56 | console.log('strapi-plugin-elasticsearch : Error while creating index - connection to ElasticSearch refused.') 57 | console.log(err); 58 | } 59 | else 60 | { 61 | console.log('strapi-plugin-elasticsearch : Error while creating index.') 62 | console.log(err); 63 | } 64 | } 65 | }, 66 | async deleteIndex(indexName){ 67 | try{ 68 | await client.indices.delete({ 69 | index: indexName 70 | }); 71 | } 72 | catch(err) 73 | { 74 | if (err.message.includes('ECONNREFUSED')) 75 | { 76 | console.log('strapi-plugin-elasticsearch : Connection to ElasticSearch refused.') 77 | console.log(err); 78 | } 79 | else 80 | { 81 | console.log('strapi-plugin-elasticsearch : Error while deleting index to ElasticSearch.') 82 | console.log(err); 83 | } 84 | } 85 | }, 86 | async attachAliasToIndex(indexName) { 87 | try{ 88 | const pluginConfig = await strapi.config.get('plugin::elasticsearch'); 89 | const aliasName = pluginConfig.indexAliasName; 90 | const aliasExists = await client.indices.existsAlias({name: aliasName}); 91 | if (aliasExists) 92 | { 93 | console.log('strapi-plugin-elasticsearch : Alias with this name already exists, removing it.'); 94 | await client.indices.deleteAlias({index: '*', name: aliasName}); 95 | } 96 | const indexExists = await client.indices.exists({index: indexName}); 97 | if (!indexExists) 98 | await this.createIndex(indexName); 99 | console.log('strapi-plugin-elasticsearch : Attaching the alias ', aliasName, ' to index : ', indexName); 100 | await client.indices.putAlias({index: indexName, name: aliasName}) 101 | } 102 | catch(err) 103 | { 104 | if (err.message.includes('ECONNREFUSED')) 105 | { 106 | console.log('strapi-plugin-elasticsearch : Attaching alias to the index - Connection to ElasticSearch refused.') 107 | console.log(err); 108 | } 109 | else 110 | { 111 | console.log('strapi-plugin-elasticsearch : Attaching alias to the index - Error while setting up alias within ElasticSearch.') 112 | console.log(err); 113 | } 114 | } 115 | }, 116 | async checkESConnection() { 117 | if (!client) 118 | return false; 119 | try { 120 | await client.ping(); 121 | return true; 122 | } 123 | catch(error) 124 | { 125 | console.error('strapi-plugin-elasticsearch : Could not connect to Elastic search.') 126 | console.error(error); 127 | return false; 128 | } 129 | 130 | }, 131 | async indexDataToSpecificIndex({itemId, itemData}, iName){ 132 | try 133 | { 134 | await client.index({ 135 | index: iName, 136 | id: itemId, 137 | document: itemData 138 | }) 139 | await client.indices.refresh({ index: iName }); 140 | } 141 | catch(err){ 142 | console.log('strapi-plugin-elasticsearch : Error encountered while indexing data to ElasticSearch.') 143 | console.log(err); 144 | throw err; 145 | } 146 | }, 147 | async indexData({itemId, itemData}) { 148 | const pluginConfig = await strapi.config.get('plugin::elasticsearch'); 149 | return await this.indexDataToSpecificIndex({itemId, itemData}, pluginConfig.indexAliasName); 150 | }, 151 | async removeItemFromIndex({itemId}) { 152 | const pluginConfig = await strapi.config.get('plugin::elasticsearch'); 153 | try 154 | { 155 | await client.delete({ 156 | index: pluginConfig.indexAliasName, 157 | id: itemId 158 | }); 159 | await client.indices.refresh({ index: pluginConfig.indexAliasName }); 160 | } 161 | catch(err){ 162 | if (err.meta.statusCode === 404) 163 | console.error('strapi-plugin-elasticsearch : The entry to be removed from the index already does not exist.') 164 | else 165 | { 166 | console.error('strapi-plugin-elasticsearch : Error encountered while removing indexed data from ElasticSearch.') 167 | throw err; 168 | } 169 | } 170 | }, 171 | async searchData(searchQuery){ 172 | try 173 | { 174 | const pluginConfig = await strapi.config.get('plugin::elasticsearch'); 175 | const result= await client.search({ 176 | index: pluginConfig.indexAliasName, 177 | ...searchQuery 178 | }); 179 | return result; 180 | } 181 | catch(err) 182 | { 183 | console.log('Search : elasticClient.searchData : Error encountered while making a search request to ElasticSearch.') 184 | throw err; 185 | } 186 | } 187 | }); -------------------------------------------------------------------------------- /server/src/services/helper.js: -------------------------------------------------------------------------------- 1 | ///START : via https://raw.githubusercontent.com/Barelydead/strapi-plugin-populate-deep/main/server/helpers/index.js 2 | 3 | const { isEmpty, merge } = require("lodash/fp"); 4 | const transformServiceProvider = require('./transform-content'); 5 | 6 | const getPluginStore = () => { 7 | return strapi.store({ 8 | environment: '', 9 | type: 'plugin', 10 | name: 'elasticsearch', 11 | }); 12 | } 13 | 14 | 15 | const getModelPopulationAttributes = (model) => { 16 | if (model.uid === "plugin::upload.file") { 17 | const { related, ...attributes } = model.attributes; 18 | return attributes; 19 | } 20 | 21 | return model.attributes; 22 | }; 23 | 24 | const getFullPopulateObject = (modelUid, maxDepth = 20, ignore) => { 25 | const skipCreatorFields = true; 26 | 27 | if (maxDepth <= 1) { 28 | return true; 29 | } 30 | if (modelUid === "admin::user" && skipCreatorFields) { 31 | return undefined; 32 | } 33 | 34 | const populate = {}; 35 | const model = strapi.getModel(modelUid); 36 | if (ignore && !ignore.includes(model.collectionName)) ignore.push(model.collectionName) 37 | for (const [key, value] of Object.entries( 38 | getModelPopulationAttributes(model) 39 | )) { 40 | if (ignore?.includes(key)) continue 41 | if (value) { 42 | if (value.type === "component") { 43 | populate[key] = getFullPopulateObject(value.component, maxDepth - 1); 44 | } else if (value.type === "dynamiczone") { 45 | const dynamicPopulate = value.components.reduce((prev, cur) => { 46 | const curPopulate = getFullPopulateObject(cur, maxDepth - 1); 47 | return curPopulate === true ? prev : merge(prev, curPopulate); 48 | }, {}); 49 | 50 | populate[key] = isEmpty(dynamicPopulate) ? true : { on: dynamicPopulate.populate }; 51 | } else if (value.type === "relation") { 52 | const relationPopulate = getFullPopulateObject( 53 | value.target, 54 | (key === 'localizations') && maxDepth > 2 ? 1 : maxDepth - 1, 55 | ignore 56 | ); 57 | if (relationPopulate) { 58 | populate[key] = relationPopulate; 59 | } 60 | } else if (value.type === "media") { 61 | populate[key] = true; 62 | } 63 | } 64 | } 65 | return isEmpty(populate) ? true : { populate }; 66 | }; 67 | 68 | ///END : via https://raw.githubusercontent.com/Barelydead/strapi-plugin-populate-deep/main/server/helpers/index.js 69 | 70 | const getPopulateObjectForComponent = (componentUid) => { 71 | const componentSchema = strapi.plugin('content-manager').service('components').findAllComponents().filter(c => c.uid === componentUid)[0]; 72 | const componentAttributes = componentSchema.attributes; 73 | const populate = {}; 74 | for (const attributeName of Object.keys(componentAttributes)) { 75 | const attribute = componentAttributes[attributeName]; 76 | if (attribute.type === 'component') { 77 | populate[attributeName] = getPopulateObjectForComponent(attribute.component) ; 78 | } 79 | else if (attribute.type === 'media') { 80 | populate[attributeName] = { fields: ['*'] }; 81 | } 82 | } 83 | return { populate }; 84 | } 85 | 86 | const getPopulateForACollection = (collectionUid) => { 87 | const collection = strapi.plugin('content-manager').service('content-types').findAllContentTypes().filter(c => c.uid === collectionUid)[0]; 88 | const selCollAttributes = collection.attributes; 89 | const populate = {}; 90 | const fields = []; 91 | for (const attributeName of Object.keys(selCollAttributes)) { 92 | const attribute = selCollAttributes[attributeName]; 93 | if (attribute.type === 'dynamiczone') { 94 | populate[attributeName] = { 95 | on: attribute.components.reduce((acc, componentUid) => { 96 | acc[componentUid] = getPopulateObjectForComponent(componentUid); 97 | return acc; 98 | }, {}) 99 | }; 100 | } else if (attribute.type === 'component') { 101 | populate[attributeName] = getPopulateObjectForComponent(attribute.component); 102 | } 103 | else if (attribute.type === 'media') { 104 | populate[attributeName] = { fields: ['*'] }; 105 | } 106 | else if (attribute.type === 'relation') { 107 | //do nothing since we currently don't support working with relations 108 | } 109 | else 110 | { 111 | fields.push(attributeName); 112 | } 113 | } 114 | return { populate, fields }; 115 | 116 | }; 117 | 118 | /* 119 | //Example config to cover extraction cases 120 | collectionConfig[collectionName] = { 121 | 'major' : {index: true}, 122 | 'sections' : { index: true, searchFieldName: 'information', 123 | 'subfields' : [ 124 | { 'component' : 'try.paragraph', 125 | 'field' : 'Text'}, 126 | { 'component' : 'try.paragraph', 127 | 'field' : 'Heading'}, 128 | { 'component' : 'try.footer', 129 | 'field' : 'footer_link', 130 | 'subfields' :[ { 131 | 'component' : 'try.link', 132 | 'field' : 'display_text' 133 | }] 134 | }] }, 135 | 'seo_details' : { 136 | index: true, searchFieldName: 'seo', 137 | 'subfields' : [ 138 | { 139 | 'component' : 'try.seo', 140 | 'field' : 'meta_description' 141 | } 142 | ] 143 | }, 144 | 'changelog' : { 145 | index: true, searchFieldName: 'breakdown', 146 | 'subfields' : [ 147 | { 148 | 'component' : 'try.revision', 149 | 'field' : 'summary' 150 | } 151 | ] 152 | } 153 | } 154 | */ 155 | function extractSubfieldData({config, data }) { 156 | let returnData = ''; 157 | if (data === null) 158 | return returnData; 159 | if (Array.isArray(data)) 160 | { 161 | const dynDataItems = data; 162 | for (let r=0; r< dynDataItems.length; r++) 163 | { 164 | const extractItem = dynDataItems[r]; 165 | for (let s=0; s ({ 229 | async getElasticsearchInfo() { 230 | const configureService = strapi.plugins['elasticsearch'].services.configureIndexing; 231 | const esInterface = strapi.plugins['elasticsearch'].services.esInterface; 232 | const pluginConfig = await strapi.config.get('plugin::elasticsearch'); 233 | 234 | const connected = pluginConfig.searchConnector && pluginConfig.searchConnector.host 235 | ? await esInterface.checkESConnection() : false; 236 | 237 | return { 238 | indexingCronSchedule : pluginConfig.indexingCronSchedule || "Not configured", 239 | elasticHost : pluginConfig.searchConnector ? 240 | pluginConfig.searchConnector.host || "Not configured" : "Not configured", 241 | elasticUserName : pluginConfig.searchConnector ? 242 | pluginConfig.searchConnector.username || "Not configured" : "Not configured", 243 | elasticCertificate : pluginConfig.searchConnector ? 244 | pluginConfig.searchConnector.certificate || "Not configured" : "Not configured", 245 | elasticIndexAlias : pluginConfig.indexAliasName || "Not configured", 246 | connected : connected, 247 | initialized : configureService.isInitialized() 248 | } 249 | }, 250 | isCollectionDraftPublish({collectionName}) { 251 | const model = strapi.getModel(collectionName); 252 | return model.attributes.publishedAt ? true : false 253 | }, 254 | getPopulateAttribute({collectionName}) { 255 | //TODO : We currently have set populate to upto 4 levels, should 256 | //this be configurable or a different default value? 257 | return getFullPopulateObject(collectionName, 4, []); 258 | }, 259 | getPopulateForACollection({collectionName}) { 260 | return getPopulateForACollection(collectionName); 261 | }, 262 | getIndexItemId ({collectionName, itemDocumentId}) { 263 | return collectionName+'::' + itemDocumentId; 264 | }, 265 | async getCurrentIndexName () { 266 | const pluginStore = getPluginStore(); 267 | const settings = await pluginStore.get({ key: 'configsettings' }); 268 | let indexName = 'strapi-plugin-elasticsearch-index_000001'; 269 | if (settings) 270 | { 271 | const objSettings = JSON.parse(settings); 272 | if (Object.keys(objSettings).includes('indexConfig')) 273 | { 274 | const idxConfig = objSettings['indexConfig']; 275 | indexName = idxConfig['name']; 276 | } 277 | } 278 | return indexName; 279 | }, 280 | async getIncrementedIndexName () { 281 | const currentIndexName = await this.getCurrentIndexName(); 282 | const number = parseInt(currentIndexName.split('index_')[1]); 283 | return 'strapi-plugin-elasticsearch-index_' + String(number+1).padStart(6,'0'); 284 | }, 285 | async storeCurrentIndexName (indexName) { 286 | const pluginStore = getPluginStore(); 287 | const settings = await pluginStore.get({ key: 'configsettings' }); 288 | if (settings) 289 | { 290 | const objSettings = JSON.parse(settings); 291 | objSettings['indexConfig'] = {'name' : indexName}; 292 | await pluginStore.set({ key: 'configsettings', value : JSON.stringify(objSettings)}); 293 | } 294 | else 295 | { 296 | const newSettings = JSON.stringify({'indexConfig' : {'name' : indexName}}) 297 | await pluginStore.set({ key: 'configsettings', value : newSettings}); 298 | } 299 | }, 300 | modifySubfieldsConfigForExtractor(collectionConfig) { 301 | const collectionName = Object.keys(collectionConfig)[0]; 302 | const attributes = Object.keys(collectionConfig[collectionName]); 303 | for (let r=0; r< attributes.length; r++) 304 | { 305 | const attr = attributes[r]; 306 | const attribFields = Object.keys(collectionConfig[collectionName][attr]); 307 | if (attribFields.includes('subfields')) 308 | { 309 | const subfielddata = collectionConfig[collectionName][attr]['subfields']; 310 | if (subfielddata.length > 0) 311 | { 312 | try { 313 | const subfieldjson = JSON.parse(subfielddata) 314 | if (Object.keys(subfieldjson).includes('subfields')) 315 | collectionConfig[collectionName][attr]['subfields'] = subfieldjson['subfields'] 316 | } 317 | catch(err) 318 | { 319 | continue; 320 | } 321 | } 322 | } 323 | } 324 | return collectionConfig; 325 | }, 326 | extractDataToIndex({collectionName, data, collectionConfig}) { 327 | collectionConfig = this.modifySubfieldsConfigForExtractor(collectionConfig); 328 | const fti = Object.keys(collectionConfig[collectionName]); 329 | const document = {} 330 | for (let k = 0; k < fti.length; k++) 331 | { 332 | const fieldConfig = collectionConfig[collectionName][fti[k]]; 333 | if (fieldConfig.index) 334 | { 335 | let val = null; 336 | if (Object.keys(fieldConfig).includes('subfields')) 337 | { 338 | val = extractSubfieldData({config: fieldConfig['subfields'], data: data[fti[k]]}) 339 | val = val ? val.trim() : val 340 | } 341 | else 342 | { 343 | val = data[fti[k]]; 344 | if (Object.keys(fieldConfig).includes('transform') && 345 | fieldConfig['transform'] === 'markdown') 346 | val = transformServiceProvider.transform({content: val, from: 'markdown'}); 347 | } 348 | 349 | if (Object.keys(fieldConfig).includes('searchFieldName')) 350 | document[fieldConfig['searchFieldName']] = val; 351 | else 352 | document[fti[k]] = val; 353 | } 354 | } 355 | return document; 356 | } 357 | }); -------------------------------------------------------------------------------- /server/src/services/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const configureIndexing = require('./configure-indexing'); 4 | const scheduleIndexing = require('./schedule-indexing'); 5 | const esInterface = require('./es-interface'); 6 | const indexer = require('./perform-indexing'); 7 | const logIndexing = require('./log-indexing'); 8 | const helper = require('./helper'); 9 | const transformContent = require('./transform-content'); 10 | 11 | module.exports = { 12 | configureIndexing, 13 | scheduleIndexing, 14 | esInterface, 15 | indexer, 16 | logIndexing, 17 | helper, 18 | transformContent 19 | }; 20 | -------------------------------------------------------------------------------- /server/src/services/log-indexing.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ strapi }) => ({ 2 | async recordIndexingPass(message) { 3 | const entry = await strapi.documents('plugin::elasticsearch.indexing-log').create({ 4 | data : { 5 | status: 'pass', 6 | details: message 7 | } 8 | }); 9 | }, 10 | async recordIndexingFail(message) { 11 | const entry = await strapi.documents('plugin::elasticsearch.indexing-log').create({ 12 | data : { 13 | status: 'fail', 14 | details: String(message) 15 | } 16 | }); 17 | }, 18 | async fetchIndexingLogs(count = 50) { 19 | const records = await strapi.documents('plugin::elasticsearch.indexing-log').findMany({ 20 | sort: { createdAt: 'DESC' }, 21 | start: 0, 22 | limit: count 23 | }); 24 | return records; 25 | } 26 | }); -------------------------------------------------------------------------------- /server/src/services/perform-indexing.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = ({ strapi }) => ({ 3 | 4 | async rebuildIndex() { 5 | const helper = strapi.plugins['elasticsearch'].services.helper; 6 | const esInterface = strapi.plugins['elasticsearch'].services.esInterface; 7 | const scheduleIndexingService = strapi.plugins['elasticsearch'].services.scheduleIndexing; 8 | const configureIndexingService = strapi.plugins['elasticsearch'].services.configureIndexing; 9 | const logIndexingService = strapi.plugins['elasticsearch'].services.logIndexing; 10 | 11 | try 12 | { 13 | console.log('strapi-plugin-elasticsearch : Request to rebuild the index received.') 14 | const oldIndexName = await helper.getCurrentIndexName(); 15 | console.log('strapi-plugin-elasticsearch : Recording the previous index name : ', oldIndexName); 16 | 17 | //Step 1 : Create a new index 18 | const newIndexName = await helper.getIncrementedIndexName(); 19 | await esInterface.createIndex(newIndexName); 20 | console.log('strapi-plugin-elasticsearch : Created new index with name : ', newIndexName); 21 | 22 | 23 | //Step 2 : Index all the stuff on this new index 24 | console.log('strapi-plugin-elasticsearch- : Starting to index all data into the new index.'); 25 | const item = await scheduleIndexingService.addFullSiteIndexingTask(); 26 | if (item.documentId) 27 | { 28 | const cols = await configureIndexingService.getCollectionsConfiguredForIndexing(); 29 | for (let r=0; r< cols.length; r++) 30 | await this.indexCollection(cols[r], newIndexName); 31 | 32 | await scheduleIndexingService.markIndexingTaskComplete(item.documentId); 33 | 34 | console.log('strapi-plugin-elasticsearch : Indexing of data into the new index complete.'); 35 | //Step 4 : Move the alias to this new index 36 | await esInterface.attachAliasToIndex(newIndexName);; 37 | console.log('strapi-plugin-elasticsearch : Attaching the newly created index to the alias.') 38 | //Step 3 : Update the search-indexing-name 39 | await helper.storeCurrentIndexName(newIndexName); 40 | 41 | console.log('strapi-plugin-elasticsearch : Deleting the previous index : ', oldIndexName); 42 | //Step 5 : Delete the previous index 43 | await esInterface.deleteIndex(oldIndexName) 44 | await logIndexingService.recordIndexingPass('Request to immediately re-index site-wide content completed successfully.'); 45 | 46 | return true; 47 | } 48 | else 49 | { 50 | await logIndexingService.recordIndexingFail('An error was encountered while trying site-wide re-indexing of content.'); 51 | return false; 52 | } 53 | } 54 | catch(err) 55 | { 56 | console.log('strapi-plugin-elasticsearch : searchController : An error was encountered while re-indexing.') 57 | console.log(err); 58 | await logIndexingService.recordIndexingFail(err); 59 | } 60 | }, 61 | async indexCollection(collectionName, indexName = null) { 62 | const helper = strapi.plugins['elasticsearch'].services.helper; 63 | const populateAttrib = helper.getPopulateForACollection({collectionName}); 64 | const isCollectionDraftPublish = helper.isCollectionDraftPublish({collectionName}); 65 | const configureIndexingService = strapi.plugins['elasticsearch'].services.configureIndexing; 66 | const esInterface = strapi.plugins['elasticsearch'].services.esInterface; 67 | if (indexName === null) 68 | indexName = await helper.getCurrentIndexName(); 69 | let entries = []; 70 | if (isCollectionDraftPublish) 71 | { 72 | entries = await strapi.documents(collectionName).findMany({ 73 | sort: { createdAt: 'DESC' }, 74 | populate: populateAttrib['populate'], 75 | status: 'published' 76 | }); 77 | } 78 | else 79 | { 80 | entries = await strapi.documents(collectionName).findMany({ 81 | sort: { createdAt: 'DESC' }, 82 | populate: populateAttrib['populate'], 83 | }); 84 | } 85 | if (entries) 86 | { 87 | for (let s=0; s r.full_site_indexing === true).length > 0 108 | if (fullSiteIndexing) 109 | { 110 | await this.rebuildIndex(); 111 | for (let r=0; r< recs.length; r++) 112 | await scheduleIndexingService.markIndexingTaskComplete(recs[r].documentId); 113 | } 114 | else 115 | { 116 | try 117 | { 118 | let fullCollectionIndexing = false; 119 | for (let r=0; r< recs.length; r++) 120 | { 121 | const col = recs[r].collection_name; 122 | if (configureIndexingService.isCollectionConfiguredToBeIndexed(col)) 123 | { 124 | //Indexing the individual item 125 | if (recs[r].item_document_id) 126 | { 127 | if (recs[r].indexing_type !== 'remove-from-index') 128 | { 129 | const populateAttrib = helper.getPopulateForACollection({collectionName: col}); 130 | const item = await strapi.documents(col).findOne({ 131 | documentId: recs[r].item_document_id, 132 | populate: populateAttrib['populate'] 133 | }); 134 | const indexItemId = helper.getIndexItemId({collectionName: col, itemDocumentId: item.documentId}); 135 | const collectionConfig = await configureIndexingService.getCollectionConfig({collectionName: col}) 136 | const dataToIndex = await helper.extractDataToIndex({ 137 | collectionName: col, data: item, collectionConfig 138 | }); 139 | await esInterface.indexData({itemId : indexItemId, itemData: dataToIndex}); 140 | await scheduleIndexingService.markIndexingTaskCompleteByItemDocumentId(recs[r].item_document_id); 141 | } 142 | else 143 | { 144 | const indexItemId = helper.getIndexItemId({collectionName: col, itemDocumentId: recs[r].item_document_id}) 145 | await esInterface.removeItemFromIndex({itemId : indexItemId}); 146 | await scheduleIndexingService.markIndexingTaskCompleteByItemDocumentId(recs[r].item_document_id); 147 | } 148 | } 149 | else //index the entire collection 150 | { 151 | //PENDING : Index an entire collection 152 | await this.indexCollection(col); 153 | await scheduleIndexingService.markIndexingTaskComplete(recs[r].documentId); 154 | await logIndexingService.recordIndexingPass('Indexing of collection ' + col + ' complete.'); 155 | fullCollectionIndexing = true; 156 | } 157 | } 158 | else 159 | await scheduleIndexingService.markIndexingTaskComplete(recs[r].documentId); 160 | } 161 | if (fullCollectionIndexing === false || (fullCollectionIndexing === true && recs.length > 1)) 162 | await logIndexingService.recordIndexingPass('Indexing of ' + String(recs.length) + ' records complete.'); 163 | } 164 | catch(err) 165 | { 166 | await logIndexingService.recordIndexingFail('Indexing of records failed - ' + ' ' + String(err)); 167 | console.log(err); 168 | return false; 169 | } 170 | } 171 | return true; 172 | }, 173 | }); -------------------------------------------------------------------------------- /server/src/services/schedule-indexing.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | module.exports = ({ strapi }) => ({ 4 | async addFullSiteIndexingTask () { 5 | const data = await strapi.documents('plugin::elasticsearch.task').create({ 6 | data : { 7 | collection_name: '', 8 | indexing_status: 'to-be-done', 9 | full_site_indexing: true, 10 | indexing_type: "add-to-index" 11 | } 12 | }); 13 | return data; 14 | }, 15 | async addCollectionToIndex({collectionUid}) { 16 | const data = await strapi.documents('plugin::elasticsearch.task').create({ 17 | data : { 18 | collection_name: collectionUid, 19 | indexing_status: 'to-be-done', 20 | full_site_indexing: false, 21 | indexing_type: "add-to-index" 22 | } 23 | }); 24 | return data; 25 | }, 26 | async addItemToIndex({collectionUid, recordId}) { 27 | const data = await strapi.documents('plugin::elasticsearch.task').create({ 28 | data : { 29 | item_document_id: recordId, 30 | collection_name: collectionUid, 31 | indexing_status: 'to-be-done', 32 | full_site_indexing: false, 33 | indexing_type: "add-to-index" 34 | } 35 | }); 36 | return data; 37 | }, 38 | async removeItemFromIndex({collectionUid, recordId}) { 39 | const data = await strapi.documents('plugin::elasticsearch.task').create({ 40 | data : { 41 | item_document_id: recordId, 42 | collection_name: collectionUid, 43 | indexing_status: 'to-be-done', 44 | full_site_indexing: false, 45 | indexing_type: "remove-from-index" 46 | } 47 | }); 48 | }, 49 | async getItemsPendingToBeIndexed(){ 50 | const entries = await strapi.documents('plugin::elasticsearch.task').findMany({ 51 | filters: { indexing_status : 'to-be-done'}, 52 | }); 53 | return entries; 54 | }, 55 | async markIndexingTaskComplete (recId) { 56 | await strapi.documents('plugin::elasticsearch.task').update({ 57 | documentId: recId, 58 | data : { 59 | 'indexing_status' : 'done' 60 | } 61 | }); 62 | }, 63 | async markIndexingTaskCompleteByItemDocumentId (recId) { 64 | const itemsToUpdate = await strapi.documents('plugin::elasticsearch.task').findMany({ 65 | filters: { 66 | item_document_id : recId, 67 | indexing_status : 'to-be-done' 68 | } 69 | }); 70 | if (itemsToUpdate.length > 0) { 71 | for (let k = 0; k ({ 2 | getWelcomeMessage() { 3 | return 'Welcome to Strapi 🚀'; 4 | }, 5 | }); 6 | 7 | export default service; 8 | -------------------------------------------------------------------------------- /server/src/services/transform-content.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const markdownToTxt = require('markdown-to-txt'); 4 | 5 | function transformMarkdownToText(md) { 6 | let text = md; 7 | try { 8 | text = markdownToTxt(md); 9 | } 10 | catch(err) { 11 | console.error('strapi-plugin-elasticsearch : Error while transforming markdown to text.'); 12 | console.error(err); 13 | } 14 | return text; 15 | } 16 | 17 | module.exports = { 18 | transform({content, from}) { 19 | if (from === 'markdown') 20 | return transformMarkdownToText(content); 21 | else 22 | return from; 23 | }, 24 | }; --------------------------------------------------------------------------------