diff --git a/AGENTS.md b/AGENTS.md index 30b675e..3013e19 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,7 +15,7 @@ This file provides guidance to Codex when working with code in this repository. ./mvnw package # Build JAR CI=true ./mvnw verify # Full verify: integration tests (Docker) and the JaCoCo gate ./mvnw verify -Dopensearch.test.image=opensearchproject/opensearch:3.9.0 # other server line -./mvnw verify -Dqqq.snapshot.version=4.1.0-SNAPSHOT # other qqq 4.1 build (profile is active by default) +./mvnw verify -Dqqq.snapshot.version=4.1.0-RC.1 # other qqq 4.1 build (profile is active by default) ``` Integration tests (require Docker) run via `mvn verify` using maven-failsafe-plugin. @@ -34,6 +34,7 @@ Integration tests use `@ExtendWith(RequiresDockerCondition.class)`: skipped with - `listeners/QuickSearchRecordChangeListener` - Real-time path: builds events in the write's transaction, publishes after commit, records failures in `quickSearchFailedEvent`. - `processes/AbstractIndexingStep` - Keyset paging by primary key with display values, run records, drift detection, run purge. `BasepullIndexStep` (replays failed events, start-anchored watermark with overlap), `ReconcileIndexStep` (in place, orphan sweep), `FullReindexStep` (all tables: new physical index plus alias swap; one table: reconcile algorithm). - `actions/QuickSearchAction` - Permission-aware search (table READ filter, `QueryAction` post-filter), bounds, `tableNames`, `limitPerTable`, `tableLabel`. +- `actions/QuickSearchRecordSearchProvider` - QQQ core `RecordSearchProviderInterface` (`POST /qqq/v1/search`): claims indexed, enabled tables; one `_msearch` per call returning ordered record ids per table; bounded by `recordSearchTimeoutMillis`; failures make core fall back and start a 30 s backoff (`QuickSearchRuntime.markRecordSearchFailed`). Registered, with default table `searchFields`, when `serveCoreRecordSearch` is true. - `metadata/*` - Component producers and the table/process helpers. **Tables Produced:** `quickSearchIndex`, `quickSearchIndexRun`, `quickSearchFailedEvent` (plus a PVS over `quickSearchIndex`). @@ -50,7 +51,7 @@ Integration tests use `@ExtendWith(RequiresDockerCondition.class)`: skipped with ## Dependencies - Java 21 -- Parent `com.kingsrook:qbit-build-parent:2.0.0` (qqq 4.0.0). 1.0 needs qqq 4.1, so the `qqq-snapshot` profile is active by default and imports `qqq-bom-pom:4.1.0-RC.1` ahead of the parent's BOM; remove that activation when the parent moves to 4.1 (ADR-0007) +- Parent `com.kingsrook:qbit-build-parent:2.0.0` (qqq 4.0.0). 1.0 needs qqq 4.1, so the `qqq-snapshot` profile is active by default and imports `qqq-bom-pom` ahead of the parent's BOM; remove that activation when the parent moves to 4.1 (ADR-0007). 1.1 imports `4.1.0-RC.2`, the first QQQ release with the record-search provider SPI - OpenSearch Java Client 3.10.0; optional AWS SDK v2 `apache-client` and `sts` for `AWS_SIGV4` - Apache HttpClient5 and jackson-datatype-jsr310, versions managed by the qqq BOM (keep them unpinned so they match its httpcore5 and jackson) - JUnit 5 + AssertJ + Mockito for testing diff --git a/CHANGELOG.md b/CHANGELOG.md index eefe7d0..382163a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ All notable changes to this project are documented here. The format follows Keep a Changelog; versions follow semantic versioning. +## [1.1.0] - Unreleased + +Requires QQQ 4.1.0-RC.2 or later, the first release that carries the record-search provider SPI; built against 4.1.0-RC.2. See `docs/MIGRATION-1.1.md`. + +### Added +- `QuickSearchRecordSearchProvider`: serves QQQ core record search (`RecordSearchAction`, `POST /qqq/v1/search`, the Next UI global search) from OpenSearch for the tables Quick Search indexes. One `_msearch` request with a per-table top-N search (same query, AND semantics and field weights as `QuickSearchAction`); returns record ids only, which core re-reads through `QueryAction` (permissions, record locks, labels). Audit finding F-11, decision D1. +- `serveCoreRecordSearch` (default `true`): registers the provider and gives indexed tables core `searchFields` (visible string and integer searchable fields) when they have none. Host `searchFields` and a host-registered provider are never replaced. +- `recordSearchTimeoutMillis` (default 2500): wall-clock limit on the OpenSearch request; on timeout or any failure core falls back to its own search, and the provider claims no tables for 30 seconds. +- `QuickSearchOpenSearchClient.searchRecordIdsPerTable`. + +### Security +- Fields of type `PASSWORD` are no longer indexed (as hidden fields already were). Reconcile or fully reindex once to remove values a 1.0 index may hold. + ## [1.0.0-RC.2] - 2026-10-06 Second release candidate, published to Maven Central from `release/1.0`. Fixes the five pre-GA findings from the PR #10 review (#13 to #17); the changes are listed under 1.0.0 below. Requires QQQ 4.1. No schema change, but two new status values: `REBUILDING` on `quickSearchIndex.status` and `AWAITING_REINDEX` on `quickSearchFailedEvent.status`; a host that sized its own `quickSearchFailedEvent.status` column needs at least 16 characters. Known gaps before GA are tracked in #12. diff --git a/CLAUDE.md b/CLAUDE.md index 1f8373b..8053359 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,7 @@ This file provides guidance to Claude Code when working with code in this reposi ./mvnw package # Build JAR CI=true ./mvnw verify # Full verify: integration tests (Docker) and the JaCoCo gate ./mvnw verify -Dopensearch.test.image=opensearchproject/opensearch:3.9.0 # other server line -./mvnw verify -Dqqq.snapshot.version=4.1.0-SNAPSHOT # other qqq 4.1 build (profile is active by default) +./mvnw verify -Dqqq.snapshot.version=4.1.0-RC.1 # other qqq 4.1 build (profile is active by default) ``` Integration tests (require Docker) run via `mvn verify` using maven-failsafe-plugin. @@ -34,6 +34,7 @@ Integration tests use `@ExtendWith(RequiresDockerCondition.class)`: skipped with - `listeners/QuickSearchRecordChangeListener` - Real-time path: builds events in the write's transaction, publishes after commit, records failures in `quickSearchFailedEvent`. - `processes/AbstractIndexingStep` - Keyset paging by primary key with display values, run records, drift detection, run purge. `BasepullIndexStep` (replays failed events, start-anchored watermark with overlap), `ReconcileIndexStep` (in place, orphan sweep), `FullReindexStep` (all tables: new physical index plus alias swap; one table: reconcile algorithm). - `actions/QuickSearchAction` - Permission-aware search (table READ filter, `QueryAction` post-filter), bounds, `tableNames`, `limitPerTable`, `tableLabel`. +- `actions/QuickSearchRecordSearchProvider` - QQQ core `RecordSearchProviderInterface` (`POST /qqq/v1/search`): claims indexed, enabled tables; one `_msearch` per call returning ordered record ids per table; bounded by `recordSearchTimeoutMillis`; failures make core fall back and start a 30 s backoff (`QuickSearchRuntime.markRecordSearchFailed`). Registered, with default table `searchFields`, when `serveCoreRecordSearch` is true. - `metadata/*` - Component producers and the table/process helpers. **Tables Produced:** `quickSearchIndex`, `quickSearchIndexRun`, `quickSearchFailedEvent` (plus a PVS over `quickSearchIndex`). @@ -50,7 +51,7 @@ Integration tests use `@ExtendWith(RequiresDockerCondition.class)`: skipped with ## Dependencies - Java 21 -- Parent `com.kingsrook:qbit-build-parent:2.0.0` (qqq 4.0.0). 1.0 needs qqq 4.1, so the `qqq-snapshot` profile is active by default and imports `qqq-bom-pom:4.1.0-RC.1` ahead of the parent's BOM; remove that activation when the parent moves to 4.1 (ADR-0007) +- Parent `com.kingsrook:qbit-build-parent:2.0.0` (qqq 4.0.0). 1.0 needs qqq 4.1, so the `qqq-snapshot` profile is active by default and imports `qqq-bom-pom` ahead of the parent's BOM; remove that activation when the parent moves to 4.1 (ADR-0007). 1.1 imports `4.1.0-RC.2`, the first QQQ release with the record-search provider SPI - OpenSearch Java Client 3.10.0; optional AWS SDK v2 `apache-client` and `sts` for `AWS_SIGV4` - Apache HttpClient5 and jackson-datatype-jsr310, versions managed by the qqq BOM (keep them unpinned so they match its httpcore5 and jackson) - JUnit 5 + AssertJ + Mockito for testing diff --git a/README.md b/README.md index b6758ed..dbc8080 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ 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). +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; 1.1 builds against 4.1.0-RC.2. ## What it does @@ -18,6 +18,7 @@ Requires QQQ 4.1 (the QBit uses 4.1's record-change listeners, after-commit call | 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. | @@ -99,7 +100,7 @@ config.withSearchableTable(new SearchableTableConfig("crmContact", List.of( .withRecordLabelFormat("%s %s", "firstName", "lastName")); ``` -Annotation and config-driven tables can be mixed; a table may appear in only one of them. Hidden fields are never indexed. A table without the change-detection field (`modifyDate` by default) is covered by reconcile instead of incremental basepull. +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. ### Searching @@ -124,6 +125,17 @@ output.getTotalHits(); output.getTotalHitsIsLowerBound(); output.getHasMore(); 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 has `searchFields`, the session may read it, and it supports query, so the producer gives each indexed table that has no `searchFields` its visible string and integer searchable fields (never hidden or password fields). `searchFields` you set yourself are kept. A table must be in the `QInstance` before the QBit is produced to get them. +- **How.** One `_msearch` request with one search per table (Quick Search's query, AND semantics and field weights); each table returns up to 4 x `limitPerTable` record ids (at most 100) in relevance order. Core re-reads them by primary key through `QueryAction`, 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 and `searchFields` untouched. 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 | @@ -159,6 +171,8 @@ Without `schedulerName` the processes exist but nothing runs them; the QBit logs | `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 | | `adminPermissionRules` | `HAS_ACCESS_PERMISSION`, base name `quickSearchAdmin` | Rules for the app and processes | | `tablePermissionRules` | `READ_WRITE_PERMISSIONS` | Rules for the operational tables | | `tableMetaDataCustomizer` | | Applied to every produced table (backend details, for example) | @@ -185,7 +199,11 @@ CI=true ./mvnw verify # plus Testcontainers integration te ./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.1 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. +Use the wrapper: Maven 3.10 cannot read the published parent POM. The build imports qqq 4.1.0-RC.2 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](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 diff --git a/docs/MIGRATION-1.1.md b/docs/MIGRATION-1.1.md new file mode 100644 index 0000000..aeefd88 --- /dev/null +++ b/docs/MIGRATION-1.1.md @@ -0,0 +1,30 @@ +# 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 QQQ `4.1.0-RC.2`. 1.1 builds against `4.1.0-RC.2` through the `qqq-snapshot` profile. + +## What changes at startup + +1. **The QBit registers a record-search provider.** `QuickSearchRecordSearchProvider` becomes the instance's `recordSearchProvider`. If the host already registered a different provider, the QBit keeps it and logs a warning; core search then does not use OpenSearch. +2. **Indexed tables get `searchFields`.** Core only searches tables with `searchFields`. Each indexed table that has none gets its visible string and integer searchable fields. Tables that already have `searchFields` keep them. A table added to the instance after the QBit is produced gets none, so add tables first (as for field validation) or set `searchFields` yourself. +3. **Password fields are no longer indexed.** Fields of type `PASSWORD` are 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 + +```java +new QuickSearchQBitConfig() + // ... + .withServeCoreRecordSearch(false); +``` + +restores 1.0 behaviour: no provider, no `searchFields` changes. diff --git a/docs/TODO.md b/docs/TODO.md index d6bc9a3..cf69cfb 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -9,7 +9,7 @@ - [ ] qqq-all follow-up PR: remove the manual `instance.addQBit(...)` for quick-search in `DemoQBits`, pass `withSchedulerName("demoScheduler")`, set `withStartupMode(QuickSearchStartupMode.DEGRADED)` for the full profile test, pin 1.0.0. - [ ] Voyage: pin 1.0.0, set `schedulerName`, switch to `opensearchUrl` and `${env.}` credentials. - [ ] CI: add a second test job with `-Dopensearch.test.image=opensearchproject/opensearch:3.9.0` (needs a qqq-orb parameter for extra Maven args, or a plain job). -- [ ] Open the qqq issue proposing `RecordSearchProviderInterface` (D1); quick-search adapter lands in 1.1. +- [x] Core SPI proposed as QRun-IO/qqq#1042 (D1); the 1.1 adapter `QuickSearchRecordSearchProvider` is on `feature/record-search-provider`. - [x] Tracking issue #12 created and recorded in the PRD (2026-10-06). - [x] PR #8 merged 2026-10-05; Dependabot PR #1 closed as superseded. - [x] PR #10 merged and `1.0.0-RC.1` published to Maven Central from `release/1.0` (2026-10-05). @@ -26,6 +26,11 @@ - [ ] Stuck `REBUILDING` after a killed or overlapping full reindex (from the PR #20 review): only another all-tables reindex clears it, `AWAITING_REINDEX` rows accumulate and drift detection stays off for those tables. No data is lost. Fix needs a liveness check on the `FULL_REINDEX` run record before reconcile may reset the status, plus refusing a second concurrent full reindex. - [ ] Drift detection still writes `NEEDS_REINDEX` into `status`, so a few-millisecond window can overwrite `REBUILDING`; the durable fix is a separate drift field (schema change, so 1.1 at the earliest). +## Before releasing 1.1.0 + +- [x] Re-pointed the `qqq-snapshot` profile from `4.1.0-SNAPSHOT` to `4.1.0-RC.2`, the first QQQ release with the record-search provider SPI (2026-10-11). +- [ ] qqq-all / Website-Backend: confirm the Next UI search box is served from OpenSearch (provider registered, indexed tables have `searchFields`). + ## Later (1.x) - [ ] Health widget for the admin app (data exists: `isHealthy`, `documentCount`, `lastRunStatus`). diff --git a/pom.xml b/pom.xml index 381f60e..144535b 100644 --- a/pom.xml +++ b/pom.xml @@ -18,7 +18,7 @@ - 1.0.0-SNAPSHOT + 1.1.0-SNAPSHOT 21 21 3.10.0 @@ -264,13 +264,15 @@ + + qqq-snapshot true - 4.1.0-RC.1 + 4.1.0-RC.2 diff --git a/src/main/java/com/kingsrook/qbits/quicksearch/QuickSearchQBitConfig.java b/src/main/java/com/kingsrook/qbits/quicksearch/QuickSearchQBitConfig.java index 01b3da3..b8492de 100644 --- a/src/main/java/com/kingsrook/qbits/quicksearch/QuickSearchQBitConfig.java +++ b/src/main/java/com/kingsrook/qbits/quicksearch/QuickSearchQBitConfig.java @@ -52,14 +52,20 @@ import com.kingsrook.qbits.quicksearch.publisher.IndexEventPublisher; ** enableMaintenanceProcesses, schedulerName plus basepull/reconcile schedules, ** permission rules for the admin surface, search bounds, and indexing limits. ** + ** Core search: serveCoreRecordSearch (default true) registers + ** QuickSearchRecordSearchProvider, so QQQ's POST /qqq/v1/search (the Next UI + ** global search) is served from OpenSearch for indexed tables, bounded by + ** recordSearchTimeoutMillis. + ** ** Tables: searchableEntityClasses (annotated entities) and/or searchableTables ** (config-driven). validate() is list-based and never throws; the producer ** fails production when it reports errors. *******************************************************************************/ public class QuickSearchQBitConfig implements QBitConfig { - public static final String DEFAULT_ADMIN_PERMISSION_BASE_NAME = "quickSearchAdmin"; - public static final String DEFAULT_BASEPULL_TIMESTAMP_FIELD = "modifyDate"; + public static final String DEFAULT_ADMIN_PERMISSION_BASE_NAME = "quickSearchAdmin"; + public static final String DEFAULT_BASEPULL_TIMESTAMP_FIELD = "modifyDate"; + public static final int DEFAULT_RECORD_SEARCH_TIMEOUT_MILLIS = 2500; private static final Pattern INDEX_NAME_PATTERN = Pattern.compile("^[a-z0-9][a-z0-9_+.-]*$"); private static final Set AWS_SERVICE_NAMES = Set.of("es", "aoss"); @@ -100,6 +106,8 @@ public class QuickSearchQBitConfig implements QBitConfig private Integer maxFieldLength = 10000; private Integer maxSearchLimit = 100; private Boolean applyRecordSecurityLocks = true; + private Boolean serveCoreRecordSearch = true; + private Integer recordSearchTimeoutMillis = DEFAULT_RECORD_SEARCH_TIMEOUT_MILLIS; private List> searchableEntityClasses; private List searchableTables; @@ -387,6 +395,7 @@ public class QuickSearchQBitConfig implements QBitConfig requirePositive(maxBulkRequestBytes, "maxBulkRequestBytes", errors); requirePositive(basepullRepeatSeconds, "basepullRepeatSeconds", errors); requirePositive(maxSearchLimit, "maxSearchLimit", errors); + requirePositive(recordSearchTimeoutMillis, "recordSearchTimeoutMillis", errors); requirePositive(maxFieldLength, "maxFieldLength", errors); requirePositive(runHistoryRetentionDays, "runHistoryRetentionDays", errors); @@ -1897,6 +1906,72 @@ public class QuickSearchQBitConfig implements QBitConfig + /******************************************************************************* + ** Getter for serveCoreRecordSearch: register QuickSearchRecordSearchProvider + ** so core RecordSearchAction (POST /qqq/v1/search) searches indexed tables in + ** OpenSearch, and give indexed tables core searchFields when they have none + *******************************************************************************/ + public Boolean getServeCoreRecordSearch() + { + return (this.serveCoreRecordSearch); + } + + + + /******************************************************************************* + ** Setter for serveCoreRecordSearch + *******************************************************************************/ + public void setServeCoreRecordSearch(Boolean serveCoreRecordSearch) + { + this.serveCoreRecordSearch = serveCoreRecordSearch; + } + + + + /******************************************************************************* + ** Fluent setter for serveCoreRecordSearch + *******************************************************************************/ + public QuickSearchQBitConfig withServeCoreRecordSearch(Boolean serveCoreRecordSearch) + { + this.serveCoreRecordSearch = serveCoreRecordSearch; + return (this); + } + + + + /******************************************************************************* + ** Getter for recordSearchTimeoutMillis: wall-clock limit on the OpenSearch + ** request behind a core record search; past it the provider fails and core + ** falls back to its own search (default 2500) + *******************************************************************************/ + public Integer getRecordSearchTimeoutMillis() + { + return (this.recordSearchTimeoutMillis); + } + + + + /******************************************************************************* + ** Setter for recordSearchTimeoutMillis + *******************************************************************************/ + public void setRecordSearchTimeoutMillis(Integer recordSearchTimeoutMillis) + { + this.recordSearchTimeoutMillis = recordSearchTimeoutMillis; + } + + + + /******************************************************************************* + ** Fluent setter for recordSearchTimeoutMillis + *******************************************************************************/ + public QuickSearchQBitConfig withRecordSearchTimeoutMillis(Integer recordSearchTimeoutMillis) + { + this.recordSearchTimeoutMillis = recordSearchTimeoutMillis; + return (this); + } + + + /******************************************************************************* ** Getter for searchableEntityClasses: entity classes annotated with @QuickSearchable diff --git a/src/main/java/com/kingsrook/qbits/quicksearch/QuickSearchQBitProducer.java b/src/main/java/com/kingsrook/qbits/quicksearch/QuickSearchQBitProducer.java index 18c679a..d50b759 100644 --- a/src/main/java/com/kingsrook/qbits/quicksearch/QuickSearchQBitProducer.java +++ b/src/main/java/com/kingsrook/qbits/quicksearch/QuickSearchQBitProducer.java @@ -28,12 +28,16 @@ import java.util.Properties; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerMultiOutput; +import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitMetaData; import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitMetaDataProducer; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; +import com.kingsrook.qbits.quicksearch.actions.QuickSearchRecordSearchProvider; import com.kingsrook.qbits.quicksearch.annotations.QuickSearchField; import com.kingsrook.qbits.quicksearch.annotations.QuickSearchable; import com.kingsrook.qbits.quicksearch.listeners.QuickSearchRecordChangeListener; @@ -50,7 +54,9 @@ import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; ** processes, admin app), applies the config's table customizer and default ** backend, stamps sourceQBitName, then calls {@link #postProduceActions}, which ** discovers searchable tables, builds the runtime, registers the record-change - ** listener and runtime service, and (in FAIL_FAST mode) prepares the index. + ** listener and runtime service, registers the core record-search provider + ** (serveCoreRecordSearch) and gives indexed tables core searchFields, and (in + ** FAIL_FAST mode) prepares the index. ** ** Host usage, unchanged from 0.x: ** new QuickSearchQBitProducer().withConfig(config).produce(qInstance); @@ -254,6 +260,12 @@ public class QuickSearchQBitProducer implements QBitMetaDataProducer tableConfigs) + { + for(QuickSearchableTableConfig tableConfig : tableConfigs) + { + QTableMetaData table = qInstance.getTable(tableConfig.getTableName()); + if(table == null) + { + LOG.warn("Indexed table is not in the instance when Quick Search is produced, so it gets no core searchFields; set them on the table for core search to use it", + logPair("tableName", tableConfig.getTableName())); + continue; + } + + if(table.getSearchFields() != null) + { + continue; + } + + QBackendMetaData backend = qInstance.getBackend(table.getBackendName()); + if(backend != null && Boolean.TRUE.equals(backend.getUsesVariants())) + { + LOG.warn("Indexed table uses backend variants, which core record search does not support; it gets no core searchFields", logPair("tableName", table.getName())); + continue; + } + + List searchFields = new ArrayList<>(); + for(String fieldName : CollectionUtils.nonNullList(tableConfig.getSearchableFields())) + { + QFieldMetaData field = table.getFields() == null ? null : table.getFields().get(fieldName); + if(field != null && !Boolean.TRUE.equals(field.getIsHidden()) && field.getType() != null + && (field.getType().isStringLike() || field.getType().isIntegral()) && !field.getType().needsMasked() + && !searchFields.contains(fieldName)) + { + searchFields.add(fieldName); + } + } + + if(searchFields.isEmpty()) + { + LOG.warn("Indexed table has no string or integer searchable field, so it gets no core searchFields; set them on the table for core search to use it", + logPair("tableName", table.getName())); + continue; + } + + table.setSearchFields(searchFields); + } + } + + + /******************************************************************************* ** Discover searchable tables from @QuickSearchable entity classes. Fails when ** a configured field does not exist on a table that is in the instance. @@ -455,22 +543,23 @@ public class QuickSearchQBitProducer implements QBitMetaDataProducer