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):
|
When you need to rebuild the full index (e.g. after a mapping change):
|
||||||
|
|
||||||
```bash
|
```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
|
php flarum blomstra:search:index build
|
||||||
|
|
||||||
# 2. Drain the queue
|
# Or keep a backup of the old index in case you need to roll back
|
||||||
php flarum queue:work --stop-when-empty
|
php flarum blomstra:search:index build --keep-backup
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
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
|
```bash
|
||||||
php flarum blomstra:search:index rollback
|
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
|
```bash
|
||||||
php flarum blomstra:search:index discard --backup
|
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
|
### Resuming or cancelling an interrupted build
|
||||||
|
|
||||||
If a build is interrupted, re-run it with the appropriate flag:
|
If a build is interrupted, re-run it with the appropriate flag:
|
||||||
|
|
@ -102,14 +116,16 @@ php flarum blomstra:search:index mapping
|
||||||
|
|
||||||
| Command | Description |
|
| 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 --resume` | Resume an interrupted build from where each seeder left off. |
|
||||||
| `build --fresh` | Drop the staging index and start completely fresh. |
|
| `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. |
|
| `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 --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. |
|
| `mapping` | Push updated mapping to the live index without rebuilding or reseeding. |
|
||||||
| `fill` | Seed only documents missing from the live index. |
|
| `fill` | Seed only documents missing from the live index. |
|
||||||
| `build --only=discussions` | Seed only the specified document type (`discussions` or `posts`). |
|
| `build --only=discussions` | Seed only the specified document type (`discussions` or `posts`). |
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,8 @@ return [
|
||||||
(new Flarum\Frontend('forum'))
|
(new Flarum\Frontend('forum'))
|
||||||
->js(__DIR__.'/js/dist/forum.js'),
|
->js(__DIR__.'/js/dist/forum.js'),
|
||||||
(new Flarum\Frontend('admin'))
|
(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'),
|
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
|
//# 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 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', () => {
|
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();
|
const languages = new Map();
|
||||||
[
|
[
|
||||||
'arabic',
|
'arabic',
|
||||||
|
|
@ -73,12 +103,6 @@ app.initializers.add('blomstra-search', () => {
|
||||||
options: Object.fromEntries(languages.entries()),
|
options: Object.fromEntries(languages.entries()),
|
||||||
default: 'english',
|
default: 'english',
|
||||||
})
|
})
|
||||||
.registerSetting({
|
|
||||||
setting: 'blomstra-search.elastic-index',
|
|
||||||
label: app.translator.trans('blomstra-search.admin.elastic-index'),
|
|
||||||
default: 'flarum',
|
|
||||||
type: 'input',
|
|
||||||
})
|
|
||||||
.registerSetting({
|
.registerSetting({
|
||||||
setting: 'blomstra-search.search-discussion-subjects',
|
setting: 'blomstra-search.search-discussion-subjects',
|
||||||
label: app.translator.trans('blomstra-search.admin.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: |
|
help: |
|
||||||
The analyzer makes search understand stop words and undertakes language
|
The analyzer makes search understand stop words and undertakes language
|
||||||
specific improvements for indexing.
|
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-discussion-subjects: Search inside discussion titles
|
||||||
search-post-bodies: Search inside comments
|
search-post-bodies: Search inside comments
|
||||||
match-sentences: Match search term against full sentence
|
match-sentences: Match search term against full sentence
|
||||||
|
|
|
||||||
|
|
@ -29,20 +29,56 @@ use Spatie\ElasticsearchQueryBuilder\Queries\TermQuery;
|
||||||
|
|
||||||
class BuildCommand extends Command
|
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
|
protected $signature = 'blomstra:search:index
|
||||||
{action? : build | promote | rollback | discard | mapping | fill}
|
{action? : build | promote | rollback | discard | mapping | fill}
|
||||||
{--fresh : With build: drop the staging index and start completely fresh}
|
{--fresh}
|
||||||
{--resume : With build: continue from where an interrupted build left off}
|
{--resume}
|
||||||
{--keep-backup : With promote: retain the replaced live index as a backup for rollback}
|
{--staging}
|
||||||
{--pending : With discard: drop the staging index}
|
{--keep-backup}
|
||||||
{--backup : With discard: drop the backup index}
|
{--pending}
|
||||||
{--only= : Seed only this document type (e.g. discussions or posts)}
|
{--backup}
|
||||||
{--max-id= : Limit seeding to IDs up to this value}
|
{--only=}
|
||||||
{--throttle= : Seconds to wait between batches}
|
{--max-id=}
|
||||||
{--i-am-sure : Skip the promote confirmation prompt (for scripts/CI)}';
|
{--throttle=}
|
||||||
|
{--i-am-sure}';
|
||||||
|
|
||||||
protected $description = 'Build and manage the Elasticsearch search index.';
|
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
|
public function handle(Container $container): void
|
||||||
{
|
{
|
||||||
/** @var Client $client */
|
/** @var Client $client */
|
||||||
|
|
@ -139,11 +175,15 @@ class BuildCommand extends Command
|
||||||
$this->runSeeders($seeders, $container->make(Queue::class), $client, $settings, $targetIndex);
|
$this->runSeeders($seeders, $container->make(Queue::class), $client, $settings, $targetIndex);
|
||||||
|
|
||||||
if ($stagingBuild) {
|
if ($stagingBuild) {
|
||||||
|
if ($this->option('staging')) {
|
||||||
$staging = $settings->get('blomstra-search.staging-index');
|
$staging = $settings->get('blomstra-search.staging-index');
|
||||||
$this->info("Build complete. Drain the queue, then promote '$staging' to live:");
|
$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 queue:work --stop-when-empty');
|
||||||
$this->line(' php flarum blomstra:search:index promote');
|
$this->line(' php flarum blomstra:search:index promote');
|
||||||
$this->line(' php flarum blomstra:search:index promote --keep-backup # retain old index for rollback');
|
} 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');
|
$staging = $settings->get('blomstra-search.staging-index');
|
||||||
|
|
||||||
if (!$staging || !$client->indices()->exists(['index' => $staging])) {
|
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.active-index', $staging);
|
||||||
$settings->set('blomstra-search.staging-index', null);
|
$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
|
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]);
|
$client->indices()->putAlias(['index' => $concrete, 'name' => $alias]);
|
||||||
|
|
||||||
$settings->set('blomstra-search.active-index', $concrete);
|
$settings->set('blomstra-search.active-index', $concrete);
|
||||||
|
$settings->set('blomstra-search.index-compatible', self::INDEX_COMPAT_VERSION);
|
||||||
|
|
||||||
foreach ($seeders as $seeder) {
|
foreach ($seeders as $seeder) {
|
||||||
$this->setContinueAt($settings, $seeder->type(), null);
|
$this->setContinueAt($settings, $seeder->type(), null);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue