Adds help/quick-search.yaml (QQQ help bundle) applied by QuickSearchHelp, a tables guardrail test, and a seeded-defaults test proving the primer fills the config tables on a fresh backend. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
21 KiB
Migrating from 1.0 to 1.1
1.1 lets QQQ core's record search (POST /qqq/v1/search, the Next UI global search box) use the Quick Search index. Hosts that do nothing get it by default.
Prerequisites
- The QQQ release that carries the record-search provider SPI (
RecordSearchProviderInterface,QInstance.withRecordSearchProvider), first released in QQQ4.1.0-RC.2. 1.1 builds against4.1.0-RC.2through theqqq-snapshotprofile.
What changes at startup
- The QBit registers a record-search provider.
QuickSearchRecordSearchProviderbecomes the instance'srecordSearchProvider. If the host already registered a different provider, the QBit keeps it and logs a warning; core search then does not use OpenSearch. - Indexed tables get
searchFields. Core only searches tables withsearchFields. Each indexed table that has none gets its visible string and integer searchable fields. Tables that already havesearchFieldskeep them. A table added to the instance after the QBit is produced gets none, so add tables first (as for field validation) or setsearchFieldsyourself. - Password fields are no longer indexed. Fields of type
PASSWORDare now dropped from the searchable fields like hidden fields. If a 1.0 index holds such values, run "Quick Search Reconcile Index" (or a full reindex) to rewrite the documents without them.
Behaviour to be aware of
- Next UI search results for indexed tables now come from OpenSearch: all words must match (in any searchable field), with prefix matching and field weights, instead of a contains-match on one field. Tables not indexed by Quick Search keep core's search.
- Results still pass core's security: core re-reads every hit through
QueryAction, so table permissions and record security locks apply, and records deleted since they were indexed do not appear. - Records are only as fresh as the index: a record updated in a way that is not yet indexed (for example while real-time indexing is off) is found by its indexed text.
- When OpenSearch is slow or down, core waits at most
recordSearchTimeoutMillis(default 2500) and then searches with its own contains-query; for the next 30 seconds it skips OpenSearch entirely.
Opting out
new QuickSearchQBitConfig()
// ...
.withServeCoreRecordSearch(false);
restores 1.0 behaviour: no provider, no searchFields changes.
Diagnostic tools (1.1.0-RC.2)
The QBit now adds admin tools to validate and debug search (see the README, "Diagnostic tools"). They are on by default.
- Two new operational tables, on
backendNamelike the others:quick_search_diagnostic_eventandquick_search_validation_run(columns in the README, "Operational tables"). Create them in the host's schema, with the naming the host applies to the other operational tables (tableMetaDataCustomizerapplies to them too). Until they exist, writing events and saving reports fails quietly (logged at warn) and the tools still run. - Permissions. Grant
quick_search_debug.hasAccessto the people who may use the read-only tools and see the widget,quick_search_admin.hasAccessfor Reindex in Search, and.readon the two new tables (the QBit writes them; users never do). - New processes have a
tableName(Quick Search Index, Quick Search Failed Event, and each indexed table), so they appear as table and record actions. They are not added to the admin app's children; a host that strips processes from its nav needs no change. - Opting out.
withEnableDiagnosticTools(false)adds none of it;withEnableRecordDiagnosticActions(false)keeps the tools on Quick Search Index but adds nothing to the indexed tables.
Snake_case names (1.1.0-RC.3)
Owner naming rule (2026-10-12): table names are lowercase snake_case, and a QBit's tables are prefixed with its full name. RC.3 renames the QQQ metadata names. The QBit ships no DDL, so nothing is dropped or rewritten; what you change depends on how your database tables are named today.
| Kind | Before (RC.1, RC.2) | From RC.3 |
|---|---|---|
| Table | quickSearchIndex |
quick_search_index |
| Table | quickSearchIndexRun |
quick_search_index_run |
| Table | quickSearchFailedEvent |
quick_search_failed_event |
| Table | quickSearchDiagnosticEvent |
quick_search_diagnostic_event |
| Table | quickSearchValidationRun |
quick_search_validation_run |
| Possible-value source | quickSearchIndex, quickSearchSearchableTable |
quick_search_index, quick_search_searchable_table |
| Table permissions | quickSearchIndex.read, quickSearchIndexRun.read, ... |
quick_search_index.read, quick_search_index_run.read, ... |
| Admin permission | quickSearchAdmin.hasAccess |
quick_search_admin.hasAccess |
| Debug permission | quickSearchDebug.hasAccess |
quick_search_debug.hasAccess |
| Record actions on failed events | quickSearchInspect_quickSearchFailedEvent, quickSearchReindexRecords_quickSearchFailedEvent |
quickSearchInspect_quick_search_failed_event, quickSearchReindexRecords_quick_search_failed_event |
Unchanged: Java class names, field names (quickSearchIndexId, tableName), process, widget and app names (quickSearchFullReindex, quickSearchDiagnostics, quickSearchAdmin app), the OpenSearch index, and the developer panel id.
With a table-name prefix (withTableNamePrefix or the producer namespace), table and possible-value source names are now the prefix in snake_case joined with _: prefix myApp_ or myApp gives my_app_quick_search_index, where RC.2 gave myApp_quickSearchIndex. Process, widget and app names keep the prefix as given.
The old names stay available as @Deprecated constants for upgrade scripts: QuickSearchIndex.LEGACY_TABLE_NAME (and the same on the other four entities), QuickSearchSearchableTablePossibleValueSourceProducer.LEGACY_NAME, and QuickSearchQBitConfig.LEGACY_ADMIN_PERMISSION_BASE_NAME / LEGACY_DEBUG_PERMISSION_BASE_NAME. They will be removed in 2.0.
Database tables
- Your tables are already
quick_search_*(for example you set the RDBMS backend name withinferBackendNameor a table customizer): no DDL. You can drop the customizer, since the QQQ name now equals the database name. - Your tables are camelCase (
"quickSearchIndex"): rename them, in a maintenance window, before starting RC.3. PostgreSQL example:
ALTER TABLE "quickSearchIndex" RENAME TO quick_search_index;
ALTER TABLE "quickSearchIndexRun" RENAME TO quick_search_index_run;
ALTER TABLE "quickSearchFailedEvent" RENAME TO quick_search_failed_event;
ALTER TABLE "quickSearchDiagnosticEvent" RENAME TO quick_search_diagnostic_event;
ALTER TABLE "quickSearchValidationRun" RENAME TO quick_search_validation_run;
Rename the matching sequences, indexes and constraints the same way if your tooling depends on their names. Data is untouched. If you cannot rename yet, point each table at its old name with an RDBMSTableBackendDetails table name in a table customizer.
Stored references
QQQ stores table, process and permission names in your own data. Update them in the same window (PostgreSQL examples; adjust schema and column names to your instance):
-- audit history: audit_table.name holds the QQQ table name
UPDATE audit_table SET name = 'quick_search_index' WHERE name = 'quickSearchIndex';
UPDATE audit_table SET name = 'quick_search_index_run' WHERE name = 'quickSearchIndexRun';
UPDATE audit_table SET name = 'quick_search_failed_event' WHERE name = 'quickSearchFailedEvent';
UPDATE audit_table SET name = 'quick_search_diagnostic_event' WHERE name = 'quickSearchDiagnosticEvent';
UPDATE audit_table SET name = 'quick_search_validation_run' WHERE name = 'quickSearchValidationRun';
-- saved views and saved reports
UPDATE saved_view SET table_name = 'quick_search_index' WHERE table_name = 'quickSearchIndex';
UPDATE saved_report SET table_name = 'quick_search_index' WHERE table_name = 'quickSearchIndex';
-- ... and the same for the other four tables
- Permission grants. Wherever your roles live (QQQ role tables, an identity provider's group attributes, code), replace
quickSearchAdmin.hasAccesswithquick_search_admin.hasAccess,quickSearchDebug.hasAccesswithquick_search_debug.hasAccess, andquickSearchIndex.read/.edit(and the other tables' permissions) with thequick_search_*names. Users without the new grants lose the admin app and tools until they are granted. - Bookmarks and links to
/app/quickSearchIndex/...move to/app/quick_search_index/.... - Index rows and failed events store your table names (
quick_search_index.table_name,quick_search_failed_event.table_name), not the QBit's, so they need no change for this rename. The OpenSearch documents are keyed by your tables too, so no reindex is needed. If you rename your own tables at the same time, update those two columns and run a full reindex.
Checklist
- Back up the database.
- Rename the tables (only if they are camelCase) and update the stored references above.
- Update permission grants.
- Upgrade to RC.3 and start;
startupModeFAIL_FASTreports any table the backend cannot find. - Open Admin › Quick Search and run Validate Search Index.
Data-driven configuration (1.1.0-RC.3)
From RC.3, which tables and fields Quick Search indexes is data: standard QQQ tables an admin edits, secured by permissions and audited. Code (@QuickSearchable / @QuickSearchField entities and searchableTables) still works, but it only seeds the records. Changes take effect without a redeploy.
New tables and columns
| Table | One row per | Notes |
|---|---|---|
quick_search_index (existing) |
searchable table | New columns source (CODE or OWNER) and owner_edited. basepull_timestamp_field is now editable. The generic Create and Delete screens are off: use Add Search Index and Remove Search Index. |
quick_search_index_field |
searchable field of an index | field_name, weight (1-1000), role (DISPLAY T1 display, TITLE, SUBTITLE, BODY, KEYWORD, EXACT), analyzer (blank, PREFIX, WORD, ENGLISH, EXACT), include_in_snippet (blank: by role), include_label, sort_order, source, owner_edited. Unique on (quick_search_index_id, field_name). Edited from the index record's Searchable Fields list. |
quick_search_settings |
one row (id 1) | The global pause (indexing_paused, paused_at, paused_by, pause_reason) and the last self-priming report. Created by the QBit. |
quick_search_config_tombstone |
table or field an admin removed | Priming never adds back what a tombstone names. Delete the tombstone to let code seed it again. |
quick_search_index_run (existing) |
run | New columns cancel_requested and cancel_requested_by (Cancel Index Run). |
The config tables are audited at field level (QQQ's audit tables; without them the audit is skipped with a log line) and use tablePermissionRules (by default quick_search_index_field.read, .edit, and so on). The new processes use adminPermissionRules (quick_search_admin.hasAccess).
Precedence: code versus data
- The records are the source of truth once the config tables exist and priming has run. Indexing (real-time, basepull, reconcile, full reindex), search, the core record-search provider and the diagnostic tools read the effective config from them. It is cached for 30 seconds and dropped at once on the server where a config row changes, so an edit takes effect immediately there and within 30 seconds on other servers.
- Code seeds; it never overrides an owner. At startup (and on each basepull until it succeeds), priming:
- adds a row (source
CODE,enabledperenabledByDefault) and its field rows for every code-declared table that has none, and queues its first build; - updates the interval and timestamp field of rows nobody edited (
owner_editedfalse);enabledis set only when a row is created; - adds code-declared fields that have no row, and updates the weight and label flag of field rows nobody edited; either change queues a rebuild of that table;
- never touches a row or field an admin added or edited (
owner_editedtrue), and never re-adds anything a tombstone names; - never deletes anything: removing an annotation leaves the rows for the admin to remove.
- adds a row (source
- Removals are remembered. Remove Search Index, and deleting a field row, write a tombstone, so code does not bring them back.
- Without the config tables (before you add the DDL below), or until priming succeeds, Quick Search uses the code-declared tables exactly as before RC.3. So the upgrade can be staged.
Self-priming
- A new code-declared table gets its rows on startup and is built by the next basepull (status
PENDING_BUILD, thenBUILDING, thenACTIVE; the run is recorded asFULL_REINDEX). No manual step is needed. - A new install, or an index that was deleted, is recreated by the basepull, and the tables that had documents in it are rebuilt. An index that is empty while a table last counted documents in it is rebuilt too.
- The Search Diagnostics widget on the Quick Search home reports it ("New: 2 tables added and building (...)"), and
QuickSearchStatus.read(runtime).bannerHtml()gives the same lines for a host's own home widget. - Existing rows from RC.2 or earlier get their field rows on the first start, with no rebuild when the index was built with exactly the code's fields.
Admin actions
All are standard streamed-ETL processes with a review step and a result step, run from the table's Actions (never from the nav):
| Action | On | What it does |
|---|---|---|
| Add Search Index | Quick Search Index (no selection) | Pick a table you can read that is not indexed and is not internal or secret; set the interval, timestamp field and enabled flag. The review shows the estimated record count, the fields it starts with (record-label fields as titles, then text fields) and what is never indexed (hidden and isSensitive fields). Saving queues the first build (or runs it at once when no scheduler runs the basepull). |
| Searchable Fields (child list on the index record) | Quick Search Index Field | Add, edit or remove fields with the standard screens; a field must exist, be indexable and not sensitive, and not be listed twice. The change applies to search and new writes at once and marks the index NEEDS_REINDEX. |
| Apply Search Index Changes | Quick Search Index (selected) | The review previews the field changes since the last build and the records the rebuild reads; executing rebuilds the table. |
| Disable Search Index, Remove Search Index | Quick Search Index (selected) | Stop indexing at once. The input step's "Search documents" choice (delete now, or keep) is restated on the review step. Remove also deletes the field rows and pending failed events and leaves a tombstone. Enabling a disabled index again (edit Enabled) queues a build. |
| Cancel Index Run | Quick Search Index Run (selected RUNNING runs) |
Sets a cooperative cancel flag that the run checks between batches; it ends CANCELLED with its counts. A cancelled all-tables rebuild keeps the previous index; a cancelled queued build waits for Apply Search Index Changes. |
| Pause Indexing, Resume Indexing | Quick Search Settings | Pauses all indexing (real-time and scheduled; a running rebuild stops after its batch) while search keeps serving the current index. Changes made while paused are kept (DEFERRED failed events) and indexed by the basepull after Resume. The home shows "Paused since ... by ...". |
Roles and analyzers: TITLE fields make the hit's label, SUBTITLE its subtitle (QuickSearchResult.subtitle), DISPLAY fields its short details (QuickSearchResult.displayValues); KEYWORD and EXACT fields stay out of the highlighted snippet and match on their own. The WORD, ENGLISH and EXACT analyzers use new document objects (fieldValuesWord, fieldValuesEnglish, fieldValuesExact); their mapping is added to an existing index at startup (no mapping-version change), and documents get them as they are rebuilt. Tables of plain fields produce the same documents and queries as before.
DDL
The QBit ships no DDL. PostgreSQL, with the snake_case column names QQQ infers (QInstanceEnricher.setInferredFieldBackendNames); use the field names as written if your backend maps fields to camelCase columns. Liquibase formatted SQL:
-- liquibase formatted sql
-- changeset quick-search:1.1.0-RC.3-1
ALTER TABLE quick_search_index ADD COLUMN IF NOT EXISTS source VARCHAR(20);
ALTER TABLE quick_search_index ADD COLUMN IF NOT EXISTS owner_edited BOOLEAN;
ALTER TABLE quick_search_index_run ADD COLUMN IF NOT EXISTS cancel_requested BOOLEAN;
ALTER TABLE quick_search_index_run ADD COLUMN IF NOT EXISTS cancel_requested_by VARCHAR(250);
-- changeset quick-search:1.1.0-RC.3-2
CREATE TABLE IF NOT EXISTS quick_search_index_field (
id INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL,
quick_search_index_id INTEGER NOT NULL,
field_name VARCHAR(255) NOT NULL,
weight INTEGER,
role VARCHAR(20),
analyzer VARCHAR(20),
include_in_snippet BOOLEAN,
include_label BOOLEAN,
sort_order INTEGER,
source VARCHAR(20),
owner_edited BOOLEAN,
create_date TIMESTAMP WITHOUT TIME ZONE,
modify_date TIMESTAMP WITHOUT TIME ZONE,
CONSTRAINT pk_quick_search_index_field PRIMARY KEY (id),
CONSTRAINT uq_quick_search_index_field UNIQUE (quick_search_index_id, field_name)
);
-- changeset quick-search:1.1.0-RC.3-3
CREATE TABLE IF NOT EXISTS quick_search_settings (
id INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL,
indexing_paused BOOLEAN,
paused_at TIMESTAMP WITHOUT TIME ZONE,
paused_by VARCHAR(250),
pause_reason VARCHAR(500),
last_primed_at TIMESTAMP WITHOUT TIME ZONE,
last_priming_summary TEXT,
last_priming_new_tables INTEGER,
create_date TIMESTAMP WITHOUT TIME ZONE,
modify_date TIMESTAMP WITHOUT TIME ZONE,
CONSTRAINT pk_quick_search_settings PRIMARY KEY (id)
);
-- changeset quick-search:1.1.0-RC.3-4
CREATE TABLE IF NOT EXISTS quick_search_config_tombstone (
id INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL,
table_name VARCHAR(255) NOT NULL,
field_name VARCHAR(255),
removed_by VARCHAR(250),
reason VARCHAR(500),
create_date TIMESTAMP WITHOUT TIME ZONE,
CONSTRAINT pk_quick_search_config_tombstone PRIMARY KEY (id)
);
CREATE INDEX IF NOT EXISTS idx_quick_search_config_tombstone_table ON quick_search_config_tombstone (table_name);
MySQL: use INT AUTO_INCREMENT for the ids, DATETIME for the timestamps and TINYINT(1) for the booleans, and drop IF NOT EXISTS from ADD COLUMN.
Behaviour changes to be aware of
quick_search_indexno longer offers Create or Delete (TABLE_INSERTandTABLE_DELETEare off), so the API cannot insert or delete rows either. Rows come from priming and Add Search Index, and go through Remove Search Index.- Basepull builds queued tables (
PENDING_BUILD) itself and recreates a missing index.NEEDS_REINDEXstill waits for an admin (Apply Search Index Changes, or Rebuild). - With the config tables, a table that is not in the instance is not part of the effective config, and
ensureIndexRowExistsno longer creates rows (priming does, honouring tombstones). - Hidden and
isSensitivefields are never indexed, whether declared in code or added as rows. quick_search_index_runandquick_search_failed_eventoffer no Create or Edit (Delete stays). The Basepull, Full Reindex and Reconcile processes are table actions on Quick Search Index rather than app children; their table input is nowsearchTableName(API callers may keep sendingtableName).quick_search_searchable_tableis now a custom possible-value source over the effective tables (it was an enum of the code-declared ones).- Tables added at runtime get core
searchFields(for the global search) when they have none, but the per-table Inspect Search Document and Reindex in Search record actions appear only after the next restart.
Table help (no action needed)
The qbit now sets bundle help (help/quick-search.yaml) on the Quick Search Admin app and each quick_search_* table, so an empty table says what it is for and how its rows get created. It fills only empty help slots; help a host already set on these tables is kept. There is no DDL change and no host include.
Checklist for data-driven configuration
- Add the DDL above (it is additive).
- Grant
quick_search_index_field,quick_search_settingsandquick_search_config_tombstonepermissions (read, and edit where admins should change them) next to the existing Quick Search grants. - Upgrade and start. The Search Diagnostics widget should not say the searchable tables come from code, and shows the first priming's summary.
- Optionally move code-declared search config into the records and delete the annotations: the rows stay and are now the owner's.