@mframework/layer-search 0.0.1
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/README.md +269 -0
- package/app/components/README.md +3 -0
- package/app/components/features/autocomplete.vue +63 -0
- package/app/components/features/searchkitSearch.vue +115 -0
- package/app/components/filters/filters.vue +0 -0
- package/app/components/molecules/SearchInput.vue +39 -0
- package/app/components/molecules/pagination.vue +21 -0
- package/app/components/molecules/resultList.vue +48 -0
- package/app/components/search.vue +87 -0
- package/app/composables/adapter/mock.ts +26 -0
- package/app/composables/adapter/types.ts +21 -0
- package/app/composables/bridges/instantsearch.ts +21 -0
- package/app/composables/bridges/react.ts +39 -0
- package/app/composables/bridges/searchkit-server.ts +51 -0
- package/app/composables/bridges/searchkit.ts +88 -0
- package/app/composables/bridges/vue.ts +38 -0
- package/app/composables/cli.ts +70 -0
- package/app/composables/config/schema.ts +16 -0
- package/app/composables/config.ts +20 -0
- package/app/composables/core/Facets.ts +9 -0
- package/app/composables/core/Filters.ts +13 -0
- package/app/composables/core/Pipeline.ts +20 -0
- package/app/composables/core/QueryBuilder.ts +27 -0
- package/app/composables/core/SearchContext.ts +54 -0
- package/app/composables/core/SearchManager.ts +26 -0
- package/app/composables/events.ts +5 -0
- package/app/composables/index.ts +12 -0
- package/app/composables/module.ts +48 -0
- package/app/composables/types/api/global-search.ts +8 -0
- package/app/composables/types.d.ts +12 -0
- package/app/composables/useSearchkit.ts +218 -0
- package/app/composables/utils/health.ts +13 -0
- package/app/composables/utils/normalizers.ts +6 -0
- package/app/pages/results.vue +85 -0
- package/app/plugins/instantsearch.js +35 -0
- package/app/plugins/search.js +103 -0
- package/app/plugins/searchClient.ts +108 -0
- package/app/utils/env.ts +28 -0
- package/app/utils/search/client.ts +53 -0
- package/nuxt.config.ts +11 -0
- package/package.json +36 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { BuiltSearchQuery } from '../core/QueryBuilder'
|
|
2
|
+
|
|
3
|
+
export interface MeeoviSearchItem {
|
|
4
|
+
id: string
|
|
5
|
+
title: string
|
|
6
|
+
description?: string
|
|
7
|
+
price?: number
|
|
8
|
+
[key: string]: any
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type MeeoviSearchAdapter = {
|
|
12
|
+
id?: string
|
|
13
|
+
type?: string
|
|
14
|
+
config?: any
|
|
15
|
+
search(query: BuiltSearchQuery | any): Promise<{
|
|
16
|
+
items: MeeoviSearchItem[]
|
|
17
|
+
total: number
|
|
18
|
+
page?: number
|
|
19
|
+
pageSize?: number
|
|
20
|
+
}>
|
|
21
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { SearchManager } from '../core/SearchManager'
|
|
2
|
+
import type { MeeoviSearchItem } from '../adapter/types'
|
|
3
|
+
|
|
4
|
+
export function createInstantSearchBridge(manager: SearchManager) {
|
|
5
|
+
return {
|
|
6
|
+
async searchFunction(helper: any) {
|
|
7
|
+
manager.context.setQuery(helper.state.query || '')
|
|
8
|
+
manager.context.setPage(helper.state.page || 1)
|
|
9
|
+
// map filters if needed from helper.state
|
|
10
|
+
|
|
11
|
+
return manager.search().then((result: any) => {
|
|
12
|
+
helper.setResults({
|
|
13
|
+
hits: result.items,
|
|
14
|
+
nbHits: result.total,
|
|
15
|
+
page: result.page - 1,
|
|
16
|
+
hitsPerPage: result.pageSize
|
|
17
|
+
})
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { useState, useCallback } from 'react'
|
|
2
|
+
import type { SearchManager } from '../core/SearchManager'
|
|
3
|
+
|
|
4
|
+
// React bridge: accept a SearchManager instance to stay adapter-agnostic.
|
|
5
|
+
export function useReactSearch(manager?: SearchManager) {
|
|
6
|
+
if (!manager) throw new Error('SearchManager instance is required for React bridge')
|
|
7
|
+
|
|
8
|
+
const [query, setQuery] = useState(manager.context.state.query)
|
|
9
|
+
const [page, setPage] = useState(manager.context.state.page)
|
|
10
|
+
const [pageSize, setPageSize] = useState(manager.context.state.pageSize)
|
|
11
|
+
const [results, setResults] = useState<any[]>([])
|
|
12
|
+
const [total, setTotal] = useState(0)
|
|
13
|
+
const [loading, setLoading] = useState(false)
|
|
14
|
+
|
|
15
|
+
const search = useCallback(async () => {
|
|
16
|
+
setLoading(true)
|
|
17
|
+
manager.context.setQuery(query)
|
|
18
|
+
manager.context.setPage(page)
|
|
19
|
+
manager.context.setPageSize(pageSize)
|
|
20
|
+
|
|
21
|
+
const res = await manager.search()
|
|
22
|
+
setResults(res.items)
|
|
23
|
+
setTotal(res.total)
|
|
24
|
+
setLoading(false)
|
|
25
|
+
}, [query, page, pageSize, manager])
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
query,
|
|
29
|
+
setQuery,
|
|
30
|
+
page,
|
|
31
|
+
setPage,
|
|
32
|
+
pageSize,
|
|
33
|
+
setPageSize,
|
|
34
|
+
results,
|
|
35
|
+
total,
|
|
36
|
+
loading,
|
|
37
|
+
search
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { graphql, buildSchema } from 'graphql'
|
|
2
|
+
import type { SearchManager } from '../core/SearchManager'
|
|
3
|
+
|
|
4
|
+
const schema = buildSchema(`
|
|
5
|
+
type Hit { id: ID, title: String, description: String, price: Float }
|
|
6
|
+
type SearchResult {
|
|
7
|
+
items: [Hit]
|
|
8
|
+
total: Int
|
|
9
|
+
page: Int
|
|
10
|
+
pageSize: Int
|
|
11
|
+
}
|
|
12
|
+
type Query {
|
|
13
|
+
search(term: String, page: Int, pageSize: Int, filters: String): SearchResult
|
|
14
|
+
}
|
|
15
|
+
`)
|
|
16
|
+
|
|
17
|
+
function parseFilters(filters?: string) {
|
|
18
|
+
if (!filters) return {}
|
|
19
|
+
const entries = String(filters).split(' AND ').map((s) => s.split(':'))
|
|
20
|
+
return Object.fromEntries(entries.map(([k, v]) => [k, v?.replace(/^"|"$/g, '')]))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function createSearchkitGraphQLHandler(manager: SearchManager) {
|
|
24
|
+
const root: any = {
|
|
25
|
+
search: async ({ term, page, pageSize, filters }: any) => {
|
|
26
|
+
manager.context.setQuery(term || '')
|
|
27
|
+
manager.context.setPage(page || 1)
|
|
28
|
+
manager.context.setPageSize(pageSize || manager.context.state.pageSize)
|
|
29
|
+
if (filters) {
|
|
30
|
+
manager.context.state.filters = parseFilters(filters) as any
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const res = await manager.search()
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
items: res.items,
|
|
37
|
+
total: res.total,
|
|
38
|
+
page: res.page,
|
|
39
|
+
pageSize: res.pageSize
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return async function handler(req: any, res: any) {
|
|
45
|
+
const { query, variables } = req.body || {}
|
|
46
|
+
const result = await graphql({ schema, source: query, rootValue: root, variableValues: variables })
|
|
47
|
+
res.json(result)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export default createSearchkitGraphQLHandler
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { SearchManager } from '../core/SearchManager'
|
|
2
|
+
import type { BuiltSearchQuery } from '../core/QueryBuilder'
|
|
3
|
+
|
|
4
|
+
type InstantSearchRequest = {
|
|
5
|
+
indexName: string
|
|
6
|
+
params: Record<string, any>
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
type InstantSearchResult = {
|
|
10
|
+
results: {
|
|
11
|
+
hits: any[]
|
|
12
|
+
nbHits: number
|
|
13
|
+
page: number
|
|
14
|
+
hitsPerPage: number
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function mapRequestToQuery(params: Record<string, any>, existing?: Partial<BuiltSearchQuery>) {
|
|
19
|
+
const q: Partial<BuiltSearchQuery> = existing ?? {}
|
|
20
|
+
|
|
21
|
+
if (typeof params.query === 'string') q.term = params.query
|
|
22
|
+
if (typeof params.page === 'number') q.page = params.page + 1 // instantsearch pages are 0-based
|
|
23
|
+
if (typeof params.hitsPerPage === 'number') q.pageSize = params.hitsPerPage
|
|
24
|
+
|
|
25
|
+
// Map simple filters from InstantSearch `facets` or `filters` param
|
|
26
|
+
if (params.filters && typeof params.filters === 'string') {
|
|
27
|
+
// basic parsing: "field:value AND other:value"
|
|
28
|
+
const entries = params.filters.split(' AND ').map((s: string) => s.split(':'))
|
|
29
|
+
q.filters = Object.fromEntries(entries.map(([k, v]) => [k, (v ?? '').replace(/^"|"$/g, '')]))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return q as BuiltSearchQuery
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function createSearchkitBridge(manager: SearchManager) {
|
|
36
|
+
return {
|
|
37
|
+
// InstantSearch client "search" method
|
|
38
|
+
async search(requests: InstantSearchRequest[]): Promise<InstantSearchResult[]> {
|
|
39
|
+
// Support single-index or multi-index by mapping each request to a search call
|
|
40
|
+
const results: InstantSearchResult[] = []
|
|
41
|
+
|
|
42
|
+
for (const req of requests) {
|
|
43
|
+
const params = req.params || {}
|
|
44
|
+
|
|
45
|
+
const built = mapRequestToQuery(params)
|
|
46
|
+
|
|
47
|
+
// apply to manager context
|
|
48
|
+
manager.context.setQuery(built.term || '')
|
|
49
|
+
manager.context.setPage(built.page || 1)
|
|
50
|
+
manager.context.setPageSize(built.pageSize || manager.context.state.pageSize)
|
|
51
|
+
if (built.filters) {
|
|
52
|
+
// clear and set simple filters
|
|
53
|
+
manager.context.state.filters = built.filters as any
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const res = await manager.search()
|
|
57
|
+
|
|
58
|
+
results.push({
|
|
59
|
+
results: {
|
|
60
|
+
hits: res.items,
|
|
61
|
+
nbHits: res.total,
|
|
62
|
+
page: (res.page || 1) - 1,
|
|
63
|
+
hitsPerPage: res.pageSize || manager.context.state.pageSize
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return results
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
// Minimal support for Searchkit/InstantSearch facet search
|
|
72
|
+
async searchForFacetValues(indexName: string, params: any) {
|
|
73
|
+
// Map to a query with term for facet matching
|
|
74
|
+
const built = mapRequestToQuery({ query: params.facetQuery })
|
|
75
|
+
|
|
76
|
+
manager.context.setQuery(built.term || '')
|
|
77
|
+
manager.context.setPage(1)
|
|
78
|
+
const res = await manager.search()
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
facetHits: (res.items || []).map((item: any) => ({ value: item[params.attribute], count: 0 })),
|
|
82
|
+
exhaustiveFacetsCount: true
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export default createSearchkitBridge
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { ref, computed } from 'vue'
|
|
2
|
+
import type { SearchManager } from '../core/SearchManager'
|
|
3
|
+
|
|
4
|
+
// Vue bridge: accept a SearchManager instance to stay adapter-agnostic.
|
|
5
|
+
export function useSearch(manager?: SearchManager) {
|
|
6
|
+
if (!manager) throw new Error('SearchManager instance is required for Vue bridge')
|
|
7
|
+
const mgr = manager as SearchManager
|
|
8
|
+
|
|
9
|
+
const query = ref(mgr.context.state.query)
|
|
10
|
+
const page = ref(mgr.context.state.page)
|
|
11
|
+
const pageSize = ref(mgr.context.state.pageSize)
|
|
12
|
+
|
|
13
|
+
const results = ref<any[]>([])
|
|
14
|
+
const total = ref(0)
|
|
15
|
+
const loading = ref(false)
|
|
16
|
+
|
|
17
|
+
async function run() {
|
|
18
|
+
loading.value = true
|
|
19
|
+
mgr.context.setQuery(query.value)
|
|
20
|
+
mgr.context.setPage(page.value)
|
|
21
|
+
mgr.context.setPageSize(pageSize.value)
|
|
22
|
+
|
|
23
|
+
const res = await mgr.search()
|
|
24
|
+
results.value = res.items
|
|
25
|
+
total.value = res.total
|
|
26
|
+
loading.value = false
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
query,
|
|
31
|
+
page,
|
|
32
|
+
pageSize,
|
|
33
|
+
results: computed(() => results.value),
|
|
34
|
+
total: computed(() => total.value),
|
|
35
|
+
loading: computed(() => loading.value),
|
|
36
|
+
search: run
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
import searchModule from './module'
|
|
7
|
+
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
9
|
+
|
|
10
|
+
async function run() {
|
|
11
|
+
const [,, command, arg] = process.argv
|
|
12
|
+
|
|
13
|
+
const defaultProvider = process.env.SEARCH_PROVIDER === 'meilisearch' ? 'meilisearch' : 'opensearch'
|
|
14
|
+
|
|
15
|
+
// Minimal CLI behaviour: prefer direct provider endpoints via env vars
|
|
16
|
+
const provider = defaultProvider
|
|
17
|
+
let search: any = null
|
|
18
|
+
// If a provider-specific client is available via env vars, use it; otherwise exit.
|
|
19
|
+
if (provider === 'meilisearch' && process.env.MEILI_HOST) {
|
|
20
|
+
search = { id: 'search:meilisearch', type: 'search' }
|
|
21
|
+
} else if (provider === 'opensearch' && process.env.OPENSEARCH_ENDPOINT) {
|
|
22
|
+
search = { id: 'search:opensearch', type: 'search' }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (!search) {
|
|
26
|
+
console.error('No search provider configured via environment')
|
|
27
|
+
process.exit(1)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (command === 'warmup') {
|
|
31
|
+
console.log('Warming up search provider...')
|
|
32
|
+
await search.search({ term: 'warmup' })
|
|
33
|
+
console.log('Warmup complete')
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (command === 'index') {
|
|
38
|
+
if (!arg) {
|
|
39
|
+
console.error('Missing file path: meeovi-search index <file.json>')
|
|
40
|
+
process.exit(1)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const filePath = path.resolve(process.cwd(), arg)
|
|
44
|
+
const data = JSON.parse(fs.readFileSync(filePath, 'utf8'))
|
|
45
|
+
|
|
46
|
+
console.log(`Indexing ${data.length} items...`)
|
|
47
|
+
|
|
48
|
+
if (search.id === 'search:opensearch') {
|
|
49
|
+
// TODO: implement bulk indexing
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (search.id === 'search:meilisearch') {
|
|
53
|
+
await fetch(`${process.env.MEILI_HOST}/indexes/${process.env.MEILI_INDEX}/documents`, {
|
|
54
|
+
method: 'POST',
|
|
55
|
+
headers: {
|
|
56
|
+
'Content-Type': 'application/json',
|
|
57
|
+
Authorization: `Bearer ${process.env.MEILI_KEY}`
|
|
58
|
+
},
|
|
59
|
+
body: JSON.stringify(data)
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
console.log('Indexing complete')
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
console.log(`Unknown command: ${command}`)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
run()
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface SearchModuleConfig {
|
|
2
|
+
defaultProvider: 'opensearch' | 'meilisearch'
|
|
3
|
+
providers: Record<string, unknown>
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function validateSearchConfig(config: SearchModuleConfig) {
|
|
7
|
+
if (!config.defaultProvider) {
|
|
8
|
+
throw new Error('[@mframework/search] Missing defaultProvider')
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (!config.providers[config.defaultProvider]) {
|
|
12
|
+
throw new Error(
|
|
13
|
+
`[@mframework/search] Provider "${config.defaultProvider}" not found in config.providers`
|
|
14
|
+
)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
|
|
2
|
+
export interface SearchConfig {
|
|
3
|
+
searchProvider: string
|
|
4
|
+
searchUrl?: string
|
|
5
|
+
apiKey?: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
let config: SearchConfig = {
|
|
9
|
+
searchProvider: 'searchkit',
|
|
10
|
+
searchUrl: '',
|
|
11
|
+
apiKey: ''
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function setSearchConfig(newConfig: Partial<SearchConfig>) {
|
|
15
|
+
config = { ...config, ...newConfig }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getSearchConfig(): SearchConfig {
|
|
19
|
+
return config
|
|
20
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export const Filters = {
|
|
2
|
+
term(field: string, value: string) {
|
|
3
|
+
return { type: 'term', field, value }
|
|
4
|
+
},
|
|
5
|
+
|
|
6
|
+
range(field: string, min?: number, max?: number) {
|
|
7
|
+
return { type: 'range', field, min, max }
|
|
8
|
+
},
|
|
9
|
+
|
|
10
|
+
boolean(field: string, value: boolean) {
|
|
11
|
+
return { type: 'boolean', field, value }
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export class SearchPipeline {
|
|
2
|
+
private before: ((q: any) => any)[] = []
|
|
3
|
+
private after: ((r: any) => any)[] = []
|
|
4
|
+
|
|
5
|
+
useBefore(fn: (query: any) => any) {
|
|
6
|
+
this.before.push(fn)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
useAfter(fn: (result: any) => any) {
|
|
10
|
+
this.after.push(fn)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
runBefore(query: any) {
|
|
14
|
+
return this.before.reduce((q, fn) => fn(q), query)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
runAfter(result: any) {
|
|
18
|
+
return this.after.reduce((r, fn) => fn(r), result)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { SearchContext } from './SearchContext'
|
|
2
|
+
|
|
3
|
+
export interface BuiltSearchQuery {
|
|
4
|
+
term: string
|
|
5
|
+
page: number
|
|
6
|
+
pageSize: number
|
|
7
|
+
sort?: string
|
|
8
|
+
filters: Record<string, any>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class QueryBuilder {
|
|
12
|
+
private context: SearchContext
|
|
13
|
+
|
|
14
|
+
constructor(context: SearchContext) {
|
|
15
|
+
this.context = context
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
build(): BuiltSearchQuery {
|
|
19
|
+
return {
|
|
20
|
+
term: this.context.state.query,
|
|
21
|
+
page: this.context.state.page,
|
|
22
|
+
pageSize: this.context.state.pageSize,
|
|
23
|
+
sort: this.context.state.sort,
|
|
24
|
+
filters: this.context.state.filters
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export interface SearchContextState {
|
|
2
|
+
query: string
|
|
3
|
+
page: number
|
|
4
|
+
pageSize: number
|
|
5
|
+
sort?: string
|
|
6
|
+
filters: Record<string, any>
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class SearchContext {
|
|
10
|
+
state: SearchContextState
|
|
11
|
+
|
|
12
|
+
constructor(initial?: Partial<SearchContextState>) {
|
|
13
|
+
this.state = {
|
|
14
|
+
query: '',
|
|
15
|
+
page: 1,
|
|
16
|
+
pageSize: 20,
|
|
17
|
+
filters: {},
|
|
18
|
+
...initial
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
setQuery(query: string) {
|
|
23
|
+
this.state.query = query
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
setPage(page: number) {
|
|
27
|
+
this.state.page = page
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
setPageSize(size: number) {
|
|
31
|
+
this.state.pageSize = size
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
setSort(sort: string | undefined) {
|
|
35
|
+
this.state.sort = sort
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
setFilter(key: string, value: any) {
|
|
39
|
+
this.state.filters[key] = value
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
removeFilter(key: string) {
|
|
43
|
+
delete this.state.filters[key]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
reset() {
|
|
47
|
+
this.state = {
|
|
48
|
+
query: '',
|
|
49
|
+
page: 1,
|
|
50
|
+
pageSize: 20,
|
|
51
|
+
filters: {}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { SearchContext, type SearchContextState } from './SearchContext'
|
|
2
|
+
import { QueryBuilder } from './QueryBuilder'
|
|
3
|
+
import { SearchPipeline } from './Pipeline'
|
|
4
|
+
|
|
5
|
+
export class SearchManager {
|
|
6
|
+
context: SearchContext
|
|
7
|
+
pipeline: SearchPipeline
|
|
8
|
+
adapter: any
|
|
9
|
+
|
|
10
|
+
constructor(adapter: any, initial?: Partial<SearchContextState>) {
|
|
11
|
+
this.context = new SearchContext(initial)
|
|
12
|
+
this.pipeline = new SearchPipeline()
|
|
13
|
+
this.adapter = adapter
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async search() {
|
|
17
|
+
const builder = new QueryBuilder(this.context)
|
|
18
|
+
let query = builder.build()
|
|
19
|
+
|
|
20
|
+
query = this.pipeline.runBefore(query)
|
|
21
|
+
|
|
22
|
+
const result = await this.adapter.search(query as any)
|
|
23
|
+
|
|
24
|
+
return this.pipeline.runAfter(result)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export * from './module'
|
|
2
|
+
export * from './adapter/types'
|
|
3
|
+
export * from './core/SearchManager'
|
|
4
|
+
export * from './core/SearchContext'
|
|
5
|
+
export * from './core/Filters'
|
|
6
|
+
export * from './core/Facets'
|
|
7
|
+
export * from './bridges/instantsearch'
|
|
8
|
+
export * from './bridges/vue'
|
|
9
|
+
export * from './bridges/react'
|
|
10
|
+
export * from './bridges/searchkit'
|
|
11
|
+
export * from './bridges/searchkit-server'
|
|
12
|
+
export * from './utils/health'
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { validateSearchConfig, type SearchModuleConfig } from './config/schema'
|
|
2
|
+
import { SearchManager } from './core/SearchManager'
|
|
3
|
+
|
|
4
|
+
// Minimal, runtime-agnostic module shim. The real runtime may provide
|
|
5
|
+
// adapters via ctx.getAdapter('search'), in which case we create a SearchManager.
|
|
6
|
+
export default {
|
|
7
|
+
id: 'search',
|
|
8
|
+
adapters: {},
|
|
9
|
+
|
|
10
|
+
async setup(ctx: any) {
|
|
11
|
+
const config: SearchModuleConfig | undefined = ctx?.config?.search
|
|
12
|
+
if (!config) return
|
|
13
|
+
|
|
14
|
+
// validate when possible
|
|
15
|
+
try {
|
|
16
|
+
validateSearchConfig(config)
|
|
17
|
+
} catch (e) {
|
|
18
|
+
// Ignore validation errors at runtime — config may be partial during startup.
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const adapter = ctx?.getAdapter ? ctx.getAdapter('search') : undefined
|
|
22
|
+
if (adapter) {
|
|
23
|
+
ctx.searchManager = new SearchManager(adapter)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Listen for app readiness or adapter registration when an event bus exists
|
|
27
|
+
const bus = ctx?.eventBus
|
|
28
|
+
if (bus && typeof bus.on === 'function') {
|
|
29
|
+
bus.on('app:ready', () => {
|
|
30
|
+
if (!ctx.searchManager) {
|
|
31
|
+
const runtimeAdapter = ctx.getAdapter('search')
|
|
32
|
+
if (runtimeAdapter) ctx.searchManager = new SearchManager(runtimeAdapter)
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
bus.on('adapter:registered', (payload: any) => {
|
|
37
|
+
try {
|
|
38
|
+
if (payload?.key === 'search' && !ctx.searchManager) {
|
|
39
|
+
const runtimeAdapter = ctx.getAdapter('search')
|
|
40
|
+
if (runtimeAdapter) ctx.searchManager = new SearchManager(runtimeAdapter)
|
|
41
|
+
}
|
|
42
|
+
} catch (e) {
|
|
43
|
+
/* noop */
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
declare module '#imports' {
|
|
2
|
+
export function useRuntimeConfig(): { public?: { searchUrl?: string } }
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
declare module '@searchkit/client' {
|
|
6
|
+
export class SearchkitClient {
|
|
7
|
+
constructor(config?: { url?: string; host?: string })
|
|
8
|
+
query(...args: unknown[]): unknown
|
|
9
|
+
fetch(...args: unknown[]): unknown
|
|
10
|
+
}
|
|
11
|
+
export type SearchkitClientConfig = { url?: string; host?: string }
|
|
12
|
+
}
|