@strav/search 0.4.30 → 1.0.0-alpha.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +20 -22
- package/src/console/index.ts +5 -0
- package/src/console/search_console_provider.ts +20 -0
- package/src/console/search_flush.ts +49 -0
- package/src/console/search_import.ts +103 -0
- package/src/console/search_list.ts +46 -0
- package/src/console/search_reindex.ts +94 -0
- package/src/drivers/meilisearch/meilisearch_driver.ts +304 -0
- package/src/drivers/memory/memory_driver.ts +344 -0
- package/src/drivers/postgres/apply_search_migration.ts +74 -0
- package/src/drivers/postgres/postgres_fts_driver.ts +493 -135
- package/src/drivers/typesense/typesense_driver.ts +345 -0
- package/src/index.ts +50 -39
- package/src/search_engine.ts +40 -25
- package/src/search_error.ts +86 -0
- package/src/search_manager.ts +112 -94
- package/src/search_provider.ts +68 -6
- package/src/searchable.ts +173 -160
- package/src/searchable_registry.ts +61 -0
- package/src/types.ts +59 -49
- package/README.md +0 -191
- package/src/commands/search_flush.ts +0 -41
- package/src/commands/search_import.ts +0 -43
- package/src/commands/search_optimize.ts +0 -52
- package/src/commands/search_rebuild.ts +0 -73
- package/src/drivers/algolia_driver.ts +0 -170
- package/src/drivers/embedded/embedded_driver.ts +0 -136
- package/src/drivers/embedded/engine/field_registry.ts +0 -97
- package/src/drivers/embedded/engine/fts_query_builder.ts +0 -184
- package/src/drivers/embedded/engine/query_compiler.ts +0 -134
- package/src/drivers/embedded/engine/schema.ts +0 -99
- package/src/drivers/embedded/engine/snippet_formatter.ts +0 -29
- package/src/drivers/embedded/engine/sqlite_engine.ts +0 -255
- package/src/drivers/embedded/engine/typo_expander.ts +0 -138
- package/src/drivers/embedded/errors.ts +0 -15
- package/src/drivers/embedded/filters/filter_compiler.ts +0 -136
- package/src/drivers/embedded/index.ts +0 -3
- package/src/drivers/embedded/storage/paths.ts +0 -23
- package/src/drivers/embedded/types.ts +0 -34
- package/src/drivers/meilisearch_driver.ts +0 -150
- package/src/drivers/null_driver.ts +0 -27
- package/src/drivers/postgres/engine/field_registry.ts +0 -116
- package/src/drivers/postgres/engine/fts_query_builder.ts +0 -105
- package/src/drivers/postgres/engine/pg_engine.ts +0 -300
- package/src/drivers/postgres/engine/query_compiler.ts +0 -165
- package/src/drivers/postgres/engine/schema.ts +0 -187
- package/src/drivers/postgres/engine/snippet_formatter.ts +0 -31
- package/src/drivers/postgres/engine/typo_expander.ts +0 -131
- package/src/drivers/postgres/errors.ts +0 -33
- package/src/drivers/postgres/filters/filter_compiler.ts +0 -138
- package/src/drivers/postgres/index.ts +0 -14
- package/src/drivers/postgres/rebuild/rebuild_inplace.ts +0 -113
- package/src/drivers/postgres/storage/identifiers.ts +0 -46
- package/src/drivers/postgres/types.ts +0 -53
- package/src/drivers/typesense_driver.ts +0 -229
- package/src/errors.ts +0 -18
- package/src/helpers.ts +0 -120
- package/stubs/config/search.ts +0 -57
- package/tsconfig.json +0 -5
package/src/search_manager.ts
CHANGED
|
@@ -1,129 +1,147 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
1
|
+
/**
|
|
2
|
+
* `SearchManager` — the facade apps use to talk to the
|
|
3
|
+
* configured full-text search engine(s).
|
|
4
|
+
*
|
|
5
|
+
* Three concept clusters:
|
|
6
|
+
*
|
|
7
|
+
* - **Engines.** Apps declare engines in `config.search.drivers`;
|
|
8
|
+
* the manager constructs them lazily on first `engine(name)`
|
|
9
|
+
* call. Custom drivers register via `manager.extend(name,
|
|
10
|
+
* factory)`.
|
|
11
|
+
*
|
|
12
|
+
* - **Index naming.** `manager.indexName(name)` prepends the
|
|
13
|
+
* configured `prefix` so apps can namespace per-env or
|
|
14
|
+
* per-app without rewriting their model code. The
|
|
15
|
+
* `searchable()` mixin always resolves through this so apps
|
|
16
|
+
* never embed raw index names in queries.
|
|
17
|
+
*
|
|
18
|
+
* - **Test hooks.** `useEngine(name, engine)` swaps an engine
|
|
19
|
+
* instance for the named slot — typically a `MemoryDriver`
|
|
20
|
+
* in unit tests against code that resolves
|
|
21
|
+
* `engine('meilisearch')`.
|
|
22
|
+
*
|
|
23
|
+
* Multitenancy: the V1 surface is single-tenant. The Postgres
|
|
24
|
+
* driver relies on `app.tenant_id` RLS (same hook
|
|
25
|
+
* `@strav/rag` / `@strav/database` use) for per-tenant
|
|
26
|
+
* isolation. Per-tenant index prefixing for Meili/Typesense
|
|
27
|
+
* lands in V1.1 if asked.
|
|
28
|
+
*/
|
|
16
29
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
30
|
+
// biome-ignore lint/style/useImportType: PostgresDatabase value import for the container path that will wire PostgresFtsDriver.
|
|
31
|
+
import { PostgresDatabase } from '@strav/database'
|
|
32
|
+
// biome-ignore lint/style/useImportType: Application value import for the container handle.
|
|
33
|
+
import { Application, inject } from '@strav/kernel'
|
|
34
|
+
import { MeilisearchDriver } from './drivers/meilisearch/meilisearch_driver.ts'
|
|
35
|
+
import { MemoryDriver } from './drivers/memory/memory_driver.ts'
|
|
36
|
+
import { PostgresFtsDriver } from './drivers/postgres/postgres_fts_driver.ts'
|
|
37
|
+
import { TypesenseDriver } from './drivers/typesense/typesense_driver.ts'
|
|
38
|
+
import { SearchConfigError } from './search_error.ts'
|
|
39
|
+
import type { SearchEngine } from './search_engine.ts'
|
|
40
|
+
import type { DriverConfig, SearchConfig } from './types.ts'
|
|
24
41
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
return SearchManager._config
|
|
32
|
-
}
|
|
42
|
+
export interface SearchManagerOptions {
|
|
43
|
+
config: SearchConfig
|
|
44
|
+
/** Optional — required only if a `postgres-fts` driver is configured. */
|
|
45
|
+
db?: PostgresDatabase
|
|
46
|
+
}
|
|
33
47
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const key = name ?? SearchManager.config.default
|
|
48
|
+
/** Factory for custom drivers — apps register via `manager.extend(name, factory)`. */
|
|
49
|
+
export type EngineFactory = (config: DriverConfig) => SearchEngine
|
|
37
50
|
|
|
38
|
-
|
|
39
|
-
|
|
51
|
+
@inject()
|
|
52
|
+
export class SearchManager {
|
|
53
|
+
readonly config: SearchConfig
|
|
54
|
+
private readonly db: PostgresDatabase | undefined
|
|
55
|
+
private readonly engines = new Map<string, SearchEngine>()
|
|
56
|
+
private readonly extensions = new Map<string, EngineFactory>()
|
|
40
57
|
|
|
41
|
-
|
|
42
|
-
if (!
|
|
43
|
-
throw new
|
|
58
|
+
constructor(options: SearchManagerOptions) {
|
|
59
|
+
if (!options.config.drivers[options.config.default]) {
|
|
60
|
+
throw new SearchConfigError(
|
|
61
|
+
`SearchManager: default driver "${options.config.default}" is not configured.`,
|
|
62
|
+
{
|
|
63
|
+
context: {
|
|
64
|
+
default: options.config.default,
|
|
65
|
+
available: Object.keys(options.config.drivers),
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
)
|
|
44
69
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
SearchManager._engines.set(key, engine)
|
|
48
|
-
return engine
|
|
70
|
+
this.config = options.config
|
|
71
|
+
this.db = options.db
|
|
49
72
|
}
|
|
50
73
|
|
|
51
|
-
|
|
52
|
-
static get prefix(): string {
|
|
53
|
-
return SearchManager._config?.prefix ?? ''
|
|
54
|
-
}
|
|
74
|
+
// ─── Engine management ────────────────────────────────────────────────
|
|
55
75
|
|
|
56
76
|
/**
|
|
57
|
-
* Resolve
|
|
58
|
-
*
|
|
59
|
-
* as `${prefix}t${tenantId}_${name}` so two tenants on the same
|
|
60
|
-
* shared engine cannot read or overwrite each other's documents.
|
|
61
|
-
*
|
|
62
|
-
* Apps that don't need multi-tenant isolation can omit the scope.
|
|
63
|
-
* The driver layer is unchanged — namespacing happens here at the
|
|
64
|
-
* manager boundary.
|
|
77
|
+
* Resolve an engine by name (or the default when omitted).
|
|
78
|
+
* Engines are constructed lazily on first use + memoized.
|
|
65
79
|
*/
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
throw new ConfigurationError(
|
|
76
|
-
`SearchManager.indexName: invalid tenantId ${JSON.stringify(tenantId)} — ` +
|
|
77
|
-
`must match /^[a-zA-Z0-9_-]+$/.`
|
|
78
|
-
)
|
|
80
|
+
engine(name?: string): SearchEngine {
|
|
81
|
+
const key = name ?? this.config.default
|
|
82
|
+
const cached = this.engines.get(key)
|
|
83
|
+
if (cached) return cached
|
|
84
|
+
const cfg = this.config.drivers[key]
|
|
85
|
+
if (!cfg) {
|
|
86
|
+
throw new SearchConfigError(`SearchManager: driver "${key}" is not configured.`, {
|
|
87
|
+
context: { requested: key, available: Object.keys(this.config.drivers) },
|
|
88
|
+
})
|
|
79
89
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
return `t${tenantId}_${name}`
|
|
90
|
+
const engine = this.createEngine(key, cfg)
|
|
91
|
+
this.engines.set(key, engine)
|
|
92
|
+
return engine
|
|
84
93
|
}
|
|
85
94
|
|
|
86
|
-
/** Register a custom driver
|
|
87
|
-
|
|
88
|
-
|
|
95
|
+
/** Register a custom driver. Subsequent `engine(name)` calls resolving to a `driver: <name>` will use it. */
|
|
96
|
+
extend(name: string, factory: EngineFactory): void {
|
|
97
|
+
this.extensions.set(name, factory)
|
|
89
98
|
}
|
|
90
99
|
|
|
91
|
-
/**
|
|
92
|
-
|
|
93
|
-
|
|
100
|
+
/** Hand-wire an engine instance under a name (tests / one-off drivers). */
|
|
101
|
+
useEngine(name: string, engine: SearchEngine): void {
|
|
102
|
+
this.engines.set(name, engine)
|
|
94
103
|
}
|
|
95
104
|
|
|
96
|
-
/**
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
105
|
+
/**
|
|
106
|
+
* Compose the configured prefix with `name`. The `searchable()`
|
|
107
|
+
* mixin always routes through this so apps that toggle the
|
|
108
|
+
* prefix in config don't have to rewrite calls.
|
|
109
|
+
*/
|
|
110
|
+
indexName(name: string): string {
|
|
111
|
+
return this.config.prefix ? `${this.config.prefix}${name}` : name
|
|
101
112
|
}
|
|
102
113
|
|
|
103
|
-
|
|
114
|
+
// ─── Internals ────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
private createEngine(name: string, config: DriverConfig): SearchEngine {
|
|
104
117
|
const driverName = config.driver ?? name
|
|
105
118
|
|
|
106
|
-
const extension =
|
|
119
|
+
const extension = this.extensions.get(driverName)
|
|
107
120
|
if (extension) return extension(config)
|
|
108
121
|
|
|
109
122
|
switch (driverName) {
|
|
123
|
+
case 'memory':
|
|
124
|
+
return new MemoryDriver()
|
|
110
125
|
case 'meilisearch':
|
|
111
126
|
return new MeilisearchDriver(config)
|
|
112
127
|
case 'typesense':
|
|
113
128
|
return new TypesenseDriver(config)
|
|
114
|
-
case 'algolia':
|
|
115
|
-
return new AlgoliaDriver(config)
|
|
116
|
-
case 'embedded':
|
|
117
|
-
return new EmbeddedDriver(config)
|
|
118
|
-
case 'postgres-fts':
|
|
119
129
|
case 'postgres':
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
130
|
+
case 'postgres-fts':
|
|
131
|
+
if (!this.db) {
|
|
132
|
+
throw new SearchConfigError(
|
|
133
|
+
'SearchManager: postgres-fts driver requires PostgresDatabase. Register DatabaseProvider before SearchProvider.',
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
return PostgresFtsDriver.fromConfig(this.db, config)
|
|
123
137
|
default:
|
|
124
|
-
throw new
|
|
125
|
-
`
|
|
138
|
+
throw new SearchConfigError(
|
|
139
|
+
`SearchManager: unknown driver "${driverName}". Register it via \`manager.extend("${driverName}", factory)\`.`,
|
|
140
|
+
{ context: { driver: driverName, name } },
|
|
126
141
|
)
|
|
127
142
|
}
|
|
128
143
|
}
|
|
129
144
|
}
|
|
145
|
+
|
|
146
|
+
/** Public alias for the container-resolution helper apps occasionally pass around. */
|
|
147
|
+
export type SearchManagerResolver = (app: Application) => SearchManager
|
package/src/search_provider.ts
CHANGED
|
@@ -1,16 +1,78 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/**
|
|
2
|
+
* `SearchProvider` — `ServiceProvider` that wires `SearchManager`
|
|
3
|
+
* into the container from `config.search`.
|
|
4
|
+
*
|
|
5
|
+
* Eager construction at boot — a malformed config or a missing
|
|
6
|
+
* postgres dependency should fail before the first call hits.
|
|
7
|
+
* Apps register `DatabaseProvider` before this one when any
|
|
8
|
+
* `postgres-fts` driver is configured; the `dependencies` array
|
|
9
|
+
* declares the order.
|
|
10
|
+
*
|
|
11
|
+
* Config defaults: when `config.search` is absent entirely, the
|
|
12
|
+
* provider boots with the in-process `memory` driver as the
|
|
13
|
+
* default so apps can call `search.engine().createIndex(...)` /
|
|
14
|
+
* `engine().search(...)` in dev without configuration.
|
|
15
|
+
* Production apps override via a real `config/search.ts`.
|
|
16
|
+
*/
|
|
4
17
|
|
|
5
|
-
|
|
6
|
-
|
|
18
|
+
// biome-ignore lint/style/useImportType: PostgresDatabase value import — required when any postgres-fts driver is configured. Loaded conditionally below.
|
|
19
|
+
import { PostgresDatabase } from '@strav/database'
|
|
20
|
+
import { type Application, ConfigError, ConfigRepository, ServiceProvider } from '@strav/kernel'
|
|
21
|
+
import { SearchManager, type SearchManagerOptions } from './search_manager.ts'
|
|
22
|
+
import { SearchableRegistry } from './searchable_registry.ts'
|
|
23
|
+
import type { SearchConfig } from './types.ts'
|
|
24
|
+
|
|
25
|
+
export class SearchProvider extends ServiceProvider {
|
|
26
|
+
override readonly name = 'search'
|
|
7
27
|
override readonly dependencies = ['config']
|
|
8
28
|
|
|
9
29
|
override register(app: Application): void {
|
|
10
|
-
app.singleton(
|
|
30
|
+
app.singleton(SearchableRegistry, () => new SearchableRegistry())
|
|
31
|
+
app.singleton(SearchManager, (c) => {
|
|
32
|
+
const raw = c.resolve(ConfigRepository).get('search') as Partial<SearchConfig> | undefined
|
|
33
|
+
const config = applyDefaults(raw)
|
|
34
|
+
|
|
35
|
+
const opts: SearchManagerOptions = { config }
|
|
36
|
+
|
|
37
|
+
const needsDb = Object.values(config.drivers).some(
|
|
38
|
+
(d) => d.driver === 'postgres-fts' || d.driver === 'postgres',
|
|
39
|
+
)
|
|
40
|
+
if (needsDb) {
|
|
41
|
+
try {
|
|
42
|
+
opts.db = c.resolve(PostgresDatabase)
|
|
43
|
+
} catch (cause) {
|
|
44
|
+
throw new ConfigError(
|
|
45
|
+
'SearchProvider: at least one driver uses `driver: "postgres-fts"` but PostgresDatabase is not registered. Register DatabaseProvider before SearchProvider.',
|
|
46
|
+
{ cause },
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return new SearchManager(opts)
|
|
51
|
+
})
|
|
11
52
|
}
|
|
12
53
|
|
|
13
54
|
override boot(app: Application): void {
|
|
55
|
+
// Force-resolve so config errors surface at boot, not on first call.
|
|
14
56
|
app.resolve(SearchManager)
|
|
15
57
|
}
|
|
16
58
|
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Fill in defaults for omitted config fields. Apps with no
|
|
62
|
+
* `config/search.ts` at all get a working in-memory setup.
|
|
63
|
+
*/
|
|
64
|
+
function applyDefaults(raw: Partial<SearchConfig> | undefined): SearchConfig {
|
|
65
|
+
const config: Partial<SearchConfig> = raw ?? {}
|
|
66
|
+
const drivers = config.drivers ?? { memory: { driver: 'memory' } }
|
|
67
|
+
const def = config.default ?? Object.keys(drivers)[0] ?? 'memory'
|
|
68
|
+
if (!drivers[def]) {
|
|
69
|
+
throw new ConfigError(
|
|
70
|
+
`SearchProvider: default driver "${def}" is not declared in config.search.drivers.`,
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
default: def,
|
|
75
|
+
...(config.prefix !== undefined ? { prefix: config.prefix } : {}),
|
|
76
|
+
drivers,
|
|
77
|
+
}
|
|
78
|
+
}
|