feat: Show admin dashboard notice when search index needs rebuilding
After a mapping change (e.g. the parent-child restructure), existing installs need to rebuild their index. A DashboardWidget appears on the admin page warning the admin and showing the command to run. Compatibility is tracked via the blomstra-search.index-compatible setting.
This commit is contained in:
parent
3039b65946
commit
81e7b6502d
52
README.md
52
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`). |
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
.ReindexWarningWidget {
|
||||
padding: 0;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
<comment>Actions and their options:</comment>
|
||||
|
||||
<info>build</info> Queue documents and promote automatically.
|
||||
<comment>--resume</comment> Resume an interrupted build from where it left off
|
||||
<comment>--fresh</comment> Drop the staging index and start completely fresh
|
||||
<comment>--staging</comment> Keep in staging — requires explicit <info>promote</info> (blue-green)
|
||||
<comment>--keep-backup</comment> Retain the replaced index for rollback
|
||||
|
||||
<info>promote</info> Swap alias to staging index (blue-green workflow).
|
||||
<comment>--keep-backup</comment> Retain the replaced index for rollback
|
||||
<comment>--i-am-sure</comment> Skip the confirmation prompt
|
||||
|
||||
<info>rollback</info> Restore the backup index to live.
|
||||
|
||||
<info>discard</info> Drop an index without promoting.
|
||||
<comment>--pending</comment> Drop the staging index (cancel a build)
|
||||
<comment>--backup</comment> Drop the backup index (cleanup after --keep-backup)
|
||||
|
||||
<info>mapping</info> Push updated mapping to the live index only.
|
||||
|
||||
<info>fill</info> Seed only documents missing from the live index.
|
||||
|
||||
<comment>Shared seeding options (build / fill):</comment>
|
||||
<comment>--only=TYPE</comment> Seed only this type: <info>discussions</info> or <info>posts</info>
|
||||
<comment>--throttle=N</comment> Seconds to wait between batches
|
||||
<comment>--max-id=N</comment> 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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue