@mantiq/search 0.1.0

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 ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@mantiq/search",
3
+ "version": "0.1.0",
4
+ "description": "Full-text search abstraction — Algolia, Meilisearch, Typesense, Elasticsearch, database, and collection drivers",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Abdullah Khan",
8
+ "homepage": "https://github.com/mantiqjs/mantiq/tree/main/packages/search",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/mantiqjs/mantiq.git",
12
+ "directory": "packages/search"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/mantiqjs/mantiq/issues"
16
+ },
17
+ "keywords": [
18
+ "mantiq",
19
+ "mantiqjs",
20
+ "search",
21
+ "algolia",
22
+ "meilisearch",
23
+ "typesense",
24
+ "elasticsearch",
25
+ "full-text-search"
26
+ ],
27
+ "engines": {
28
+ "bun": ">=1.1.0"
29
+ },
30
+ "main": "./src/index.ts",
31
+ "types": "./src/index.ts",
32
+ "exports": {
33
+ ".": {
34
+ "bun": "./src/index.ts",
35
+ "default": "./src/index.ts"
36
+ }
37
+ },
38
+ "files": [
39
+ "src/",
40
+ "package.json",
41
+ "README.md",
42
+ "LICENSE"
43
+ ],
44
+ "scripts": {
45
+ "build": "bun build ./src/index.ts --outdir ./dist --target bun --packages=external",
46
+ "test": "bun test",
47
+ "typecheck": "tsc --noEmit",
48
+ "clean": "rm -rf dist"
49
+ },
50
+ "peerDependencies": {
51
+ "@mantiq/core": "^0.1.0",
52
+ "@mantiq/database": "^0.1.0"
53
+ },
54
+ "devDependencies": {
55
+ "bun-types": "latest",
56
+ "typescript": "^5.7.0",
57
+ "@mantiq/core": "workspace:*",
58
+ "@mantiq/database": "workspace:*"
59
+ }
60
+ }
@@ -0,0 +1,149 @@
1
+ import type { SearchEngine, SearchResult } from './contracts/SearchEngine.ts'
2
+
3
+ export interface WhereClause {
4
+ field: string
5
+ value: any
6
+ }
7
+
8
+ export interface WhereInClause {
9
+ field: string
10
+ values: any[]
11
+ }
12
+
13
+ export interface OrderClause {
14
+ column: string
15
+ direction: 'asc' | 'desc'
16
+ }
17
+
18
+ export interface PaginatedSearchResult<T = any> {
19
+ data: T[]
20
+ total: number
21
+ perPage: number
22
+ currentPage: number
23
+ lastPage: number
24
+ hasMorePages: boolean
25
+ }
26
+
27
+ export class SearchBuilder {
28
+ readonly model: any
29
+ readonly query: string
30
+ readonly wheres: WhereClause[] = []
31
+ readonly whereIns: WhereInClause[] = []
32
+ readonly orders: OrderClause[] = []
33
+
34
+ private _limit: number | null = null
35
+ private _offset: number | null = null
36
+ private _callback: ((engine: SearchEngine, query: string, builder: SearchBuilder) => any) | null = null
37
+ private _engine: SearchEngine
38
+
39
+ constructor(model: any, query: string, engine: SearchEngine, callback?: (engine: SearchEngine, query: string, builder: SearchBuilder) => any) {
40
+ this.model = model
41
+ this.query = query
42
+ this._engine = engine
43
+ this._callback = callback ?? null
44
+ }
45
+
46
+ where(field: string, value: any): this {
47
+ this.wheres.push({ field, value })
48
+ return this
49
+ }
50
+
51
+ whereIn(field: string, values: any[]): this {
52
+ this.whereIns.push({ field, values })
53
+ return this
54
+ }
55
+
56
+ orderBy(column: string, direction: 'asc' | 'desc' = 'asc'): this {
57
+ this.orders.push({ column, direction })
58
+ return this
59
+ }
60
+
61
+ take(count: number): this {
62
+ this._limit = count
63
+ return this
64
+ }
65
+
66
+ limit(count: number): this {
67
+ return this.take(count)
68
+ }
69
+
70
+ skip(count: number): this {
71
+ this._offset = count
72
+ return this
73
+ }
74
+
75
+ offset(count: number): this {
76
+ return this.skip(count)
77
+ }
78
+
79
+ getLimit(): number | null {
80
+ return this._limit
81
+ }
82
+
83
+ getOffset(): number | null {
84
+ return this._offset
85
+ }
86
+
87
+ /** Execute the search and return hydrated models. */
88
+ async get(): Promise<any[]> {
89
+ const results = await this.raw()
90
+ if (results.keys.length === 0) return []
91
+ return this.hydrateModels(results)
92
+ }
93
+
94
+ /** Execute a paginated search and return hydrated models with pagination info. */
95
+ async paginate(perPage = 15, page = 1): Promise<PaginatedSearchResult> {
96
+ const results = await this._engine.paginate(this, perPage, page)
97
+ const data = results.keys.length > 0 ? await this.hydrateModels(results) : []
98
+ const lastPage = Math.max(1, Math.ceil(results.total / perPage))
99
+
100
+ return {
101
+ data,
102
+ total: results.total,
103
+ perPage,
104
+ currentPage: page,
105
+ lastPage,
106
+ hasMorePages: page < lastPage,
107
+ }
108
+ }
109
+
110
+ /** Get raw engine results without hydrating models. */
111
+ async raw(): Promise<SearchResult> {
112
+ if (this._callback) {
113
+ return this._callback(this._engine, this.query, this)
114
+ }
115
+ return this._engine.search(this)
116
+ }
117
+
118
+ /** Get just the matching model keys/IDs. */
119
+ async keys(): Promise<(string | number)[]> {
120
+ const results = await this.raw()
121
+ return results.keys
122
+ }
123
+
124
+ /** Get the total count of matching records. */
125
+ async count(): Promise<number> {
126
+ const results = await this.raw()
127
+ return results.total
128
+ }
129
+
130
+ /** Hydrate model instances from the database using the search result keys. */
131
+ private async hydrateModels(results: SearchResult): Promise<any[]> {
132
+ if (results.keys.length === 0) return []
133
+
134
+ const ModelClass = this.model
135
+ const pk = ModelClass.primaryKey ?? 'id'
136
+
137
+ const models = await ModelClass.query().whereIn(pk, results.keys).get()
138
+
139
+ // Preserve search result ordering
140
+ const modelMap = new Map<string | number, any>()
141
+ for (const m of models) {
142
+ modelMap.set(m.getAttribute(pk), m)
143
+ }
144
+
145
+ return results.keys
146
+ .map((key) => modelMap.get(key))
147
+ .filter(Boolean)
148
+ }
149
+ }
@@ -0,0 +1,95 @@
1
+ import type { SearchEngine } from './contracts/SearchEngine.ts'
2
+ import type { SearchConfig, EngineConfig } from './contracts/SearchConfig.ts'
3
+ import { SearchError } from './errors/SearchError.ts'
4
+
5
+ type EngineFactory = (config: EngineConfig) => SearchEngine
6
+
7
+ export class SearchManager {
8
+ private readonly engines = new Map<string, SearchEngine>()
9
+ private readonly customEngines = new Map<string, EngineFactory>()
10
+
11
+ constructor(
12
+ private readonly config: SearchConfig,
13
+ private readonly builtInEngines: Map<string, EngineFactory> = new Map(),
14
+ ) {}
15
+
16
+ /** Get an engine instance by name, lazy-loaded and cached. */
17
+ driver(name?: string): SearchEngine {
18
+ const engineName = name ?? this.config.default
19
+ if (this.engines.has(engineName)) return this.engines.get(engineName)!
20
+
21
+ const engineConfig = this.config.engines[engineName]
22
+ if (!engineConfig) {
23
+ throw new SearchError(`Search engine "${engineName}" is not configured`, { engine: engineName })
24
+ }
25
+
26
+ const engine = this.createEngine(engineConfig)
27
+ this.engines.set(engineName, engine)
28
+ return engine
29
+ }
30
+
31
+ /** Register a custom engine driver. */
32
+ extend(name: string, factory: EngineFactory): void {
33
+ this.customEngines.set(name, factory)
34
+ }
35
+
36
+ /** Get the default driver name. */
37
+ getDefaultDriver(): string {
38
+ return this.config.default
39
+ }
40
+
41
+ /** Get the search config. */
42
+ getConfig(): SearchConfig {
43
+ return this.config
44
+ }
45
+
46
+ /** Get the index prefix. */
47
+ getPrefix(): string {
48
+ return this.config.prefix
49
+ }
50
+
51
+ /** Forget a cached engine instance. */
52
+ forget(name: string): void {
53
+ this.engines.delete(name)
54
+ }
55
+
56
+ // ── Delegation to default engine ──────────────────────────────────
57
+
58
+ async update(models: any[]): Promise<void> {
59
+ return this.driver().update(models)
60
+ }
61
+
62
+ async delete(models: any[]): Promise<void> {
63
+ return this.driver().delete(models)
64
+ }
65
+
66
+ async flush(indexName: string): Promise<void> {
67
+ return this.driver().flush(indexName)
68
+ }
69
+
70
+ async createIndex(name: string, options?: Record<string, any>): Promise<void> {
71
+ return this.driver().createIndex(name, options)
72
+ }
73
+
74
+ async deleteIndex(name: string): Promise<void> {
75
+ return this.driver().deleteIndex(name)
76
+ }
77
+
78
+ // ── Private ───────────────────────────────────────────────────────
79
+
80
+ private createEngine(config: EngineConfig): SearchEngine {
81
+ const driverName = config.driver
82
+
83
+ // Custom engines first
84
+ if (this.customEngines.has(driverName)) {
85
+ return this.customEngines.get(driverName)!(config)
86
+ }
87
+
88
+ // Built-in engines
89
+ if (this.builtInEngines.has(driverName)) {
90
+ return this.builtInEngines.get(driverName)!(config)
91
+ }
92
+
93
+ throw new SearchError(`Unsupported search driver "${driverName}"`, { driver: driverName })
94
+ }
95
+ }
@@ -0,0 +1,62 @@
1
+ import { getSearchManager } from './helpers/search.ts'
2
+
3
+ /**
4
+ * Model observer that automatically syncs models with the search index.
5
+ * Registered by makeSearchable() on each searchable model class.
6
+ */
7
+ export class SearchObserver {
8
+ async saved(model: any): Promise<void> {
9
+ if (typeof model.shouldBeSearchable === 'function' && !model.shouldBeSearchable()) {
10
+ await this.tryDelete(model)
11
+ return
12
+ }
13
+ await this.tryUpdate(model)
14
+ }
15
+
16
+ async deleted(model: any): Promise<void> {
17
+ await this.tryDelete(model)
18
+ }
19
+
20
+ async forceDeleted(model: any): Promise<void> {
21
+ await this.tryDelete(model)
22
+ }
23
+
24
+ async restored(model: any): Promise<void> {
25
+ await this.tryUpdate(model)
26
+ }
27
+
28
+ private async tryUpdate(model: any): Promise<void> {
29
+ try {
30
+ const manager = getSearchManager()
31
+ const config = manager.getConfig()
32
+
33
+ if (config.queue) {
34
+ // Dispatch to queue if configured
35
+ try {
36
+ const { dispatch } = await import('@mantiq/queue')
37
+ const { MakeSearchableJob } = await import('./jobs/MakeSearchableJob.ts')
38
+ const job = new MakeSearchableJob([model], 'update')
39
+ const queueName = typeof config.queue === 'string' ? config.queue : undefined
40
+ const pending = dispatch(job)
41
+ if (queueName) pending.onQueue(queueName)
42
+ } catch {
43
+ // Queue not available, fall back to sync
44
+ await manager.driver().update([model])
45
+ }
46
+ } else {
47
+ await manager.driver().update([model])
48
+ }
49
+ } catch {
50
+ // Silently fail — search indexing should not break model operations
51
+ }
52
+ }
53
+
54
+ private async tryDelete(model: any): Promise<void> {
55
+ try {
56
+ const manager = getSearchManager()
57
+ await manager.driver().delete([model])
58
+ } catch {
59
+ // Silently fail
60
+ }
61
+ }
62
+ }
@@ -0,0 +1,59 @@
1
+ import { SearchManager } from './SearchManager.ts'
2
+ import { setSearchManager, SEARCH_MANAGER } from './helpers/search.ts'
3
+ import type { SearchConfig } from './contracts/SearchConfig.ts'
4
+ import { DEFAULT_CONFIG } from './contracts/SearchConfig.ts'
5
+ import type { SearchEngine } from './contracts/SearchEngine.ts'
6
+
7
+ export class SearchServiceProvider {
8
+ constructor(private readonly app: any) {}
9
+
10
+ register(): void {
11
+ this.app.singleton(SearchManager, () => {
12
+ const configRepo = this.app.make?.('ConfigRepository') ?? this.app.config?.()
13
+ const config: SearchConfig = configRepo?.get?.('search') ?? DEFAULT_CONFIG
14
+
15
+ const builtInEngines = new Map<string, (cfg: any) => SearchEngine>()
16
+
17
+ builtInEngines.set('collection', () => {
18
+ const { CollectionEngine } = require('./drivers/CollectionEngine.ts')
19
+ return new CollectionEngine()
20
+ })
21
+
22
+ builtInEngines.set('database', (cfg) => {
23
+ const { DatabaseEngine } = require('./drivers/DatabaseEngine.ts')
24
+ return new DatabaseEngine(cfg.connection)
25
+ })
26
+
27
+ builtInEngines.set('algolia', (cfg) => {
28
+ const { AlgoliaEngine } = require('./drivers/AlgoliaEngine.ts')
29
+ return new AlgoliaEngine(cfg.applicationId, cfg.apiKey, cfg.indexSettings)
30
+ })
31
+
32
+ builtInEngines.set('meilisearch', (cfg) => {
33
+ const { MeilisearchEngine } = require('./drivers/MeilisearchEngine.ts')
34
+ return new MeilisearchEngine(cfg.host, cfg.apiKey)
35
+ })
36
+
37
+ builtInEngines.set('typesense', (cfg) => {
38
+ const { TypesenseEngine } = require('./drivers/TypesenseEngine.ts')
39
+ return new TypesenseEngine(cfg.host, cfg.port, cfg.protocol, cfg.apiKey)
40
+ })
41
+
42
+ builtInEngines.set('elasticsearch', (cfg) => {
43
+ const { ElasticsearchEngine } = require('./drivers/ElasticsearchEngine.ts')
44
+ return new ElasticsearchEngine(cfg.hosts, cfg.apiKey, cfg.username, cfg.password)
45
+ })
46
+
47
+ const manager = new SearchManager(config, builtInEngines)
48
+ setSearchManager(manager)
49
+ return manager
50
+ })
51
+
52
+ this.app.alias?.(SearchManager, SEARCH_MANAGER)
53
+ }
54
+
55
+ boot(): void {
56
+ // Service provider boot — nothing needed here.
57
+ // Models opt in to search via makeSearchable() in their booted().
58
+ }
59
+ }
@@ -0,0 +1,89 @@
1
+ import { SearchBuilder } from './SearchBuilder.ts'
2
+ import { SearchObserver } from './SearchObserver.ts'
3
+ import { getSearchManager } from './helpers/search.ts'
4
+
5
+ /**
6
+ * Makes a Model class searchable. Call in static booted().
7
+ *
8
+ * Adds: Model.search(query), Model.makeAllSearchable(), Model.removeAllFromSearch(),
9
+ * instance.toSearchableArray(), instance.searchableKey(), instance.shouldBeSearchable()
10
+ */
11
+ export function makeSearchable(ModelClass: any): void {
12
+ // ── Static methods ──────────────────────────────────────────────
13
+
14
+ /** Start a search query against the model's search index. */
15
+ ModelClass.search = function (query: string, callback?: any): SearchBuilder {
16
+ const manager = getSearchManager()
17
+ return new SearchBuilder(this, query, manager.driver(), callback)
18
+ }
19
+
20
+ /** Get the search index name for this model. */
21
+ if (!ModelClass.searchableAs) {
22
+ ModelClass.searchableAs = function (): string {
23
+ const prefix = getSearchManager().getPrefix()
24
+ return prefix + (this.table ?? this.name.toLowerCase() + 's')
25
+ }
26
+ }
27
+
28
+ /** Import all model records into the search index. */
29
+ ModelClass.makeAllSearchable = async function (chunkSize = 500): Promise<void> {
30
+ const manager = getSearchManager()
31
+ const engine = manager.driver()
32
+ let offset = 0
33
+
34
+ while (true) {
35
+ const models = await this.query().limit(chunkSize).offset(offset).get()
36
+ if (models.length === 0) break
37
+
38
+ const searchable = models.filter((m: any) =>
39
+ typeof m.shouldBeSearchable === 'function' ? m.shouldBeSearchable() : true,
40
+ )
41
+
42
+ if (searchable.length > 0) {
43
+ await engine.update(searchable)
44
+ }
45
+
46
+ if (models.length < chunkSize) break
47
+ offset += chunkSize
48
+ }
49
+ }
50
+
51
+ /** Remove all model records from the search index. */
52
+ ModelClass.removeAllFromSearch = async function (): Promise<void> {
53
+ const manager = getSearchManager()
54
+ const indexName = this.searchableAs()
55
+ await manager.driver().flush(indexName)
56
+ }
57
+
58
+ // ── Instance methods (defaults, overridable by the model) ───────
59
+
60
+ if (!ModelClass.prototype.toSearchableArray) {
61
+ ModelClass.prototype.toSearchableArray = function (): Record<string, any> {
62
+ return this.toObject ? this.toObject() : { ...this.attributes }
63
+ }
64
+ }
65
+
66
+ if (!ModelClass.prototype.searchableKey) {
67
+ ModelClass.prototype.searchableKey = function (): string | number {
68
+ const pk = (this.constructor as any).primaryKey ?? 'id'
69
+ return this.getAttribute(pk)
70
+ }
71
+ }
72
+
73
+ if (!ModelClass.prototype.shouldBeSearchable) {
74
+ ModelClass.prototype.shouldBeSearchable = function (): boolean {
75
+ return true
76
+ }
77
+ }
78
+
79
+ // ── Register observer for auto-indexing ─────────────────────────
80
+
81
+ try {
82
+ const observer = new SearchObserver()
83
+ if (typeof ModelClass.observe === 'function') {
84
+ ModelClass.observe(observer)
85
+ }
86
+ } catch {
87
+ // Events package may not be available — manual indexing only
88
+ }
89
+ }
@@ -0,0 +1,22 @@
1
+ import { getSearchManager } from '../helpers/search.ts'
2
+
3
+ export class SearchDeleteIndexCommand {
4
+ readonly name = 'search:delete-index'
5
+ readonly description = 'Delete a search index'
6
+ readonly usage = 'search:delete-index <name>'
7
+
8
+ async handle(args: { positionals: string[] }, io: any): Promise<void> {
9
+ const name = args.positionals[0]
10
+ if (!name) {
11
+ io.error('Please provide an index name: search:delete-index <name>')
12
+ return
13
+ }
14
+
15
+ try {
16
+ await getSearchManager().deleteIndex(name)
17
+ io.success(`Index "${name}" deleted.`)
18
+ } catch (err) {
19
+ io.error(`Failed to delete index: ${err instanceof Error ? err.message : String(err)}`)
20
+ }
21
+ }
22
+ }
@@ -0,0 +1,44 @@
1
+ import { getSearchManager } from '../helpers/search.ts'
2
+
3
+ export class SearchFlushCommand {
4
+ readonly name = 'search:flush'
5
+ readonly description = 'Remove all records for a model from the search index'
6
+ readonly usage = 'search:flush <model>'
7
+
8
+ async handle(args: { positionals: string[] }, io: any): Promise<void> {
9
+ const modelName = args.positionals[0]
10
+ if (!modelName) {
11
+ io.error('Please provide a model name: search:flush <model>')
12
+ return
13
+ }
14
+
15
+ io.info(`Flushing ${modelName} from search index...`)
16
+
17
+ try {
18
+ const ModelClass = await this.resolveModel(modelName)
19
+ if (typeof ModelClass.removeAllFromSearch === 'function') {
20
+ await ModelClass.removeAllFromSearch()
21
+ io.success(`All ${modelName} records have been removed from the search index.`)
22
+ } else {
23
+ io.error(`${modelName} is not searchable. Call makeSearchable() in its booted() method.`)
24
+ }
25
+ } catch (err) {
26
+ io.error(`Failed to flush: ${err instanceof Error ? err.message : String(err)}`)
27
+ }
28
+ }
29
+
30
+ private async resolveModel(name: string): Promise<any> {
31
+ const paths = [
32
+ `../../app/Models/${name}.ts`,
33
+ `../../Models/${name}.ts`,
34
+ `../../app/Models/${name}`,
35
+ ]
36
+ for (const p of paths) {
37
+ try {
38
+ const mod = await import(p)
39
+ return mod[name] ?? mod.default
40
+ } catch { /* try next */ }
41
+ }
42
+ throw new Error(`Could not resolve model "${name}".`)
43
+ }
44
+ }
@@ -0,0 +1,45 @@
1
+ import { getSearchManager } from '../helpers/search.ts'
2
+
3
+ export class SearchImportCommand {
4
+ readonly name = 'search:import'
5
+ readonly description = 'Import all records for a model into the search index'
6
+ readonly usage = 'search:import <model>'
7
+
8
+ async handle(args: { positionals: string[] }, io: any): Promise<void> {
9
+ const modelName = args.positionals[0]
10
+ if (!modelName) {
11
+ io.error('Please provide a model name: search:import <model>')
12
+ return
13
+ }
14
+
15
+ io.info(`Importing ${modelName}...`)
16
+
17
+ try {
18
+ const ModelClass = await this.resolveModel(modelName)
19
+ if (typeof ModelClass.makeAllSearchable === 'function') {
20
+ await ModelClass.makeAllSearchable()
21
+ io.success(`All ${modelName} records have been imported.`)
22
+ } else {
23
+ io.error(`${modelName} is not searchable. Call makeSearchable() in its booted() method.`)
24
+ }
25
+ } catch (err) {
26
+ io.error(`Failed to import: ${err instanceof Error ? err.message : String(err)}`)
27
+ }
28
+ }
29
+
30
+ private async resolveModel(name: string): Promise<any> {
31
+ // Try common model paths
32
+ const paths = [
33
+ `../../app/Models/${name}.ts`,
34
+ `../../Models/${name}.ts`,
35
+ `../../app/Models/${name}`,
36
+ ]
37
+ for (const p of paths) {
38
+ try {
39
+ const mod = await import(p)
40
+ return mod[name] ?? mod.default
41
+ } catch { /* try next */ }
42
+ }
43
+ throw new Error(`Could not resolve model "${name}". Ensure it exists in app/Models/.`)
44
+ }
45
+ }
@@ -0,0 +1,22 @@
1
+ import { getSearchManager } from '../helpers/search.ts'
2
+
3
+ export class SearchIndexCommand {
4
+ readonly name = 'search:index'
5
+ readonly description = 'Create a search index'
6
+ readonly usage = 'search:index <name>'
7
+
8
+ async handle(args: { positionals: string[] }, io: any): Promise<void> {
9
+ const name = args.positionals[0]
10
+ if (!name) {
11
+ io.error('Please provide an index name: search:index <name>')
12
+ return
13
+ }
14
+
15
+ try {
16
+ await getSearchManager().createIndex(name)
17
+ io.success(`Index "${name}" created.`)
18
+ } catch (err) {
19
+ io.error(`Failed to create index: ${err instanceof Error ? err.message : String(err)}`)
20
+ }
21
+ }
22
+ }
@@ -0,0 +1,30 @@
1
+ export type EngineConfig =
2
+ | { driver: 'collection' }
3
+ | { driver: 'database'; connection?: string }
4
+ | { driver: 'algolia'; applicationId: string; apiKey: string; indexSettings?: Record<string, any> }
5
+ | { driver: 'meilisearch'; host: string; apiKey: string }
6
+ | { driver: 'typesense'; host: string; port: number; protocol: 'http' | 'https'; apiKey: string }
7
+ | { driver: 'elasticsearch'; hosts: string[]; apiKey?: string; username?: string; password?: string }
8
+
9
+ export interface SearchConfig {
10
+ /** Default engine name */
11
+ default: string
12
+ /** Index name prefix (e.g., 'prod_') */
13
+ prefix: string
14
+ /** false = sync, true = default queue, string = named queue */
15
+ queue: boolean | string
16
+ /** Include __soft_deleted field in indexed data */
17
+ softDelete: boolean
18
+ /** Engine configurations keyed by name */
19
+ engines: Record<string, EngineConfig>
20
+ }
21
+
22
+ export const DEFAULT_CONFIG: SearchConfig = {
23
+ default: 'collection',
24
+ prefix: '',
25
+ queue: false,
26
+ softDelete: false,
27
+ engines: {
28
+ collection: { driver: 'collection' },
29
+ },
30
+ }