forge-recovery a709dd5f47
QRun-IO/ci-policy: ci / fork-guard (push) Skipped
QRun-IO/ci-policy: ci / head-profile (push) Skipped
QRun-IO/ci-policy: ci / application-template-0 (push) Skipped
QRun-IO/ci-policy: ci / gradle-1 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (push) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (push) Skipped
QRun-IO/ci-policy: ci / node-8 (push) Skipped
QRun-IO/ci-policy: ci / node-9 (push) Skipped
QRun-IO/ci-policy: ci / node-10 (push) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (push) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (push) Skipped
QRun-IO/ci-policy: ci / source-config-13 (push) Skipped
QRun-IO/ci-policy: ci / source-config-14 (push) Skipped
QRun-IO/ci-policy: ci / android-0 (push) Skipped
QRun-IO/ci-policy: ci / original-mcp (push) Skipped
QRun-IO/ci-policy: ci / not-enrolled (push) Skipped
QRun-IO/ci-policy: native / fork-deny (push) Skipped
QRun-IO/ci-policy: native / dotnet-0 (push) Skipped
Shared Java Maven CI with Docker / secrets (push) Successful in 27s
QRun-IO/ci-policy: native / native-result (push) Successful in 7s
QRun-IO/ci-policy: Shared Java Maven CI with Docker / secrets (push) Successful in 24s
QRun-IO/ci-policy: ci / repository-policy (push) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (push) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (push) Skipped
QRun-IO/ci-policy: Shared Java Maven CI with Docker / verify (push) Successful in 1m30s
Shared Java Maven CI with Docker / verify (push) Successful in 2m3s
QRun-IO/ci-policy: Shared Java Maven CI with Docker / gate (push) Successful in 12s
QRun-IO/ci-policy: ci / java-docker-3 (push) Successful in 43m2s
Shared Java Maven CI with Docker / gate (push) Successful in 13s
qrun-release / release (push) Successful in 43m31s
QRun-IO/ci-policy: ci / ci-result (push) Successful in 4s
Shared QRun source version / version (push) Failing after 10m19s
qrun-release / postpublish (push) Failing after 10m18s
ci: enroll shared Gitea lifecycle
2026-11-02 22:20:45 +00:00
2026-03-30 06:48:59 -05:00
2026-03-30 06:48:59 -05:00
2026-03-30 06:48:59 -05:00

QBit: Quick Search

Version License Java

OpenSearch-Powered Full-Text Search for QQQ Applications

This QBit provides global search across QQQ application tables via OpenSearch. Annotate entity classes or declare tables via config, configure a connection, produce the QBit, and gain real-time indexing, scheduled basepull catch-up, per-field relevance boosting, and a pagination-aware search API.

Core Capabilities

  • Real-Time Indexing: Post-insert, post-update, and post-delete customizers index records immediately on write
  • Basepull Safety Net: Scheduled process catches records missed during downtime or bulk imports
  • Field-Level Boosting: @QuickSearchField(weight = N) controls relevance ranking per field
  • Edge-Ngram Typeahead: Custom analyzer enables prefix matching ("ord" finds "order")
  • Paginated Search: QuickSearchAction returns results with scores, highlights, total hits, and hasMore flag
  • Config-Driven Indexing: SearchableTableConfig API indexes tables from third-party QBit jars without modifying their source
  • Extensible Publisher: IndexEventPublisher interface allows swapping in RabbitMQ, Kafka, or any async transport
  • Admin Observability: Operational tables track index status and run history

Open Source & Full Control

QBit Quick Search is 100% open source under Apache 2.0. All data stays in your OpenSearch cluster.

Architecture

Design Principles

  1. Real-time first, basepull as safety net: Table customizers index records synchronously on every insert, update, and delete. The basepull process runs on a schedule as a catch-up mechanism for bulk imports, OpenSearch downtime, or any records the real-time path missed.

  2. Annotation and config-driven discovery: Entity classes marked with @QuickSearchable and @QuickSearchField are discovered at produce time. Tables from third-party jars can be declared via SearchableTableConfig in code. Both paths merge during produce().

  3. Extensible transport: The IndexEventPublisher strategy interface decouples the write path from the indexing path. The default SynchronousIndexEventPublisher writes directly to OpenSearch. Consumers can provide a queue-backed implementation for async indexing.

Technology Stack

  • Java 21 with QQQ backend modules
  • OpenSearch 2.x for full-text search with custom edge-ngram analyzer
  • QQQ Framework: Entities, processes, customizers, permissions, API layer

Module Organization

