diff --git a/README.md b/README.md index 1f9548e..a04bae3 100644 --- a/README.md +++ b/README.md @@ -39,34 +39,48 @@ php flarum queue:work When you need to rebuild the full index (e.g. after a mapping change): ```bash -# 1. Build into a staging index — live index is untouched +# Simple rebuild — promotes automatically once all jobs are queued php flarum blomstra:search:index build -# 2. Drain the queue -php flarum queue:work --stop-when-empty - -# 3a. Promote the new index to live -php flarum blomstra:search:index promote -# 3b: promote but keep the old index as a backup for rollback -php flarum blomstra:search:index promote --keep-backup - -# 4. Add content added between 'build' and 'promote' -php flarum blomstra:search:index fill - +# Or keep a backup of the old index in case you need to roll back +php flarum blomstra:search:index build --keep-backup ``` -If you kept the backup and want to roll back: +After the queue drains, fill any gaps from content posted during the build: + +```bash +php flarum blomstra:search:index fill +``` + +If you kept a backup and want to roll back: ```bash php flarum blomstra:search:index rollback ``` -Once you are satisfied with the new index, drop the backup: +Once satisfied with the new index, drop the backup: ```bash php flarum blomstra:search:index discard --backup ``` +### Blue-green rebuild (manual promotion) + +Use `--staging` to keep the old index live until you explicitly promote: + +```bash +# 1. Build into a staging index — live index is untouched +php flarum blomstra:search:index build --staging + +# 2. Drain the queue +php flarum queue:work --stop-when-empty + +# 3. Promote the staging index to live +php flarum blomstra:search:index promote +# Or keep the old index as a backup: +php flarum blomstra:search:index promote --keep-backup +``` + ### Resuming or cancelling an interrupted build If a build is interrupted, re-run it with the appropriate flag: @@ -102,14 +116,16 @@ php flarum blomstra:search:index mapping | Command | Description | |---|---| -| `build` | Build into a new timestamped staging index. On first install, aliases it immediately so search is live during seeding. On subsequent runs, use `promote` when the queue is drained. | +| `build` | Rebuild the index and promote automatically once all jobs are queued. On first install, aliases immediately so search is live during seeding. | +| `build --keep-backup` | Rebuild and promote, retaining the old index as a backup for rollback. | +| `build --staging` | Build into a staging index without promoting — use `promote` when ready (blue-green workflow). | | `build --resume` | Resume an interrupted build from where each seeder left off. | | `build --fresh` | Drop the staging index and start completely fresh. | -| `promote` | Atomically swap the alias from the live index to the completed staging index. Prompts for confirmation. | +| `promote` | Atomically swap the alias to the staging index. Prompts for confirmation (blue-green workflow). | | `promote --keep-backup` | Promote and retain the replaced live index as a backup for rollback. | -| `rollback` | Restore the backup index to live (after `promote --keep-backup`). Deletes the index that was live. | +| `rollback` | Restore the backup index to live. Deletes the index that was live. | | `discard --pending` | Drop the staging index without promoting (cancels an in-progress build). | -| `discard --backup` | Drop the backup index (cleanup after `promote --keep-backup`). | +| `discard --backup` | Drop the backup index (cleanup after `--keep-backup`). | | `mapping` | Push updated mapping to the live index without rebuilding or reseeding. | | `fill` | Seed only documents missing from the live index. | | `build --only=discussions` | Seed only the specified document type (`discussions` or `posts`). | diff --git a/extend.php b/extend.php index 0ff9a15..457aa34 100644 --- a/extend.php +++ b/extend.php @@ -20,7 +20,8 @@ return [ (new Flarum\Frontend('forum')) ->js(__DIR__.'/js/dist/forum.js'), (new Flarum\Frontend('admin')) - ->js(__DIR__.'/js/dist/admin.js'), + ->js(__DIR__.'/js/dist/admin.js') + ->css(__DIR__.'/resources/less/admin.less'), new Flarum\Locales(__DIR__.'/resources/locale'), diff --git a/js/dist/admin.js b/js/dist/admin.js index 81a5c50..4ec3e4a 100644 --- a/js/dist/admin.js +++ b/js/dist/admin.js @@ -1,2 +1,2 @@ -(()=>{var a={n:e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return a.d(t,{a:t}),t},d:(e,t)=>{for(var r in t)a.o(t,r)&&!a.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},o:(a,e)=>Object.prototype.hasOwnProperty.call(a,e),r:a=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(a,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(a,"__esModule",{value:!0})}},e={};(()=>{"use strict";a.r(e);const t=flarum.core.compat["admin/app"];var r=a.n(t);r().initializers.add("blomstra-search",(function(){var a=new Map;["arabic","armenian","basque","bengali","brazilian","bulgarian","catalan","cjk","czech","danish","dutch","english","estonian","finnish","french","galician","german","greek","hindi","hungarian","indonesian","irish","italian","latvian","lithuanian","norwegian","persian","portuguese","romanian","russian","sorani","spanish","swedish","turkish","thai"].forEach((function(e){a.set(e,e)})),r().extensionData.for("blomstra-search").registerSetting({setting:"blomstra-search.elastic-endpoint",label:r().translator.trans("blomstra-search.admin.elastic-endpoint"),type:"input"}).registerSetting({setting:"blomstra-search.elastic-username",label:r().translator.trans("blomstra-search.admin.elastic-username"),type:"input"}).registerSetting({setting:"blomstra-search.elastic-password",label:r().translator.trans("blomstra-search.admin.elastic-password"),type:"password"}).registerSetting({setting:"blomstra-search.elastic-index",label:r().translator.trans("blomstra-search.admin.elastic-index"),default:"flarum",type:"input"}).registerSetting({setting:"blomstra-search.analyzer-language",label:r().translator.trans("blomstra-search.admin.analyzer.label"),help:r().translator.trans("blomstra-search.admin.analyzer.help"),type:"select",options:Object.fromEntries(a.entries()),default:"english"}).registerSetting({setting:"blomstra-search.elastic-index",label:r().translator.trans("blomstra-search.admin.elastic-index"),default:"flarum",type:"input"}).registerSetting({setting:"blomstra-search.search-discussion-subjects",label:r().translator.trans("blomstra-search.admin.search-discussion-subjects"),type:"switch"}).registerSetting({setting:"blomstra-search.search-post-bodies",label:r().translator.trans("blomstra-search.admin.search-post-bodies"),type:"switch"})}))})(),module.exports=e})(); +(()=>{var t={n:a=>{var e=a&&a.__esModule?()=>a.default:()=>a;return t.d(e,{a:e}),e},d:(a,e)=>{for(var r in e)t.o(e,r)&&!t.o(a,r)&&Object.defineProperty(a,r,{enumerable:!0,get:e[r]})},o:(t,a)=>Object.prototype.hasOwnProperty.call(t,a),r:t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})}},a={};(()=>{"use strict";function e(t,a){return e=Object.setPrototypeOf||function(t,a){return t.__proto__=a,t},e(t,a)}t.r(a);const r=flarum.core.compat["admin/app"];var n=t.n(r);const s=flarum.core.compat["common/extend"],i=flarum.core.compat["admin/components/DashboardPage"];var o=t.n(i);const l=flarum.core.compat["admin/components/DashboardWidget"];var c=t.n(l);const d=flarum.core.compat["common/components/Alert"];var p=t.n(d),u=function(t){var a,r;function s(){return t.apply(this,arguments)||this}r=t,(a=s).prototype=Object.create(r.prototype),a.prototype.constructor=a,e(a,r);var i=s.prototype;return i.className=function(){return"ReindexWarningWidget"},i.content=function(){return m(p(),{type:"warning",dismissible:!1,icon:"fas fa-exclamation-triangle",title:n().translator.trans("blomstra-search.admin.reindex-required.title")},n().translator.trans("blomstra-search.admin.reindex-required.detail"))},s}(c());n().initializers.add("blomstra-search",(function(){var t=n().data.settings["blomstra-search.active-index"],a=n().data.settings["blomstra-search.index-compatible"];t&&"v2"!==a&&(0,s.extend)(o().prototype,"availableWidgets",(function(t){t.add("blomstra-search-reindex",m(u),110)}));var e=new Map;["arabic","armenian","basque","bengali","brazilian","bulgarian","catalan","cjk","czech","danish","dutch","english","estonian","finnish","french","galician","german","greek","hindi","hungarian","indonesian","irish","italian","latvian","lithuanian","norwegian","persian","portuguese","romanian","russian","sorani","spanish","swedish","turkish","thai"].forEach((function(t){e.set(t,t)})),n().extensionData.for("blomstra-search").registerSetting({setting:"blomstra-search.elastic-endpoint",label:n().translator.trans("blomstra-search.admin.elastic-endpoint"),type:"input"}).registerSetting({setting:"blomstra-search.elastic-username",label:n().translator.trans("blomstra-search.admin.elastic-username"),type:"input"}).registerSetting({setting:"blomstra-search.elastic-password",label:n().translator.trans("blomstra-search.admin.elastic-password"),type:"password"}).registerSetting({setting:"blomstra-search.elastic-index",label:n().translator.trans("blomstra-search.admin.elastic-index"),default:"flarum",type:"input"}).registerSetting({setting:"blomstra-search.analyzer-language",label:n().translator.trans("blomstra-search.admin.analyzer.label"),help:n().translator.trans("blomstra-search.admin.analyzer.help"),type:"select",options:Object.fromEntries(e.entries()),default:"english"}).registerSetting({setting:"blomstra-search.search-discussion-subjects",label:n().translator.trans("blomstra-search.admin.search-discussion-subjects"),type:"switch"}).registerSetting({setting:"blomstra-search.search-post-bodies",label:n().translator.trans("blomstra-search.admin.search-post-bodies"),type:"switch"})}))})(),module.exports=a})(); //# sourceMappingURL=admin.js.map \ No newline at end of file diff --git a/js/dist/admin.js.map b/js/dist/admin.js.map index 9a2fe66..9b7408c 100644 --- a/js/dist/admin.js.map +++ b/js/dist/admin.js.map @@ -1 +1 @@ -{"version":3,"file":"admin.js","mappings":"MACA,IAAIA,EAAsB,CCA1BA,EAAyBC,IACxB,IAAIC,EAASD,GAAUA,EAAOE,WAC7B,IAAOF,EAAiB,QACxB,IAAM,EAEP,OADAD,EAAoBI,EAAEF,EAAQ,CAAEG,EAAGH,IAC5BA,CAAM,ECLdF,EAAwB,CAACM,EAASC,KACjC,IAAI,IAAIC,KAAOD,EACXP,EAAoBS,EAAEF,EAAYC,KAASR,EAAoBS,EAAEH,EAASE,IAC5EE,OAAOC,eAAeL,EAASE,EAAK,CAAEI,YAAY,EAAMC,IAAKN,EAAWC,IAE1E,ECNDR,EAAwB,CAACc,EAAKC,IAAUL,OAAOM,UAAUC,eAAeC,KAAKJ,EAAKC,GCClFf,EAAyBM,IACH,oBAAXa,QAA0BA,OAAOC,aAC1CV,OAAOC,eAAeL,EAASa,OAAOC,YAAa,CAAEC,MAAO,WAE7DX,OAAOC,eAAeL,EAAS,aAAc,CAAEe,OAAO,GAAO,G,+BCL9D,MAAM,EAA+BC,OAAOC,KAAKC,OAAO,a,aCExDC,IAAAA,aAAAA,IAAqB,mBAAmB,WACtC,IAAMC,EAAY,IAAIC,IACtB,CACE,SACA,WACA,SACA,UACA,YACA,YACA,UACA,MACA,QACA,SACA,QACA,UACA,WACA,UACA,SACA,WACA,SACA,QACA,QACA,YACA,aACA,QACA,UACA,UACA,aACA,YACA,UACA,aACA,WACA,UACA,SACA,UACA,UACA,UACA,QACAC,SAAQ,SAACC,GACTH,EAAUI,IAAID,EAAUA,EACzB,IAEDJ,IAAAA,cAAAA,IACO,mBACJM,gBAAgB,CACfC,QAAS,mCACTC,MAAOR,IAAAA,WAAAA,MAAqB,0CAC5BS,KAAM,UAEPH,gBAAgB,CACfC,QAAS,mCACTC,MAAOR,IAAAA,WAAAA,MAAqB,0CAC5BS,KAAM,UAEPH,gBAAgB,CACfC,QAAS,mCACTC,MAAOR,IAAAA,WAAAA,MAAqB,0CAC5BS,KAAM,aAEPH,gBAAgB,CACfC,QAAS,gCACTC,MAAOR,IAAAA,WAAAA,MAAqB,uCAC5BU,QAAS,SACTD,KAAM,UAEPH,gBAAgB,CACfC,QAAS,oCACTC,MAAOR,IAAAA,WAAAA,MAAqB,wCAC5BW,KAAMX,IAAAA,WAAAA,MAAqB,uCAC3BS,KAAM,SACNG,QAAS3B,OAAO4B,YAAYZ,EAAUa,WACtCJ,QAAS,YAEVJ,gBAAgB,CACfC,QAAS,gCACTC,MAAOR,IAAAA,WAAAA,MAAqB,uCAC5BU,QAAS,SACTD,KAAM,UAEPH,gBAAgB,CACfC,QAAS,6CACTC,MAAOR,IAAAA,WAAAA,MAAqB,oDAC5BS,KAAM,WAEPH,gBAAgB,CACfC,QAAS,qCACTC,MAAOR,IAAAA,WAAAA,MAAqB,4CAC5BS,KAAM,UAEX,G","sources":["webpack://@blomstra/search/webpack/bootstrap","webpack://@blomstra/search/webpack/runtime/compat get default export","webpack://@blomstra/search/webpack/runtime/define property getters","webpack://@blomstra/search/webpack/runtime/hasOwnProperty shorthand","webpack://@blomstra/search/webpack/runtime/make namespace object","webpack://@blomstra/search/external root \"flarum.core.compat['admin/app']\"","webpack://@blomstra/search/./src/admin/index.ts"],"sourcesContent":["// The require scope\nvar __webpack_require__ = {};\n\n","// getDefaultExport function for compatibility with non-harmony modules\n__webpack_require__.n = (module) => {\n\tvar getter = module && module.__esModule ?\n\t\t() => (module['default']) :\n\t\t() => (module);\n\t__webpack_require__.d(getter, { a: getter });\n\treturn getter;\n};","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['admin/app'];","import app from 'flarum/admin/app';\n\napp.initializers.add('blomstra-search', () => {\n const languages = new Map();\n [\n 'arabic',\n 'armenian',\n 'basque',\n 'bengali',\n 'brazilian',\n 'bulgarian',\n 'catalan',\n 'cjk',\n 'czech',\n 'danish',\n 'dutch',\n 'english',\n 'estonian',\n 'finnish',\n 'french',\n 'galician',\n 'german',\n 'greek',\n 'hindi',\n 'hungarian',\n 'indonesian',\n 'irish',\n 'italian',\n 'latvian',\n 'lithuanian',\n 'norwegian',\n 'persian',\n 'portuguese',\n 'romanian',\n 'russian',\n 'sorani',\n 'spanish',\n 'swedish',\n 'turkish',\n 'thai',\n ].forEach((language) => {\n languages.set(language, language);\n });\n\n app.extensionData\n .for('blomstra-search')\n .registerSetting({\n setting: 'blomstra-search.elastic-endpoint',\n label: app.translator.trans('blomstra-search.admin.elastic-endpoint'),\n type: 'input',\n })\n .registerSetting({\n setting: 'blomstra-search.elastic-username',\n label: app.translator.trans('blomstra-search.admin.elastic-username'),\n type: 'input',\n })\n .registerSetting({\n setting: 'blomstra-search.elastic-password',\n label: app.translator.trans('blomstra-search.admin.elastic-password'),\n type: 'password',\n })\n .registerSetting({\n setting: 'blomstra-search.elastic-index',\n label: app.translator.trans('blomstra-search.admin.elastic-index'),\n default: 'flarum',\n type: 'input',\n })\n .registerSetting({\n setting: 'blomstra-search.analyzer-language',\n label: app.translator.trans('blomstra-search.admin.analyzer.label'),\n help: app.translator.trans('blomstra-search.admin.analyzer.help'),\n type: 'select',\n options: Object.fromEntries(languages.entries()),\n default: 'english',\n })\n .registerSetting({\n setting: 'blomstra-search.elastic-index',\n label: app.translator.trans('blomstra-search.admin.elastic-index'),\n default: 'flarum',\n type: 'input',\n })\n .registerSetting({\n setting: 'blomstra-search.search-discussion-subjects',\n label: app.translator.trans('blomstra-search.admin.search-discussion-subjects'),\n type: 'switch',\n })\n .registerSetting({\n setting: 'blomstra-search.search-post-bodies',\n label: app.translator.trans('blomstra-search.admin.search-post-bodies'),\n type: 'switch',\n });\n});\n"],"names":["__webpack_require__","module","getter","__esModule","d","a","exports","definition","key","o","Object","defineProperty","enumerable","get","obj","prop","prototype","hasOwnProperty","call","Symbol","toStringTag","value","flarum","core","compat","app","languages","Map","forEach","language","set","registerSetting","setting","label","type","default","help","options","fromEntries","entries"],"sourceRoot":""} \ No newline at end of file +{"version":3,"file":"admin.js","mappings":"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","sources":["webpack://@blomstra/search/webpack/bootstrap","webpack://@blomstra/search/webpack/runtime/compat get default export","webpack://@blomstra/search/webpack/runtime/define property getters","webpack://@blomstra/search/webpack/runtime/hasOwnProperty shorthand","webpack://@blomstra/search/webpack/runtime/make namespace object","webpack://@blomstra/search/./node_modules/@babel/runtime/helpers/esm/setPrototypeOf.js","webpack://@blomstra/search/external root \"flarum.core.compat['admin/app']\"","webpack://@blomstra/search/external root \"flarum.core.compat['common/extend']\"","webpack://@blomstra/search/external root \"flarum.core.compat['admin/components/DashboardPage']\"","webpack://@blomstra/search/external root \"flarum.core.compat['admin/components/DashboardWidget']\"","webpack://@blomstra/search/external root \"flarum.core.compat['common/components/Alert']\"","webpack://@blomstra/search/./src/admin/index.ts","webpack://@blomstra/search/./node_modules/@babel/runtime/helpers/esm/inheritsLoose.js"],"sourcesContent":["// The require scope\nvar __webpack_require__ = {};\n\n","// getDefaultExport function for compatibility with non-harmony modules\n__webpack_require__.n = (module) => {\n\tvar getter = module && module.__esModule ?\n\t\t() => (module['default']) :\n\t\t() => (module);\n\t__webpack_require__.d(getter, { a: getter });\n\treturn getter;\n};","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","export default function _setPrototypeOf(o, p) {\n _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) {\n o.__proto__ = p;\n return o;\n };\n\n return _setPrototypeOf(o, p);\n}","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['admin/app'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/extend'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['admin/components/DashboardPage'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['admin/components/DashboardWidget'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/components/Alert'];","import app from 'flarum/admin/app';\nimport { extend } from 'flarum/common/extend';\nimport DashboardPage from 'flarum/admin/components/DashboardPage';\nimport DashboardWidget from 'flarum/admin/components/DashboardWidget';\nimport Alert from 'flarum/common/components/Alert';\n\nconst REQUIRED_INDEX_COMPAT = 'v2';\n\nclass ReindexWarningWidget extends DashboardWidget {\n className() {\n return 'ReindexWarningWidget';\n }\n\n content() {\n return m(Alert, {\n type: 'warning',\n dismissible: false,\n icon: 'fas fa-exclamation-triangle',\n title: app.translator.trans('blomstra-search.admin.reindex-required.title'),\n }, app.translator.trans('blomstra-search.admin.reindex-required.detail'));\n }\n}\n\napp.initializers.add('blomstra-search', () => {\n const activeIndex = app.data.settings['blomstra-search.active-index'];\n const compatVersion = app.data.settings['blomstra-search.index-compatible'];\n\n if (activeIndex && compatVersion !== REQUIRED_INDEX_COMPAT) {\n extend(DashboardPage.prototype, 'availableWidgets', function (items) {\n items.add('blomstra-search-reindex', m(ReindexWarningWidget), 110);\n });\n }\n\n const languages = new Map();\n [\n 'arabic',\n 'armenian',\n 'basque',\n 'bengali',\n 'brazilian',\n 'bulgarian',\n 'catalan',\n 'cjk',\n 'czech',\n 'danish',\n 'dutch',\n 'english',\n 'estonian',\n 'finnish',\n 'french',\n 'galician',\n 'german',\n 'greek',\n 'hindi',\n 'hungarian',\n 'indonesian',\n 'irish',\n 'italian',\n 'latvian',\n 'lithuanian',\n 'norwegian',\n 'persian',\n 'portuguese',\n 'romanian',\n 'russian',\n 'sorani',\n 'spanish',\n 'swedish',\n 'turkish',\n 'thai',\n ].forEach((language) => {\n languages.set(language, language);\n });\n\n app.extensionData\n .for('blomstra-search')\n .registerSetting({\n setting: 'blomstra-search.elastic-endpoint',\n label: app.translator.trans('blomstra-search.admin.elastic-endpoint'),\n type: 'input',\n })\n .registerSetting({\n setting: 'blomstra-search.elastic-username',\n label: app.translator.trans('blomstra-search.admin.elastic-username'),\n type: 'input',\n })\n .registerSetting({\n setting: 'blomstra-search.elastic-password',\n label: app.translator.trans('blomstra-search.admin.elastic-password'),\n type: 'password',\n })\n .registerSetting({\n setting: 'blomstra-search.elastic-index',\n label: app.translator.trans('blomstra-search.admin.elastic-index'),\n default: 'flarum',\n type: 'input',\n })\n .registerSetting({\n setting: 'blomstra-search.analyzer-language',\n label: app.translator.trans('blomstra-search.admin.analyzer.label'),\n help: app.translator.trans('blomstra-search.admin.analyzer.help'),\n type: 'select',\n options: Object.fromEntries(languages.entries()),\n default: 'english',\n })\n .registerSetting({\n setting: 'blomstra-search.search-discussion-subjects',\n label: app.translator.trans('blomstra-search.admin.search-discussion-subjects'),\n type: 'switch',\n })\n .registerSetting({\n setting: 'blomstra-search.search-post-bodies',\n label: app.translator.trans('blomstra-search.admin.search-post-bodies'),\n type: 'switch',\n });\n});\n","import setPrototypeOf from \"./setPrototypeOf.js\";\nexport default function _inheritsLoose(subClass, superClass) {\n subClass.prototype = Object.create(superClass.prototype);\n subClass.prototype.constructor = subClass;\n setPrototypeOf(subClass, superClass);\n}"],"names":["__webpack_require__","module","getter","__esModule","d","a","exports","definition","key","o","Object","defineProperty","enumerable","get","obj","prop","prototype","hasOwnProperty","call","Symbol","toStringTag","value","_setPrototypeOf","p","setPrototypeOf","__proto__","flarum","core","compat","ReindexWarningWidget","subClass","superClass","create","constructor","className","content","m","Alert","type","dismissible","icon","title","app","DashboardWidget","activeIndex","compatVersion","extend","DashboardPage","items","add","languages","Map","forEach","language","set","registerSetting","setting","label","default","help","options","fromEntries","entries"],"sourceRoot":""} \ No newline at end of file diff --git a/js/src/admin/index.ts b/js/src/admin/index.ts index fd096ee..42da4ee 100644 --- a/js/src/admin/index.ts +++ b/js/src/admin/index.ts @@ -1,6 +1,36 @@ import app from 'flarum/admin/app'; +import { extend } from 'flarum/common/extend'; +import DashboardPage from 'flarum/admin/components/DashboardPage'; +import DashboardWidget from 'flarum/admin/components/DashboardWidget'; +import Alert from 'flarum/common/components/Alert'; + +const REQUIRED_INDEX_COMPAT = 'v2'; + +class ReindexWarningWidget extends DashboardWidget { + className() { + return 'ReindexWarningWidget'; + } + + content() { + return m(Alert, { + type: 'warning', + dismissible: false, + icon: 'fas fa-exclamation-triangle', + title: app.translator.trans('blomstra-search.admin.reindex-required.title'), + }, app.translator.trans('blomstra-search.admin.reindex-required.detail')); + } +} app.initializers.add('blomstra-search', () => { + const activeIndex = app.data.settings['blomstra-search.active-index']; + const compatVersion = app.data.settings['blomstra-search.index-compatible']; + + if (activeIndex && compatVersion !== REQUIRED_INDEX_COMPAT) { + extend(DashboardPage.prototype, 'availableWidgets', function (items) { + items.add('blomstra-search-reindex', m(ReindexWarningWidget), 110); + }); + } + const languages = new Map(); [ 'arabic', @@ -73,12 +103,6 @@ app.initializers.add('blomstra-search', () => { options: Object.fromEntries(languages.entries()), default: 'english', }) - .registerSetting({ - setting: 'blomstra-search.elastic-index', - label: app.translator.trans('blomstra-search.admin.elastic-index'), - default: 'flarum', - type: 'input', - }) .registerSetting({ setting: 'blomstra-search.search-discussion-subjects', label: app.translator.trans('blomstra-search.admin.search-discussion-subjects'), diff --git a/resources/less/admin.less b/resources/less/admin.less new file mode 100644 index 0000000..ab4e33e --- /dev/null +++ b/resources/less/admin.less @@ -0,0 +1,3 @@ +.ReindexWarningWidget { + padding: 0; +} diff --git a/resources/locale/en.yml b/resources/locale/en.yml index 5d72824..16a6597 100644 --- a/resources/locale/en.yml +++ b/resources/locale/en.yml @@ -9,6 +9,9 @@ blomstra-search: help: | The analyzer makes search understand stop words and undertakes language specific improvements for indexing. + reindex-required: + title: Search index rebuild required + detail: "The search index is not compatible with this version of the extension. Run: php flarum blomstra:search:index build" search-discussion-subjects: Search inside discussion titles search-post-bodies: Search inside comments match-sentences: Match search term against full sentence diff --git a/src/Commands/BuildCommand.php b/src/Commands/BuildCommand.php index a0a8b7c..e4b1ca9 100644 --- a/src/Commands/BuildCommand.php +++ b/src/Commands/BuildCommand.php @@ -29,20 +29,56 @@ use Spatie\ElasticsearchQueryBuilder\Queries\TermQuery; class BuildCommand extends Command { + /** + * Bump this when a mapping change requires a full reindex. + * Stored in blomstra-search.index-compatible after a successful build. + */ + public const INDEX_COMPAT_VERSION = 'v2'; + protected $signature = 'blomstra:search:index {action? : build | promote | rollback | discard | mapping | fill} - {--fresh : With build: drop the staging index and start completely fresh} - {--resume : With build: continue from where an interrupted build left off} - {--keep-backup : With promote: retain the replaced live index as a backup for rollback} - {--pending : With discard: drop the staging index} - {--backup : With discard: drop the backup index} - {--only= : Seed only this document type (e.g. discussions or posts)} - {--max-id= : Limit seeding to IDs up to this value} - {--throttle= : Seconds to wait between batches} - {--i-am-sure : Skip the promote confirmation prompt (for scripts/CI)}'; + {--fresh} + {--resume} + {--staging} + {--keep-backup} + {--pending} + {--backup} + {--only=} + {--max-id=} + {--throttle=} + {--i-am-sure}'; protected $description = 'Build and manage the Elasticsearch search index.'; + protected $help = <<<'HELP' +Actions and their options: + + build Queue documents and promote automatically. + --resume Resume an interrupted build from where it left off + --fresh Drop the staging index and start completely fresh + --staging Keep in staging — requires explicit promote (blue-green) + --keep-backup Retain the replaced index for rollback + + promote Swap alias to staging index (blue-green workflow). + --keep-backup Retain the replaced index for rollback + --i-am-sure Skip the confirmation prompt + + rollback Restore the backup index to live. + + discard Drop an index without promoting. + --pending Drop the staging index (cancel a build) + --backup Drop the backup index (cleanup after --keep-backup) + + mapping Push updated mapping to the live index only. + + fill Seed only documents missing from the live index. + +Shared seeding options (build / fill): + --only=TYPE Seed only this type: discussions or posts + --throttle=N Seconds to wait between batches + --max-id=N Limit seeding to IDs up to this value +HELP; + public function handle(Container $container): void { /** @var Client $client */ @@ -139,11 +175,15 @@ class BuildCommand extends Command $this->runSeeders($seeders, $container->make(Queue::class), $client, $settings, $targetIndex); if ($stagingBuild) { - $staging = $settings->get('blomstra-search.staging-index'); - $this->info("Build complete. Drain the queue, then promote '$staging' to live:"); - $this->line(' php flarum queue:work --stop-when-empty'); - $this->line(' php flarum blomstra:search:index promote'); - $this->line(' php flarum blomstra:search:index promote --keep-backup # retain old index for rollback'); + if ($this->option('staging')) { + $staging = $settings->get('blomstra-search.staging-index'); + $this->info("Staging build complete. Drain the queue, then promote '$staging' to live:"); + $this->line(' php flarum queue:work --stop-when-empty'); + $this->line(' php flarum blomstra:search:index promote'); + } else { + $this->info('Jobs queued. Promoting now (search improves as the queue drains)...'); + $this->performPromote($client, $alias, $settings); + } } } @@ -159,6 +199,11 @@ class BuildCommand extends Command } } + $this->performPromote($client, $alias, $settings); + } + + protected function performPromote(Client $client, string $alias, SettingsRepositoryInterface $settings): void + { $staging = $settings->get('blomstra-search.staging-index'); if (!$staging || !$client->indices()->exists(['index' => $staging])) { @@ -202,6 +247,7 @@ class BuildCommand extends Command $settings->set('blomstra-search.active-index', $staging); $settings->set('blomstra-search.staging-index', null); + $settings->set('blomstra-search.index-compatible', self::INDEX_COMPAT_VERSION); } protected function runRollback(Client $client, string $alias, SettingsRepositoryInterface $settings): void @@ -376,6 +422,7 @@ class BuildCommand extends Command $client->indices()->putAlias(['index' => $concrete, 'name' => $alias]); $settings->set('blomstra-search.active-index', $concrete); + $settings->set('blomstra-search.index-compatible', self::INDEX_COMPAT_VERSION); foreach ($seeders as $seeder) { $this->setContinueAt($settings, $seeder->type(), null);