Compare commits

...
Author SHA1 Message Date
james.maesandClaude Fable 5.1 143d865c14 feat(enterprise): quick search over every demo and sample table
Quick search indexed only customer and order, so searching "Illinois" (a
Places state of the sample app) found nothing. The enterprise profile now
registers, through qbit-quick-search 1.1's code registration and
self-priming, every demo table (customer, order, address, the shipping
geo tables) and every visible sample-app table with a record label
(person, pet, carrier, the Places country/state/city/address tables, the
field and adornment labs...). Each gets its record label fields plus the
obvious text fields (name, title, description, email, city, state...),
STRING fields only, so dates, numbers and booleans are never indexed as
text; tables without a label are labelled by name. Priming creates the
index rows and queues each new table for its first build, which the
basepull (now every 60 s) runs, so a fresh index fills at startup and a
running one picks up new tables within a minute. QBit admin tables are
left out.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-01 10:37:46 -06:00
2 changed files with 135 additions and 2 deletions

No files matched your search

@@ -14,6 +14,7 @@ import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -60,6 +61,7 @@ import com.kingsrook.qbits.qacapture.QaCaptureQBitProducer;
import com.kingsrook.qbits.quicksearch.QuickSearchQBitConfig;
import com.kingsrook.qbits.quicksearch.QuickSearchQBitProducer;
import com.kingsrook.qbits.quicksearch.SearchableFieldConfig;
import com.kingsrook.qbits.quicksearch.SearchableTableConfig;
import com.kingsrook.qbits.redis.RedisConnectionConfig;
import com.kingsrook.qbits.redis.RedisQBitConfig;
import com.kingsrook.qbits.redis.RedisQBitProducer;
@@ -85,6 +87,8 @@ import com.kingsrook.qqq.backend.core.jobs.DurableJobsMetaDataProvider;
import com.kingsrook.qqq.backend.core.model.audits.AuditsMetaDataProvider;
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.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData;
@@ -369,8 +373,12 @@ final class EnterpriseFeatures
.withAllowPlaintextCredentials(true)
.withSchedulerName(schedulerName)
.withTableMetaDataCustomizer(tableCustomizer)
.withSearchableTable("customer", List.of(new SearchableFieldConfig("name"), new SearchableFieldConfig("email")))
.withSearchableTable("order", List.of(new SearchableFieldConfig("orderNo"), new SearchableFieldConfig("status")));
// new tables are queued for their first build, which the basepull runs: keep it quick
.withBasepullRepeatSeconds(60);
for(SearchableTableConfig table : searchableTables(i))
{
searchConfig.withSearchableTable(table);
}
new QuickSearchQBitProducer().withConfig(searchConfig).produce(i).addSelfToInstance(i);
});
}
@@ -485,6 +493,72 @@ final class EnterpriseFeatures
/** Text fields worth searching besides a table's record label fields. */
private static final Set<String> SEARCHABLE_FIELD_NAMES = Set.of("name", "label", "title", "description", "email",
"firstName", "lastName", "city", "state", "country", "code", "orderNo", "status");
/** The backends of the demo and sample data (QBit admin tables live on h2 too, and are left out). */
private static final Set<String> SAMPLE_BACKENDS = Set.of("rdbms", "memory");
private static final List<String> DEMO_TABLES = List.of("customer", "order", "address", "shipping_country",
"shipping_stateProvince", "shipping_city");
/*******************************************************************************
** Quick Search registrations for the demo tables and every visible sample-app
** table with a record label: the label fields plus the obvious text fields,
** STRING fields only (OpenSearch indexes them as searchable text; dates,
** numbers and booleans stay out). Self-priming creates their index rows and
** queues each for its first build.
*******************************************************************************/
static List<SearchableTableConfig> searchableTables(QInstance instance)
{
List<SearchableTableConfig> tables = new ArrayList<>();
for(QTableMetaData table : instance.getTables().values())
{
boolean demo = DEMO_TABLES.contains(table.getName());
boolean sample = SAMPLE_BACKENDS.contains(table.getBackendName()) && !table.getIsHidden()
&& table.getRecordLabelFields() != null && !table.getRecordLabelFields().isEmpty();
if(!demo && !sample)
{
continue;
}
Set<String> names = new LinkedHashSet<>();
for(String labelField : Objects.requireNonNullElse(table.getRecordLabelFields(), List.<String>of()))
{
names.add(labelField);
}
names.addAll(SEARCHABLE_FIELD_NAMES);
List<SearchableFieldConfig> fields = new ArrayList<>();
for(String name : names)
{
QFieldMetaData field = table.getFields().get(name);
if(field != null && field.getType() == QFieldType.STRING)
{
fields.add(new SearchableFieldConfig(name).withWeight(Objects.requireNonNullElse(table.getRecordLabelFields(), List.<String>of()).contains(name) ? 3 : 1));
}
}
if(fields.isEmpty())
{
continue;
}
SearchableTableConfig config = new SearchableTableConfig(table.getName(), fields);
if((table.getRecordLabelFields() == null || table.getRecordLabelFields().isEmpty()) && fields.stream().anyMatch(f -> "name".equals(f.getFieldName())))
{
///////////////////////////////////////////////////////////////
// label hits by name where the table itself has no label //
///////////////////////////////////////////////////////////////
config.withRecordLabelFormat("%s", "name");
}
if(!table.getFields().containsKey("modifyDate"))
{
config.withBasepullTimestampField(null);
}
tables.add(config);
}
return (tables);
}
/*******************************************************************************
** The core profile has no apps and several qbits only add tables. Put the
** demo tables in a "QQQ All" app and every visible table that no app reaches
@@ -0,0 +1,59 @@
/*
* Copyright 2026 Kingsrook, LLC
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0
*/
package com.kingsrook.qqq.all;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import com.kingsrook.qbits.quicksearch.SearchableFieldConfig;
import com.kingsrook.qbits.quicksearch.SearchableTableConfig;
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.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryBackendModule;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
/** Quick Search covers the demo and sample tables, with their text fields only. */
class EnterpriseQuickSearchTablesTest
{
@TempDir Path dataDirectory;
@Test
void demoAndSampleTablesWithTheirTextFields() throws Exception
{
QInstance instance = new QqqAllApplication(dataDirectory, "tcp://127.0.0.1:61616").defineQInstance();
///////////////////////////////////////////////////////////////////////
// a stand-in for a sample-app table: labelled, on the sample's //
// backend, with fields OpenSearch must not index as text //
///////////////////////////////////////////////////////////////////////
instance.addBackend(new QBackendMetaData().withName("rdbms").withBackendType(MemoryBackendModule.class));
instance.addTable(new QTableMetaData().withName("countryState").withBackendName("rdbms").withPrimaryKeyField("id")
.withRecordLabelFormat("%s").withRecordLabelFields("name")
.withField(new QFieldMetaData("id", QFieldType.INTEGER))
.withField(new QFieldMetaData("name", QFieldType.STRING))
.withField(new QFieldMetaData("description", QFieldType.STRING))
.withField(new QFieldMetaData("code", QFieldType.INTEGER))
.withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME)));
Map<String, List<String>> tables = EnterpriseFeatures.searchableTables(instance).stream()
.collect(Collectors.toMap(SearchableTableConfig::getTableName,
table -> table.getFields().stream().map(SearchableFieldConfig::getFieldName).toList()));
assertEquals(List.of("name", "email"), tables.get("customer").stream().sorted((a, b) -> a.equals("name") ? -1 : 1).toList());
assertTrue(tables.get("shipping_stateProvince").contains("name"));
assertEquals(List.of("name", "description"), tables.get("countryState"));
assertFalse(tables.containsKey("webhook"), "QBit admin tables on h2 stay out");
assertFalse(tables.containsKey("product"), "the filesystem product table is not a demo search table");
}
}