qbit-quick-search/
  src/main/java/com/kingsrook/qbits/quicksearch/
    QuickSearchQBitConfig.java        -- Configuration and validation
    QuickSearchQBitProducer.java      -- Produces tables, processes, customizers
    QuickSearchQBitContext.java       -- Static runtime context
    QuickSearchableTableConfig.java   -- Per-table index configuration
    SearchableFieldConfig.java        -- Config-driven field definition
    SearchableTableConfig.java        -- Config-driven table definition
    annotations/                      -- @QuickSearchable, @QuickSearchField
    model/                            -- QuickSearchIndex, QuickSearchIndexRun entities
    opensearch/                       -- OpenSearch client, document model, bulk results
    actions/                          -- QuickSearchAction, input/output DTOs
    processes/                        -- AbstractIndexingStep, BasepullIndexStep, FullReindexStep
    customizers/                      -- Post-insert, post-update, post-delete customizers
    publisher/                        -- IndexEventPublisher interface, SynchronousIndexEventPublisher

Getting Started

Prerequisites

  • Java 21+
  • Maven 3.8+
  • OpenSearch 2.x instance
  • QQQ Application (this is a QBit, not a standalone application)

Usage

Maven dependency

<dependency>
    <groupId>com.kingsrook.qbits</groupId>
    <artifactId>qbit-quick-search</artifactId>
    <version>0.2.0-SNAPSHOT</version>
</dependency>

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;
}

Minimal setup

QuickSearchQBitConfig config = new QuickSearchQBitConfig()
   .withBackendName("yourBackendName")
   .withOpensearchHost("localhost")
   .withOpensearchPort(9200)
   .withOpensearchIndexName("my-app-search")
   .withSearchableEntityClasses(List.of(Customer.class, Order.class));

new QuickSearchQBitProducer()
   .withConfig(config)
   .produce(qInstance);

With authentication and SSL

QuickSearchQBitConfig config = new QuickSearchQBitConfig()
   .withBackendName("yourBackendName")
   .withOpensearchHost("search.example.com")
   .withOpensearchPort(443)
   .withUseSsl(true)
   .withOpensearchUsername("admin")
   .withOpensearchPassword("secretPassword")
   .withOpensearchIndexName("my-app-search")
   .withSearchableEntityClasses(List.of(Customer.class));

With custom publisher (async indexing)

QuickSearchQBitConfig config = new QuickSearchQBitConfig()
   .withBackendName("yourBackendName")
   .withOpensearchHost("localhost")
   .withOpensearchPort(9200)
   .withOpensearchIndexName("my-app-search")
   .withSearchableEntityClasses(List.of(Customer.class))
   .withIndexEventPublisher(new RabbitMqIndexEventPublisher(connectionFactory));

Config-driven table indexing

When tables come from third-party QBit jars (like qbit-crm or qbit-wms), you cannot add annotations to their entity classes. Use SearchableTableConfig and SearchableFieldConfig to declare those tables as searchable in your host application config.

QuickSearchQBitConfig config = new QuickSearchQBitConfig()
   .withBackendName("rdbms")
   .withOpensearchHost("localhost")
   .withOpensearchPort(9200)
   .withOpensearchIndexName("voyage-search")
   .withSearchableTable("crmContact", List.of(
      new SearchableFieldConfig("firstName").withWeight(2),
      new SearchableFieldConfig("lastName").withWeight(2),
      new SearchableFieldConfig("email").withWeight(3),
      new SearchableFieldConfig("phone").withWeight(1)))
   .withSearchableTable(new SearchableTableConfig("crmFormSubmission", List.of(
      new SearchableFieldConfig("email").withWeight(3),
      new SearchableFieldConfig("firstName").withWeight(1),
      new SearchableFieldConfig("message").withWeight(1)))
      .withRecordLabelFormat("%s - %s", "formType", "email"));

withSearchableTable() accepts either a (String tableName, List<SearchableFieldConfig> fields) shorthand or a full SearchableTableConfig for advanced options.

SearchableFieldConfig
Method Description
new SearchableFieldConfig(fieldName) Constructor; QQQ field name is required
.withWeight(int) Boost factor for relevance ranking (default 1)
.withIncludeLabel(boolean) Prefix indexed text with the field label (default false)
SearchableTableConfig
Method Description
new SearchableTableConfig(tableName, fields) Constructor; table name and at least one field required
.withBasepullIntervalMinutes(int) Override basepull schedule (defaults to config-level setting)
.withBasepullTimestampField(String) Change detection field (default "modifyDate")
.withEnabledByDefault(boolean) Whether indexing starts active (default true)
.withRecordLabelFormat(String format, String... fields) String.format pattern and field names for search result display label
Record label override

By default, search results use QQQ's record label for display. Config-driven tables can override this with withRecordLabelFormat(). The format string is passed to String.format() with field values pulled from the record in the order specified.

new SearchableTableConfig("crmFormSubmission", fields)
   .withRecordLabelFormat("%s - %s", "formType", "email");
// produces labels like "Contact Form - user@example.com"

Mixing annotations and config

Annotations and config-driven declarations can be used together. Both paths merge during produce(). Each table must appear in only one source; duplicate table names across annotation and config sources will produce a validation error.

