QBit: Quick Search
OpenSearch-backed full-text search across the tables of a QQQ application.
Annotate entity classes or declare tables in config, point the QBit at an OpenSearch cluster, produce it into your QInstance, and you get real-time indexing of committed writes, a scheduled catch-up and reconcile, a blackout-free full rebuild, and a permission-aware search API with relevance scoring, typeahead, highlights and pagination.
Requires QQQ 4.1 (the QBit uses 4.1's record-change listeners, after-commit callbacks and runtime services). Serving core search (POST /qqq/v1/search, below) needs QQQ 4.1.0-RC.2 or later, the first release that carries the record-search provider SPI. The record developer panel (below) needs QQQ 4.1.0-RC.4 or later, which carries the developer panel contract; the 1.1 line builds against 4.1.0-RC.4 from the next release candidate on.
What it does
| Capability | How |
|---|---|
| Real-time indexing | One RecordChangeListenerInterface registered on the instance; events are published after the write's transaction commits, so rolled-back writes never reach the index. Failures are recorded in quickSearchFailedEvent and replayed by the scheduled basepull. |
| Basepull catch-up | Scheduled process (schedulerName plus basepullRepeatSeconds) that re-indexes rows changed since the previous run's start, with a configurable overlap, paging by primary key. |
| Reconcile | Scheduled or on-demand process that re-indexes everything in place, then removes documents whose source row is gone, including documents of tables no longer configured. No search blackout. |
| Full rebuild | Indexes every table into a fresh physical index and atomically swaps the alias; the old index keeps serving until the new one is complete. Also upgrades an index created by an older release. |
| Core search | QuickSearchRecordSearchProvider serves QQQ's RecordSearchAction (POST /qqq/v1/search, the Next UI global search box) from OpenSearch for the indexed tables, with a timeout and an automatic fallback to core's own search. |
| Search | QuickSearchAction: AND semantics, edge-ngram typeahead with a plain search analyzer, per-field weights, highlights, tableNames and limitPerTable, bounded paging, tableLabel on results. Only tables the session may read are searched; hits are re-read through QueryAction so record security locks apply. |
| Connections | HTTP or HTTPS, basic auth, AWS IAM SigV4, custom CA or trust store, mTLS, timeouts, pool size, a transport customizer SPI for anything else. Secrets by ${env.X} reference. |
| Operations | quickSearchIndex (per-table status, counts, last run, last error), quickSearchIndexRun (history, purged after runHistoryRetentionDays), quickSearchFailedEvent, a quickSearchAdmin app, explicit permission rules, fail-fast or degraded startup. |
Supported platforms
Tested in CI against the Docker images named in pom.xml (opensearch.test.image).
| Target | Status |
|---|---|
| Self-managed OpenSearch 2.19.x and 3.x | Supported, security plugin disabled or HTTP basic authentication, HTTP or HTTPS with a trusted, configured or (development only) unverified certificate |
| Amazon OpenSearch Service managed domains, OpenSearch 2.x or 3.x | Supported with fine-grained access control: either an internal-database master user with authMode BASIC, or IAM with authMode AWS_SIGV4 (awsRegion, awsServiceName es, optional awsAssumeRoleArn). Port 443 over HTTPS. Keep bulkBatchSize and maxBulkRequestBytes under the instance type's 10 MiB payload limit on small instances. |
| Amazon OpenSearch Serverless | Not supported in 1.0 (_delete_by_query and _refresh, used by reconcile and full rebuild, are not offered there) |
| Elasticsearch, OpenSearch 1.x | Not supported |
Least-privilege role for the identity the QBit uses, on the alias and its physical indexes ({indexName}*): create_index, read, write, delete, plus indices:admin/refresh*, indices:admin/aliases, indices:admin/delete (for the alias swap) and indices:admin/mapping/get; cluster level cluster_composite_ops for _bulk. For AWS fine-grained access control map the same to the basic-auth user or the IAM role. Treat the index as sensitive data: it holds copies of every indexed field value.
For AWS_SIGV4 add these optional dependencies to your host (the QBit declares them optional):
<dependency><groupId>software.amazon.awssdk</groupId><artifactId>apache-client</artifactId></dependency>
<dependency><groupId>software.amazon.awssdk</groupId><artifactId>sts</artifactId></dependency> <!-- only for awsAssumeRoleArn -->
Getting started
<dependency>
<groupId>com.kingsrook.qbits</groupId>
<artifactId>qbit-quick-search</artifactId>
<version>1.1.0-RC.1</version>
</dependency>
1.1.0-RC.1 is the current release candidate (the 1.1 line, published from release/1.1; it needs QQQ 4.1.0-RC.2 or later, see Upgrading from 1.0). Hosts staying on the 1.0 line use 1.0.0-RC.2; 1.0.0 follows once host applications have adopted it (#12).
Annotate entity classes:
@QuickSearchable(tableName = "customer")
public class Customer
{
@QuickSearchField(weight = 3)
private String name;
@QuickSearchField(weight = 1)
private String email;
@QuickSearchField(weight = 2, includeLabel = true)
private String accountNumber;
}
Produce the QBit (after your backends, tables and scheduler are in the instance):
QuickSearchQBitConfig config = new QuickSearchQBitConfig()
.withBackendName("rdbms")
.withOpensearchUrl("https://search.example.com:443")
.withAuthMode(QuickSearchAuthMode.BASIC)
.withOpensearchUsername("${env.OPENSEARCH_USER}")
.withOpensearchPassword("${env.OPENSEARCH_PASSWORD}")
.withOpensearchIndexName("my-app-search")
.withSchedulerName("myScheduler")
.withSearchableEntityClasses(List.of(Customer.class, Order.class));
new QuickSearchQBitProducer().withConfig(config).produce(qInstance);
The producer implements QQQ's QBitMetaDataProducer, registers its own QBitMetaData (com.kingsrook.qbits:quick-search), and adds its tables, processes and app to the instance. Do not add a QBitMetaData for it yourself.
Tables from other QBits or jars, without annotations:
config.withSearchableTable(new SearchableTableConfig("crmContact", List.of(
new SearchableFieldConfig("firstName").withWeight(2),
new SearchableFieldConfig("lastName").withWeight(2),
new SearchableFieldConfig("email").withWeight(3)))
.withRecordLabelFormat("%s %s", "firstName", "lastName"));
Annotation and config-driven tables can be mixed; a table may appear in only one of them. Hidden and password fields are never indexed. A table without the change-detection field (modifyDate by default) is covered by reconcile instead of incremental basepull, and the QBit logs a warning for it at startup. Set basepullTimestampField to null on a config-driven table to opt out without the warning.
Searching
QuickSearchOutput output = new QuickSearchAction().execute(new QuickSearchInput()
.withSearchTerm("widget")
.withTableNames(List.of("customer", "order")) // optional
.withLimit(25)
.withOffset(0));
for(QuickSearchResult result : output.getResults())
{
result.getTableName(); result.getTableLabel(); result.getRecordId();
result.getRecordLabel(); result.getScore(); result.getHighlightSnippet();
}
output.getTotalHits(); output.getTotalHitsIsLowerBound(); output.getHasMore();
limitPerTable returns up to that many hits from each table instead of one ranked list. Terms shorter than 2 characters return nothing; longer than 100 characters are rejected. limit is capped at maxSearchLimit and offset + limit at 10,000.
offset skips N accessible results: with record security locks on, hits a lock hides are not counted, so consecutive pages never overlap or skip a visible hit, and with limitPerTable the offset applies within each table. With locks on, totalHits counts hits confirmed accessible so far; totalHitsIsLowerBound is true when the scan stopped before reading every candidate (page full, result window, or scan budget). A single-list search reads at most 10,000 raw hits in batches of at most 1,000. In per-table mode the T tables share that budget, each getting max(10,000 / T, 2 x limitPerTable) hits, so one call reads at most max(10,000, T x 2 x limitPerTable) raw hits. Paging within a table stops past that share of raw hits, as it does past 10,000 in a single list: hasMore turns false and totalHitsIsLowerBound stays true to signal unreachable matches.
Search runs as the current QContext session: tables the session cannot read are skipped, and with applyRecordSecurityLocks (default) hits are re-read through QueryAction so record security locks apply.
Core search (POST /qqq/v1/search)
QQQ core has its own record search (RecordSearchAction, served at POST /qqq/v1/search and used by the Next UI's global search box): a case-insensitive contains-query over each table's searchFields. With serveCoreRecordSearch (default true) the producer registers QuickSearchRecordSearchProvider on the instance, and core asks it to search every table Quick Search indexes. The API and the UI do not change.
- What is served from OpenSearch. Tables this QBit indexes and that are enabled in
quickSearchIndex. Core offers a table only when it hassearchFields, the session may read it, and it supports query, so the producer gives each indexed table that has nosearchFieldsits visible string and integer searchable fields (never hidden or password fields).searchFieldsyou set yourself are kept. A table must be in theQInstancebefore the QBit is produced to get them. - How. One
_msearchrequest with one search per table (Quick Search's query, AND semantics and field weights); each table returns up to 4 xlimitPerTablerecord ids (at most 100) in relevance order. Core re-reads them by primary key throughQueryAction, so table permissions, record security locks and record labels are core's, and records deleted since they were indexed drop out. - Failure. The OpenSearch request is cancelled after
recordSearchTimeoutMillis(default 2500). Any failure (timeout, cluster down, index missing) makes core search those tables with its own contains-query instead, so search degrades but never fails. After a failure, Quick Search claims no tables for 30 seconds, so a down cluster does not add a timeout to every search. - Opting out.
withServeCoreRecordSearch(false)leaves core search andsearchFieldsuntouched. If the host already registered another record-search provider, the QBit keeps it and logs a warning (an instance has one provider).
QuickSearchAction remains the richer API (scores, highlights, pagination, totals).
Processes
| Process | Trigger | What it does |
|---|---|---|
| Quick Search Basepull Index | Scheduled every basepullRepeatSeconds when schedulerName is set; also on demand |
Recovers a full reindex that stopped without finishing, replays failed real-time events, re-indexes rows changed since the last run start minus basepullOverlapSeconds, purges old run history |
| Quick Search Reconcile Index | Cron via reconcileCronExpression when schedulerName is set; also on demand (optional tableName input) |
Re-indexes everything in place, removes documents without a source row and documents of unconfigured tables |
| Quick Search Full Reindex | On demand (optional tableName input) |
All tables: build a fresh physical index and swap the alias. One table: reconcile algorithm in place. Fails to start while another full reindex is running |
Without schedulerName the processes exist but nothing runs them; the QBit logs a warning at startup.
Scheduling
The schedules run only when the host starts through QQQ's QApplicationLauncher (com.kingsrook.qqq.middleware.javalin.QApplicationLauncher, in qqq-middleware-javalin). QApplicationLauncher.run(new MyApplication(), new QApplicationLauncherConfig()) starts the Javalin server, then the QScheduleManager (because the QBit's processes are scheduled), then the instance's runtime services. A host that starts only a server, or builds the QInstance itself, gets the processes but nothing runs them on a schedule, unless it calls QScheduleManager.initInstance(qInstance, systemSessionSupplier).start() itself. qqq.scheduleManager.enabled=false (or QQQ_SCHEDULE_MANAGER_ENABLED=false) turns schedules off on a node.
Basepull's fixed interval runs on any QQQ scheduler. reconcileCronExpression needs a scheduler that supports cron, which in QQQ 4.1 means a QuartzSchedulerMetaData; a SimpleSchedulerMetaData runs fixed intervals only. The expression uses Quartz syntax, seconds first (0 0 3 * * ? is 03:00 every day), and needs reconcileCronTimeZoneId, a Java time zone ID such as UTC or America/Chicago; QQQ has no default zone. produce() rejects a cron without a zone, an invalid expression or zone, and a cron on a scheduler in the instance that does not support cron.
qInstance.addScheduler(new QuartzSchedulerMetaData().withName("quartz").withProperties(quartzProperties));
config.withSchedulerName("quartz")
.withReconcileCronExpression("0 0 3 * * ?")
.withReconcileCronTimeZoneId("America/Chicago");
Diagnostic tools
With enableDiagnosticTools (default true) the QBit adds tools to validate the search and debug it from the admin UI. They are table and record actions (never left-nav entries), gated by permissions, recorded in quickSearchDiagnosticEvent, limited to diagnosticRunsPerMinute runs per user and tool, and read-only except Reindex in Search.
| Tool | Where | What it does |
|---|---|---|
| Search Playground | Quick Search Index, Actions (select rows to pre-fill their tables) | Runs a term the way the global search does, as you: the path each table takes (OpenSearch provider, core database search, or not searched) and why; the provider's own request, timed; the global search results you would see; per indexed table, the production query with scores, highlights, matched fields, which hits you can read and which the global search shows, the query DSL and the raw response (hits you cannot read are redacted); the term's tokens; and explain for a hit (table:id). The form stays open, so you can change the term and run again. |
| Validate Search Index | Quick Search Index, Actions; also scheduled by validationCronExpression |
Cluster and index health, the alias, leftover physical indexes from interrupted rebuilds, the live mapping (version, field types, analyzers, the fieldValues template) and analysis settings, then per table: database rows vs documents, a sample of records checked present and current in the index, a random sample of documents checked to still have a record, the index row's status and last run, and waiting failed events. Each finding is OK, INFO, WARN or FAIL with what to do. The report is saved as a quickSearchValidationRun row (HTML for the record view, JSON for tools). Database reads run as the system user so counts compare like with like; the report holds counts, ids and field names, never record values. One validation runs at a time per server. |
| Test Search Analyzer | Quick Search Index, Actions | _analyze on the live index: the tokens of a text as indexed (edge n-grams of 2 to 20 characters) and as a query, and, given a term, whether every term token is among the text's indexed tokens. |
| Inspect Search Document | Every indexed table's records (Actions), and Quick Search Failed Event rows | The record's document in the index next to the document indexing would write from the database now, field by field (current, stale or missing), with versions and the raw document. Enter a term to see why the record does or does not match it: term length, table enabled, read permission, document present and current, each term token found or not (and why: one character, longer than 20, not in the text, or only in the database's newer values), explain, and the record's rank against the number of ids the global search takes. |
| Reindex in Search | Every indexed table's records, and Quick Search Failed Event rows | Rebuild the selected records' documents from the database now (versioned writes, so a newer document is never overwritten); a record that no longer exists has its document removed; a record you cannot read is skipped. Asks to confirm. Needs the admin permission. |
| Search Diagnostics widget | The Quick Search Admin app home (hosts with their own home add quickSearchDiagnostics) |
Whether the global search is served by OpenSearch on this server or backing off (and why), failure and fallback counts, the latest search failures from every server, and the latest validation run. |
Search failures are recorded too: when the provider's OpenSearch request fails or times out (and core falls back to its own search), or QuickSearchAction's search fails, a SEARCH_FAILED event keeps the tables, duration, error class and message, and the server. The user's term is never stored, only its length. At most 120 events are written per server per minute.
Permissions: the read-only tools and the widget use debugPermissionRules (default quickSearchDebug.hasAccess); Reindex in Search uses adminPermissionRules (quickSearchAdmin.hasAccess); the two history tables use tablePermissionRules (grant .read only: rows are written by the QBit). Impersonation ("search as user X") is deliberately not offered: a fabricated session would not carry the real user's security key values, so its results would mislead.
Record developer panel
With QQQ 4.1.0-RC.4 or later the QBit registers a record developer panel provider (QuickSearchDeveloperPanelProvider, id quickSearch, in QQQ's DeveloperPanelsMetaData). Frontends that render developer panels (the Next UI, /app/{table}/{id}/dev) show a "Quick Search" panel on every record of an indexed table, to users who may read Quick Search Index. Every part is permission-checked:
| Part | Shown when |
|---|---|
| Index: enabled, status, record count, last basepull, last full reindex, searchable fields, link to the index row | always (read on Quick Search Index) |
| Latest index run: type, status, start and end, processed, indexed, errors | the user may read Quick Search Index Run |
| Search document: verdict (current, stale, missing) and a Field / Database / Index / Match table | enableDiagnosticTools and the debug permission (debugPermissionRules); otherwise a note says why it is missing |
| Failed index events of the record | the user may read Quick Search Failed Event and the record has any |
| Actions: Inspect search document, Reindex this record | the record actions exist and the user may run them (debug and admin permission) |
| Link to Quick Search Admin | the user may open the app |
The panel's status is Indexed, Stale, Not indexed, Disabled (the table is turned off in Quick Search Index), or OpenSearch unavailable. When OpenSearch cannot be reached the panel still shows the database-side state, with a warning in place of the document comparison. Hosts without a developer-panel frontend are unaffected.
Configuration reference
| Field | Default | Description |
|---|---|---|
backendName |
required | QQQ backend for the operational tables |
opensearchUrl |
https://host[:port]; preferred over opensearchHost, opensearchPort, useSsl; credentials in the URL are rejected |
|
opensearchHost, opensearchPort, useSsl |
useSsl=false |
Legacy connection fields |
opensearchIndexName |
required | Alias name owned by this QBit; lowercase, OpenSearch naming rules |
authMode |
inferred | NONE, BASIC (inferred when credentials are set), AWS_SIGV4 |
opensearchUsername, opensearchPassword |
BASIC credentials; ${env.X} references recommended (an empty value counts as missing); refused over plain HTTP unless allowPlaintextCredentials |
|
awsRegion, awsServiceName, awsAssumeRoleArn |
region from AWS_REGION; es |
AWS_SIGV4 settings |
tls |
JVM defaults | caCertificatePath or trustStorePath/trustStorePassword/trustStoreType, keyStorePath/keyStorePassword/keyStoreType (mTLS), hostnameVerification (false logs a warning), insecureSkipVerify (loopback only unless allowInsecureInProduction) |
connectTimeoutMillis, responseTimeoutMillis, maxConnections |
5000, 60000, 30 | Transport settings |
transportCustomizer |
QCodeReference to an OpenSearchTransportCustomizer (bearer tokens, API keys, interceptors) |
|
startupMode |
FAIL_FAST |
FAIL_FAST fails produce() when the cluster is unreachable; DEGRADED boots and retries on first use |
enableRealTimeIndexing |
true |
Register the record-change listener |
enableBasepullProcess, enableMaintenanceProcesses |
true |
Register basepull; register full reindex and reconcile. enableScheduledProcesses is a deprecated alias for both |
schedulerName, basepullRepeatSeconds, reconcileCronExpression, reconcileCronTimeZoneId |
none, 300, none, none | Scheduling; a cron needs the time zone ID and a Quartz scheduler, and nothing runs without QApplicationLauncher (see Scheduling) |
basepullOverlapSeconds |
300 | Re-read window before the previous run start |
defaultBasepullIntervalMinutes |
5 | Per-table due interval for config-driven tables |
runHistoryRetentionDays |
30 | Purge run records older than this; null keeps them |
fullReindexStaleMinutes |
30 | A running full reindex that shows no sign of life for this long is treated as stopped: it no longer blocks a new one, and basepull recovers its tables. Keep it well above the time one page of sourceBatchSize records takes; null means the default |
bulkBatchSize, maxBulkRequestBytes, sourceBatchSize |
500, 5 MiB, 1000 | Batch sizes |
maxFieldLength |
10000 | Truncate each indexed value |
maxSearchLimit |
100 | Largest page size |
applyRecordSecurityLocks |
true |
Re-read hits through QueryAction |
serveCoreRecordSearch |
true |
Register QuickSearchRecordSearchProvider so core POST /qqq/v1/search uses OpenSearch for indexed tables, and give those tables searchFields when they have none |
recordSearchTimeoutMillis |
2500 | Wall-clock limit on the OpenSearch request behind a core search; past it, core falls back to its own search |
enableDiagnosticTools |
true |
Add the diagnostic tools, the quickSearchDiagnosticEvent and quickSearchValidationRun tables and the diagnostics widget |
enableRecordDiagnosticActions |
true |
With the tools on, add Inspect Search Document and Reindex in Search to every indexed table |
validationCronExpression, validationCronTimeZoneId |
none | Schedule Validate Search Index; same rules as the reconcile schedule (a cron-capable scheduler such as Quartz, a valid Quartz expression, a time zone ID) |
validationSampleSize |
50 | Records and documents sampled per table by validation (at most 1000) |
diagnosticRunsPerMinute |
30 | Per-user, per-tool limit on diagnostic runs |
debugPermissionRules |
HAS_ACCESS_PERMISSION, base name quickSearchDebug |
Rules for the read-only diagnostic tools and the widget |
adminPermissionRules |
HAS_ACCESS_PERMISSION, base name quickSearchAdmin |
Rules for the app, the maintenance processes and Reindex in Search |
tablePermissionRules |
READ_WRITE_PERMISSIONS |
Rules for the operational tables |
tableMetaDataCustomizer |
Applied to every produced table (backend details, for example) | |
searchableEntityClasses, searchableTables |
at least one | What to index |
indexEventPublisher |
synchronous | Replace the publisher (experimental SPI; see below) |
@QuickSearchable(tableName, fields, basepullIntervalMinutes, basepullTimestampField, enabledByDefault) and @QuickSearchField(weight, includeLabel) keep their 0.x meaning.
Operational tables
quickSearchIndex: one row per table withenabled(honoured by indexing and search, cached for one minute),basepullIntervalMinutes,lastBasepullTime,lastReconcileTime,lastFullReindexTime,lastRunStatus,lastErrorMessage,recordCount,documentCount,realTimeErrorCount,status(ACTIVE,NEEDS_REINDEXwhen the configured fields changed since the row was created, orREBUILDINGwhile a full reindex of all tables runs). Unique ontableName.quickSearchIndexRun:runType(BASEPULL,RECONCILE,FULL_REINDEX),status, counts,errorMessage.quickSearchDiagnosticEvent(withenableDiagnosticTools):eventType(SEARCH_FAILED,TOOL_RUN,REINDEX,RATE_LIMITED),source,outcome,userName,tableNames,searchTerm(tool runs only),durationMillis,errorClass,errorMessage(text),detail(text),hostName,createDate. Purged afterrunHistoryRetentionDays.quickSearchValidationRun(withenableDiagnosticTools):status(PASS,WARN,FAIL),triggerType(MANUAL,SCHEDULED),userName,tableNames,sampleSize,tablesChecked,failCount,warnCount,summary,reportHtml(HTML),reportJson(text),startTime,endTime,createDate. Purged afterrunHistoryRetentionDays.quickSearchFailedEvent: real-time events that could not be applied;statusPENDINGorEXHAUSTEDafter 10 attempts. Deletes made while a full reindex of all tables runs are also recorded here with statusAWAITING_REINDEX: the reindex applies them to the new index after the alias swap (basepull does not replay them). While a table isREBUILDING, drift detection is suspended for it. Only one full reindex runs at a time: starting one while another is running fails. A full reindex shows it is running through itsFULL_REINDEXrun records, which stayRUNNINGwithmodifyDaterefreshed after every page. If it is killed mid-run (pod restart, OOM, deploy), recovery is automatic once its run records have shown no sign of life forfullReindexStaleMinutes(30 by default): the next basepull, or the next full reindex, marks those run recordsFAILED, returns the tables toACTIVEwithlastBasepullTimemoved back to the run's start, and turns theAWAITING_REINDEXrowsPENDINGso basepull replays the deletes. No data is lost. A run that was only slow and is judged stopped notices before its alias swap and fails without swapping, or, after its swap, stops applying its captured deletes; basepull skips a replayed delete whose record exists again.
Extending
IndexEventPublisher lets a host replace the synchronous publisher with a queue. In 1.0 the SPI is experimental: IndexEvent carries the table, record id, action and the record snapshot, but no sequence number, so an asynchronous consumer must order INDEX and DELETE for the same record itself. OpenSearchTransportCustomizer customizes the HTTP transport.
Building
./mvnw test # unit tests (memory backend, mocked client)
CI=true ./mvnw verify # plus Testcontainers integration tests and the coverage gate (Docker required)
./mvnw verify -Dopensearch.test.image=opensearchproject/opensearch:3.9.0
Use the wrapper: Maven 3.10 cannot read the published parent POM. The build imports qqq 4.1.0-RC.4 through the default-active qqq-snapshot profile until qbit-build-parent 2.1.0 ships (override with -Dqqq.snapshot.version=...). Without Docker the integration tests are skipped locally; with CI=true they fail instead.
Upgrading from 1.0
See docs/MIGRATION-1.1.md. In short: 1.1 needs the QQQ release with the record-search provider SPI; core search starts using OpenSearch for indexed tables by default, and indexed tables gain searchFields (set serveCoreRecordSearch(false) to keep 1.0 behaviour).
Upgrading from 0.x
See docs/MIGRATION-1.0.md. In short: QQQ 4.1 is required, remove any manual addQBit for this QBit, set schedulerName, prefer opensearchUrl and ${env.} secrets, grant quickSearchAdmin.hasAccess and the table permissions, add the new operational table columns and quickSearchFailedEvent, and run a full reindex once so the index gets the new mapping.
Adopting in another app
Follow Getting started, the Configuration reference and Operational tables (the host application creates the three tables; this QBit ships no DDL), then run a full reindex. Hosts on 1.0 or 0.x should read the upgrade sections above.
See also the QQQ adoption checklist for the full per-feature list (QQQ upgrade, Next dashboard, QBits, migrations, permissions).
License
Apache License, Version 2.0. See LICENSE.