QuickSearchQBitConfig config = new QuickSearchQBitConfig()
   .withBackendName("rdbms")
   .withOpensearchHost("localhost")
   .withOpensearchPort(9200)
   .withOpensearchIndexName("my-app-search")
   .withSearchableEntityClasses(List.of(Customer.class, Order.class))
   .withSearchableTable("crmContact", List.of(
      new SearchableFieldConfig("firstName").withWeight(2),
      new SearchableFieldConfig("lastName").withWeight(2),
      new SearchableFieldConfig("email").withWeight(3)));

Searching

Once the QBit is produced, use QuickSearchAction to execute searches:

QuickSearchOutput output = new QuickSearchAction().execute(
   new QuickSearchInput()
      .withSearchTerm("widget")
      .withLimit(25)
      .withOffset(0));

// Check results
System.out.println("Total hits: " + output.getTotalHits());
System.out.println("Has more: " + output.getHasMore());

for(QuickSearchResult result : output.getResults())
{
   System.out.println(result.getTableName() + ":" + result.getRecordId()
      + " - " + result.getRecordLabel()
      + " (score: " + result.getScore() + ")");
}

To filter results to a single table:

QuickSearchOutput output = new QuickSearchAction().execute(
   new QuickSearchInput()
      .withSearchTerm("blue")
      .withTableName("customer")
      .withLimit(10));

Triggering a Full Reindex

The full reindex process can be triggered programmatically:

RunBackendStepInput input = new RunBackendStepInput();
input.addValue("tableName", "customer"); // optional: omit to reindex all tables
RunBackendStepOutput output = new RunBackendStepOutput();
new FullReindexStep().run(input, output);

Or through the QQQ admin UI via the "Full Reindex" process.

Data Model

Tables (2)

Table Description
quickSearchIndex Per-table index configuration, status, and basepull tracking
quickSearchIndexRun Individual indexing run history with record counts and error tracking

Processes (2)

Process Description
Basepull Index Scheduled: queries each source table for records modified since last run, indexes in batches
Full Reindex On-demand: deletes and re-indexes all records for one or all tables

Configuration Reference

Field Type Default Description
backendName String required QQQ backend for operational tables
opensearchHost String required OpenSearch hostname
opensearchPort Integer required OpenSearch port
opensearchIndexName String required OpenSearch index name
searchableEntityClasses List<Class<?>> none Entity classes with @QuickSearchable
searchableTables List<SearchableTableConfig> none Config-driven table definitions
opensearchUsername String none HTTP basic auth username
opensearchPassword String none HTTP basic auth password
useSsl Boolean false Use HTTPS
tableNamePrefix String none Prefix for produced table names
enableRealTimeIndexing Boolean true Register post-insert/update/delete customizers
enableScheduledProcesses Boolean true Register scheduled basepull process
defaultBasepullIntervalMinutes Integer 5 Default basepull schedule
bulkBatchSize Integer 500 Documents per OpenSearch bulk request
sourceBatchSize Integer 1000 Records per source table query batch
indexEventPublisher IndexEventPublisher sync Custom publisher for async transport

At least one of searchableEntityClasses or searchableTables must be provided. Both can be used together.

Annotations

@QuickSearchable

Attribute Type Default Description
tableName String required QQQ table name
fields String[] {} Fallback field list if no @QuickSearchField annotations
basepullIntervalMinutes int 5 Basepull schedule for this table
basepullTimestampField String "modifyDate" Field for change detection
enabledByDefault boolean true Active on first produce

@QuickSearchField

Attribute Type Default Description
weight int 1 Boost factor for search relevance
includeLabel boolean false Prefix value with field label in indexed text

Companion QBit Integration

qbit-crm

Index crmContact, crmCompany, and crmDeal for full-text global search across CRM records.

qbit-wms

Index wmsItem and wmsLocation for warehouse item and location search.

Testing

mvn test                    # Run all 174 unit tests
mvn verify                  # Run unit tests + integration tests (requires Docker)
mvn test -Dtest=ClassName   # Run single test class

Coverage

  • 70%+ instruction coverage (JaCoCo enforced)
  • 90%+ class coverage (JaCoCo enforced)
  • All unit tests run in-memory via MemoryRecordStore (no OpenSearch required)
  • Integration tests use Testcontainers with real OpenSearch

Documentation

Contributing

QBit Quick Search is open source and welcomes contributions.

About Kingsrook

QBit Quick Search is built by Kingsrook - making engineers more productive through intelligent automation and developer tools.

License

This project is licensed under the Apache License, Version 2.0 - see the LICENSE file for details.

S
Description
QQQ QBit: OpenSearch-backed full-text search across a QQQ app's tables (real-time indexing, reconcile, zero-blackout rebuild, permission-aware search, core /qqq/v1/search provider)
Readme
5.4 MiB
0 Stars 3 Watchers 0 Forks
v0.2.0
Latest
2026-03-30 11:49:29 +00:00
Languages
Java 100%