@meeovi/layer-search 1.0.6 → 1.0.7
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 +39 -5
- package/app/composables/bridges/searchkit-server.ts +51 -0
- package/app/composables/bridges/searchkit.ts +88 -0
- package/app/composables/index.ts +4 -1
- package/app/composables/module.ts +41 -0
- package/app/composables/utils/health.ts +13 -0
- package/app/plugins/search.js +1 -1
- package/app/utils/search/client.ts +40 -0
- package/dist/app/composables/adapter/meilisearch.d.ts +8 -0
- package/dist/app/composables/adapter/meilisearch.js +36 -0
- package/dist/app/composables/adapter/mock.d.ts +3 -0
- package/dist/app/composables/adapter/mock.js +19 -0
- package/dist/app/composables/adapter/opensearch.d.ts +8 -0
- package/dist/app/composables/adapter/opensearch.js +46 -0
- package/dist/app/composables/adapter/types.d.ts +12 -0
- package/dist/app/composables/adapter/types.js +1 -0
- package/dist/app/composables/bridges/instantsearch.d.ts +4 -0
- package/dist/app/composables/bridges/instantsearch.js +17 -0
- package/dist/app/composables/bridges/react.d.ts +12 -0
- package/dist/app/composables/bridges/react.js +34 -0
- package/dist/app/composables/bridges/vue.d.ts +9 -0
- package/dist/app/composables/bridges/vue.js +31 -0
- package/dist/app/composables/cli.d.ts +2 -0
- package/dist/app/composables/cli.js +69 -0
- package/dist/app/composables/config/schema.d.ts +5 -0
- package/dist/app/composables/config/schema.js +8 -0
- package/dist/app/composables/config.d.ts +7 -0
- package/dist/app/composables/config.js +11 -0
- package/dist/app/composables/core/Facets.d.ts +17 -0
- package/dist/app/composables/core/Facets.js +8 -0
- package/dist/app/composables/core/Filters.d.ts +18 -0
- package/dist/app/composables/core/Filters.js +11 -0
- package/dist/app/composables/core/Normalizers.d.ts +0 -0
- package/dist/app/composables/core/Normalizers.js +1 -0
- package/dist/app/composables/core/Pipeline.d.ts +8 -0
- package/dist/app/composables/core/Pipeline.js +16 -0
- package/dist/app/composables/core/QueryBuilder.d.ts +13 -0
- package/dist/app/composables/core/QueryBuilder.js +15 -0
- package/dist/app/composables/core/SearchContext.d.ts +18 -0
- package/dist/app/composables/core/SearchContext.js +38 -0
- package/dist/app/composables/core/SearchManager.d.ts +10 -0
- package/dist/app/composables/core/SearchManager.js +20 -0
- package/dist/app/composables/events.d.ts +11 -0
- package/dist/app/composables/events.js +1 -0
- package/dist/app/composables/index.d.ts +9 -0
- package/dist/app/composables/index.js +9 -0
- package/dist/app/composables/module.d.ts +17 -0
- package/dist/app/composables/module.js +39 -0
- package/dist/app/composables/types/api/global-search.d.ts +8 -0
- package/dist/app/composables/types/api/global-search.js +1 -0
- package/dist/app/composables/utils/normalizers.d.ts +1 -0
- package/dist/app/composables/utils/normalizers.js +6 -0
- package/dist/app/utils/search/client.d.ts +3 -0
- package/dist/app/utils/search/client.js +31 -0
- package/dist/layers/search/app/composables/adapter/meilisearch.d.ts +8 -0
- package/dist/layers/search/app/composables/adapter/meilisearch.js +36 -0
- package/dist/layers/search/app/composables/adapter/mock.d.ts +3 -0
- package/dist/layers/search/app/composables/adapter/mock.js +19 -0
- package/dist/layers/search/app/composables/adapter/opensearch.d.ts +8 -0
- package/dist/layers/search/app/composables/adapter/opensearch.js +46 -0
- package/dist/layers/search/app/composables/adapter/types.d.ts +12 -0
- package/dist/layers/search/app/composables/adapter/types.js +1 -0
- package/dist/layers/search/app/composables/bridges/instantsearch.d.ts +4 -0
- package/dist/layers/search/app/composables/bridges/instantsearch.js +17 -0
- package/dist/layers/search/app/composables/bridges/react.d.ts +12 -0
- package/dist/layers/search/app/composables/bridges/react.js +34 -0
- package/dist/layers/search/app/composables/bridges/searchkit-server.d.ts +3 -0
- package/dist/layers/search/app/composables/bridges/searchkit-server.js +44 -0
- package/dist/layers/search/app/composables/bridges/searchkit.d.ts +21 -0
- package/dist/layers/search/app/composables/bridges/searchkit.js +60 -0
- package/dist/layers/search/app/composables/bridges/vue.d.ts +9 -0
- package/dist/layers/search/app/composables/bridges/vue.js +31 -0
- package/dist/layers/search/app/composables/cli.d.ts +2 -0
- package/dist/layers/search/app/composables/cli.js +69 -0
- package/dist/layers/search/app/composables/config/schema.d.ts +5 -0
- package/dist/layers/search/app/composables/config/schema.js +8 -0
- package/dist/layers/search/app/composables/config.d.ts +7 -0
- package/dist/layers/search/app/composables/config.js +11 -0
- package/dist/layers/search/app/composables/core/Facets.d.ts +17 -0
- package/dist/layers/search/app/composables/core/Facets.js +8 -0
- package/dist/layers/search/app/composables/core/Filters.d.ts +18 -0
- package/dist/layers/search/app/composables/core/Filters.js +11 -0
- package/dist/layers/search/app/composables/core/Normalizers.d.ts +0 -0
- package/dist/layers/search/app/composables/core/Normalizers.js +1 -0
- package/dist/layers/search/app/composables/core/Pipeline.d.ts +8 -0
- package/dist/layers/search/app/composables/core/Pipeline.js +16 -0
- package/dist/layers/search/app/composables/core/QueryBuilder.d.ts +13 -0
- package/dist/layers/search/app/composables/core/QueryBuilder.js +15 -0
- package/dist/layers/search/app/composables/core/SearchContext.d.ts +18 -0
- package/dist/layers/search/app/composables/core/SearchContext.js +38 -0
- package/dist/layers/search/app/composables/core/SearchManager.d.ts +10 -0
- package/dist/layers/search/app/composables/core/SearchManager.js +20 -0
- package/dist/layers/search/app/composables/events.d.ts +11 -0
- package/dist/layers/search/app/composables/events.js +1 -0
- package/dist/layers/search/app/composables/index.d.ts +12 -0
- package/dist/layers/search/app/composables/index.js +12 -0
- package/dist/layers/search/app/composables/module.d.ts +17 -0
- package/dist/layers/search/app/composables/module.js +73 -0
- package/dist/layers/search/app/composables/types/api/global-search.d.ts +8 -0
- package/dist/layers/search/app/composables/types/api/global-search.js +1 -0
- package/dist/layers/search/app/composables/utils/health.d.ts +11 -0
- package/dist/layers/search/app/composables/utils/health.js +11 -0
- package/dist/layers/search/app/composables/utils/normalizers.d.ts +1 -0
- package/dist/layers/search/app/composables/utils/normalizers.js +6 -0
- package/dist/layers/search/app/utils/search/client.d.ts +3 -0
- package/dist/layers/search/app/utils/search/client.js +31 -0
- package/dist/layers/search/nuxt.config.d.ts +2 -0
- package/dist/layers/search/nuxt.config.js +7 -0
- package/dist/layers/search/test/runtime-adapter.spec.d.ts +1 -0
- package/dist/layers/search/test/runtime-adapter.spec.js +49 -0
- package/dist/nuxt.config.d.ts +2 -0
- package/dist/nuxt.config.js +7 -0
- package/dist/packages/core/src/adapters/auth.d.ts +9 -0
- package/dist/packages/core/src/adapters/auth.js +1 -0
- package/dist/packages/core/src/adapters/cart.d.ts +22 -0
- package/dist/packages/core/src/adapters/cart.js +1 -0
- package/dist/packages/core/src/adapters/catalog.d.ts +17 -0
- package/dist/packages/core/src/adapters/catalog.js +1 -0
- package/dist/packages/core/src/adapters/common.d.ts +9 -0
- package/dist/packages/core/src/adapters/common.js +1 -0
- package/dist/packages/core/src/adapters/lists.d.ts +17 -0
- package/dist/packages/core/src/adapters/lists.js +1 -0
- package/dist/packages/core/src/adapters/search.d.ts +21 -0
- package/dist/packages/core/src/adapters/search.js +1 -0
- package/dist/packages/core/src/plugins/defineAdapter.d.ts +2 -0
- package/dist/packages/core/src/plugins/defineAdapter.js +3 -0
- package/dist/packages/core/src/plugins/defineModule.d.ts +2 -0
- package/dist/packages/core/src/plugins/defineModule.js +3 -0
- package/dist/packages/core/src/plugins/registry.d.ts +14 -0
- package/dist/packages/core/src/plugins/registry.js +46 -0
- package/dist/packages/core/src/runtime/app.d.ts +2 -0
- package/dist/packages/core/src/runtime/app.js +20 -0
- package/dist/packages/core/src/runtime/context.d.ts +9 -0
- package/dist/packages/core/src/runtime/context.js +11 -0
- package/dist/packages/core/src/runtime/hooks.d.ts +5 -0
- package/dist/packages/core/src/runtime/hooks.js +18 -0
- package/dist/packages/core/src/runtime/lifecycle.d.ts +4 -0
- package/dist/packages/core/src/runtime/lifecycle.js +5 -0
- package/dist/packages/core/src/types/adapters.d.ts +14 -0
- package/dist/packages/core/src/types/adapters.js +1 -0
- package/dist/packages/core/src/types/app.d.ts +13 -0
- package/dist/packages/core/src/types/app.js +1 -0
- package/dist/packages/core/src/types/config.d.ts +8 -0
- package/dist/packages/core/src/types/config.js +1 -0
- package/dist/packages/core/src/types/events.d.ts +20 -0
- package/dist/packages/core/src/types/events.js +22 -0
- package/dist/packages/core/src/types/module.d.ts +15 -0
- package/dist/packages/core/src/types/module.js +1 -0
- package/package.json +9 -4
- package/test/runtime-adapter.spec.ts +61 -0
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<!-- packages/search/README.md -->
|
|
2
2
|
# @meeovi/search
|
|
3
3
|
|
|
4
|
-
A modular, provider-agnostic search
|
|
4
|
+
A modular, provider-agnostic search layer for the Alternate Framework. It provides a unified, typed search API with pluggable adapters (Meilisearch, OpenSearch, mock adapters), lightweight bridges for UI integrations (InstantSearch / Searchkit), a small CLI for indexing/warmup, and event hooks.
|
|
5
5
|
|
|
6
6
|
## Features
|
|
7
7
|
|
|
@@ -51,9 +51,12 @@ const app = createAlternateApp({
|
|
|
51
51
|
|
|
52
52
|
await app.start()
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
54
|
+
// After app startup you can access the registered `search` adapter via the runtime
|
|
55
|
+
const searchAdapter = app.context.getAdapter('search')
|
|
56
|
+
if (searchAdapter) {
|
|
57
|
+
const results = await searchAdapter.search({ term: 'shoes', page: 1, pageSize: 10 })
|
|
58
|
+
console.log(results.items)
|
|
59
|
+
}
|
|
57
60
|
```
|
|
58
61
|
|
|
59
62
|
## Adapters
|
|
@@ -92,7 +95,7 @@ const mock = createMockSearchAdapter([
|
|
|
92
95
|
{ id: '2', title: 'Blue Shirt' }
|
|
93
96
|
])
|
|
94
97
|
|
|
95
|
-
const results = await mock.search({ term: 'red' })
|
|
98
|
+
const results = await mock.search({ term: 'red', page: 1, pageSize: 10 })
|
|
96
99
|
```
|
|
97
100
|
|
|
98
101
|
## Configuration
|
|
@@ -115,6 +118,37 @@ Example `search` config in your app:
|
|
|
115
118
|
|
|
116
119
|
The module validates that `defaultProvider` and the referenced provider configuration exist, and that required fields for each adapter are present.
|
|
117
120
|
|
|
121
|
+
## UI Integrations (InstantSearch / Searchkit)
|
|
122
|
+
|
|
123
|
+
This layer includes bridges that let UI code use Algolia InstantSearch or Searchkit clients without coupling the UI to a particular backend provider. Use `createInstantSearchBridge` / `createSearchkitBridge` on the client, and `createSearchkitGraphQLHandler` on the server when exposing a GraphQL endpoint for Searchkit-server.
|
|
124
|
+
|
|
125
|
+
Example (client):
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
import { createInstantSearchBridge } from '@meeovi/search'
|
|
129
|
+
// `manager` is the `SearchManager` instance available on the app context
|
|
130
|
+
const bridge = createInstantSearchBridge(manager)
|
|
131
|
+
|
|
132
|
+
const instantsearchClient = {
|
|
133
|
+
search(requests) {
|
|
134
|
+
return bridge.searchFunction({ state: requests[0].params, setResults: () => {} })
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Example (server - Express):
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
import express from 'express'
|
|
143
|
+
import { createSearchkitGraphQLHandler } from '@meeovi/search'
|
|
144
|
+
|
|
145
|
+
const app = express()
|
|
146
|
+
app.use(express.json())
|
|
147
|
+
app.post('/graphql', createSearchkitGraphQLHandler(manager))
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
These bridges map InstantSearch/Searchkit request shapes into the layer's `SearchManager` and underlying adapters so UI code doesn't need to change when you swap search providers.
|
|
151
|
+
|
|
118
152
|
## Events
|
|
119
153
|
|
|
120
154
|
This module emits bus events that you can listen to:
|
|
@@ -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
|
package/app/composables/index.ts
CHANGED
|
@@ -6,4 +6,7 @@ export * from './core/Filters'
|
|
|
6
6
|
export * from './core/Facets'
|
|
7
7
|
export * from './bridges/instantsearch'
|
|
8
8
|
export * from './bridges/vue'
|
|
9
|
-
export * from './bridges/react'
|
|
9
|
+
export * from './bridges/react'
|
|
10
|
+
export * from './bridges/searchkit'
|
|
11
|
+
export * from './bridges/searchkit-server'
|
|
12
|
+
export * from './utils/health'
|
|
@@ -65,8 +65,49 @@ export default defineAlternateModule({
|
|
|
65
65
|
ctx.searchManager = new SearchManager<MeeoviSearchItem>(adapter)
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
// Ensure search manager exists now if adapter already registered
|
|
69
|
+
if (!ctx.searchManager && ctx.getAdapter('search')) {
|
|
70
|
+
const runtimeAdapter = ctx.getAdapter('search') as
|
|
71
|
+
| SearchAdapter<MeeoviSearchItem>
|
|
72
|
+
| undefined
|
|
73
|
+
if (runtimeAdapter) {
|
|
74
|
+
ctx.searchManager = new SearchManager<MeeoviSearchItem>(runtimeAdapter)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Listen for app readiness and for runtime adapter registrations so
|
|
79
|
+
// new adapters (registered by other modules) will auto-create the
|
|
80
|
+
// search manager when they become available.
|
|
68
81
|
bus.on('app:ready', () => {
|
|
82
|
+
if (!ctx.searchManager) {
|
|
83
|
+
const runtimeAdapter = ctx.getAdapter('search') as
|
|
84
|
+
| SearchAdapter<MeeoviSearchItem>
|
|
85
|
+
| undefined
|
|
86
|
+
if (runtimeAdapter) {
|
|
87
|
+
ctx.searchManager = new SearchManager<MeeoviSearchItem>(runtimeAdapter)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
69
90
|
console.info('[@meeovi/search] Search module initialized')
|
|
70
91
|
})
|
|
92
|
+
|
|
93
|
+
// Optional runtime event: if other modules emit `adapter:registered`
|
|
94
|
+
// with a `{ key }` payload, respond and initialize when the `search`
|
|
95
|
+
// adapter becomes available.
|
|
96
|
+
// This is defensive — the core registry does not emit this by default,
|
|
97
|
+
// but some runtimes may choose to.
|
|
98
|
+
bus.on('adapter:registered' as any, (payload: any) => {
|
|
99
|
+
try {
|
|
100
|
+
if (payload?.key === 'search' && !ctx.searchManager) {
|
|
101
|
+
const runtimeAdapter = ctx.getAdapter('search') as
|
|
102
|
+
| SearchAdapter<MeeoviSearchItem>
|
|
103
|
+
| undefined
|
|
104
|
+
if (runtimeAdapter) {
|
|
105
|
+
ctx.searchManager = new SearchManager<MeeoviSearchItem>(runtimeAdapter)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
} catch (e) {
|
|
109
|
+
/* noop */
|
|
110
|
+
}
|
|
111
|
+
})
|
|
71
112
|
}
|
|
72
113
|
})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { SearchAdapter } from '@meeovi/core'
|
|
2
|
+
|
|
3
|
+
export async function checkAdapterHealth(adapter: SearchAdapter<any>) {
|
|
4
|
+
try {
|
|
5
|
+
// perform a minimal search to verify connectivity
|
|
6
|
+
const res = await adapter.search({ term: '', page: 1, pageSize: 1, filters: {} } as any)
|
|
7
|
+
return { ok: true, total: res?.total ?? null }
|
|
8
|
+
} catch (e: any) {
|
|
9
|
+
return { ok: false, error: e?.message || String(e) }
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default checkAdapterHealth
|
package/app/plugins/search.js
CHANGED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Lightweight helper to obtain a search client and index name for InstantSearch
|
|
2
|
+
// This is intentionally minimal — it throws when no configuration is present
|
|
3
|
+
// so the caller can fall back gracefully.
|
|
4
|
+
import type { SearchClient } from 'instantsearch.js'
|
|
5
|
+
|
|
6
|
+
export function getIndexName(): string {
|
|
7
|
+
return (
|
|
8
|
+
process.env.NUXT_PUBLIC_SEARCH_INDEX ||
|
|
9
|
+
process.env.SEARCH_INDEX ||
|
|
10
|
+
'default'
|
|
11
|
+
)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getSearchClient(): SearchClient {
|
|
15
|
+
const host = process.env.NUXT_PUBLIC_SEARCHKIT_HOST || process.env.SEARCHKIT_HOST
|
|
16
|
+
if (!host) {
|
|
17
|
+
throw new Error('Searchkit host not configured via SEARCHKIT_HOST or NUXT_PUBLIC_SEARCHKIT_HOST')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Defer importing heavy searchkit/instantsearch client until runtime.
|
|
21
|
+
// Consumers can replace this implementation with a provider-specific client.
|
|
22
|
+
// Here we attempt to use @searchkit/instantsearch-client if available.
|
|
23
|
+
// If not present, let the import fail so the plugin can fallback.
|
|
24
|
+
// The actual creation API differs between versions; adjust as needed.
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
26
|
+
const createClient = require('@searchkit/instantsearch-client')
|
|
27
|
+
if (!createClient) {
|
|
28
|
+
throw new Error('@searchkit/instantsearch-client is not installed')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Create a minimal client. The factory API may vary; this is a best-effort
|
|
32
|
+
// placeholder that should be adapted to your Searchkit configuration.
|
|
33
|
+
// If your Searchkit installation exposes a helper, prefer using that.
|
|
34
|
+
try {
|
|
35
|
+
return createClient({ host })
|
|
36
|
+
} catch (e: any) {
|
|
37
|
+
// rethrow with context
|
|
38
|
+
throw new Error('Failed to create Searchkit client: ' + (e?.message || e))
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type SearchAdapterConfig } from '@meeovi/core';
|
|
2
|
+
interface MeiliConfig extends SearchAdapterConfig {
|
|
3
|
+
host: string;
|
|
4
|
+
apiKey?: string;
|
|
5
|
+
index: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function createMeilisearchAdapter(config: MeiliConfig): import("@meeovi/core").AlternateAdapter<SearchAdapterConfig>;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { defineAlternateAdapter } from '@meeovi/core';
|
|
2
|
+
export function createMeilisearchAdapter(config) {
|
|
3
|
+
const headers = {
|
|
4
|
+
'Content-Type': 'application/json'
|
|
5
|
+
};
|
|
6
|
+
if (config.apiKey) {
|
|
7
|
+
headers['Authorization'] = `Bearer ${config.apiKey}`;
|
|
8
|
+
}
|
|
9
|
+
const adapter = {
|
|
10
|
+
id: 'search:meilisearch',
|
|
11
|
+
type: 'search',
|
|
12
|
+
config,
|
|
13
|
+
async search(query) {
|
|
14
|
+
const params = new URLSearchParams({
|
|
15
|
+
q: query.term,
|
|
16
|
+
offset: String((query.page - 1) * query.pageSize),
|
|
17
|
+
limit: String(query.pageSize)
|
|
18
|
+
});
|
|
19
|
+
const res = await fetch(`${config.host}/indexes/${config.index}/search?${params}`, {
|
|
20
|
+
method: 'POST',
|
|
21
|
+
headers,
|
|
22
|
+
body: JSON.stringify({
|
|
23
|
+
filter: Object.entries(query.filters).map(([field, value]) => `${field} = ${value}`)
|
|
24
|
+
})
|
|
25
|
+
});
|
|
26
|
+
const json = await res.json();
|
|
27
|
+
return {
|
|
28
|
+
items: json.hits,
|
|
29
|
+
total: json.estimatedTotalHits ?? json.hits.length,
|
|
30
|
+
page: query.page,
|
|
31
|
+
pageSize: query.pageSize
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
return defineAlternateAdapter(adapter);
|
|
36
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { defineAlternateAdapter } from '@meeovi/core';
|
|
2
|
+
export function createMockSearchAdapter(items = []) {
|
|
3
|
+
const cfg = { provider: 'mock' };
|
|
4
|
+
const adapter = {
|
|
5
|
+
id: 'search:mock',
|
|
6
|
+
type: 'search',
|
|
7
|
+
config: cfg,
|
|
8
|
+
async search(query) {
|
|
9
|
+
const filtered = items.filter((item) => item.title.toLowerCase().includes(query.term.toLowerCase()));
|
|
10
|
+
return {
|
|
11
|
+
items: filtered,
|
|
12
|
+
total: filtered.length,
|
|
13
|
+
page: 1,
|
|
14
|
+
pageSize: filtered.length
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
return defineAlternateAdapter(adapter);
|
|
19
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type SearchAdapterConfig } from '@meeovi/core';
|
|
2
|
+
interface OpenSearchConfig extends SearchAdapterConfig {
|
|
3
|
+
endpoint: string;
|
|
4
|
+
index: string;
|
|
5
|
+
apiKey?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function createOpenSearchAdapter(config: OpenSearchConfig): import("@meeovi/core").AlternateAdapter<SearchAdapterConfig>;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { defineAlternateAdapter } from '@meeovi/core';
|
|
2
|
+
import { normalizeOpenSearchHit } from '../utils/normalizers';
|
|
3
|
+
export function createOpenSearchAdapter(config) {
|
|
4
|
+
const adapter = {
|
|
5
|
+
id: 'search:opensearch',
|
|
6
|
+
type: 'search',
|
|
7
|
+
config,
|
|
8
|
+
async search(query) {
|
|
9
|
+
const body = {
|
|
10
|
+
query: {
|
|
11
|
+
bool: {
|
|
12
|
+
must: [
|
|
13
|
+
{
|
|
14
|
+
multi_match: {
|
|
15
|
+
query: query.term,
|
|
16
|
+
fields: ['title^3', 'description', 'tags']
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
filter: Object.entries(query.filters).map(([field, value]) => ({
|
|
21
|
+
term: { [field]: value }
|
|
22
|
+
}))
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
from: (query.page - 1) * query.pageSize,
|
|
26
|
+
size: query.pageSize
|
|
27
|
+
};
|
|
28
|
+
const res = await fetch(`${config.endpoint}/${config.index}/_search`, {
|
|
29
|
+
method: 'POST',
|
|
30
|
+
headers: {
|
|
31
|
+
'Content-Type': 'application/json',
|
|
32
|
+
...(config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : {})
|
|
33
|
+
},
|
|
34
|
+
body: JSON.stringify(body)
|
|
35
|
+
});
|
|
36
|
+
const json = await res.json();
|
|
37
|
+
return {
|
|
38
|
+
items: json.hits.hits.map(normalizeOpenSearchHit),
|
|
39
|
+
total: json.hits.total.value,
|
|
40
|
+
page: query.page,
|
|
41
|
+
pageSize: query.pageSize
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
return defineAlternateAdapter(adapter);
|
|
46
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { SearchAdapter, SearchResult } from '@meeovi/core';
|
|
2
|
+
import type { BuiltSearchQuery } from '../core/QueryBuilder';
|
|
3
|
+
export interface MeeoviSearchItem {
|
|
4
|
+
id: string;
|
|
5
|
+
title: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
price?: number;
|
|
8
|
+
[key: string]: unknown;
|
|
9
|
+
}
|
|
10
|
+
export type MeeoviSearchAdapter = SearchAdapter<MeeoviSearchItem> & {
|
|
11
|
+
search(query: BuiltSearchQuery): Promise<SearchResult<MeeoviSearchItem>>;
|
|
12
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function createInstantSearchBridge(manager) {
|
|
2
|
+
return {
|
|
3
|
+
searchFunction(helper) {
|
|
4
|
+
manager.context.setQuery(helper.state.query || '');
|
|
5
|
+
manager.context.setPage(helper.state.page || 1);
|
|
6
|
+
// map filters if needed from helper.state
|
|
7
|
+
return manager.search().then((result) => {
|
|
8
|
+
helper.setResults({
|
|
9
|
+
hits: result.items,
|
|
10
|
+
nbHits: result.total,
|
|
11
|
+
page: result.page - 1,
|
|
12
|
+
hitsPerPage: result.pageSize
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare function useReactSearch(): {
|
|
2
|
+
query: string;
|
|
3
|
+
setQuery: import("react").Dispatch<import("react").SetStateAction<string>>;
|
|
4
|
+
page: number;
|
|
5
|
+
setPage: import("react").Dispatch<import("react").SetStateAction<number>>;
|
|
6
|
+
pageSize: number;
|
|
7
|
+
setPageSize: import("react").Dispatch<import("react").SetStateAction<number>>;
|
|
8
|
+
results: any[];
|
|
9
|
+
total: number;
|
|
10
|
+
loading: boolean;
|
|
11
|
+
search: () => Promise<void>;
|
|
12
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { useState, useCallback } from 'react';
|
|
2
|
+
import { useAlternateContext } from '@meeovi/core';
|
|
3
|
+
export function useReactSearch() {
|
|
4
|
+
const ctx = useAlternateContext();
|
|
5
|
+
const manager = ctx.searchManager;
|
|
6
|
+
const [query, setQuery] = useState(manager.context.state.query);
|
|
7
|
+
const [page, setPage] = useState(manager.context.state.page);
|
|
8
|
+
const [pageSize, setPageSize] = useState(manager.context.state.pageSize);
|
|
9
|
+
const [results, setResults] = useState([]);
|
|
10
|
+
const [total, setTotal] = useState(0);
|
|
11
|
+
const [loading, setLoading] = useState(false);
|
|
12
|
+
const search = useCallback(async () => {
|
|
13
|
+
setLoading(true);
|
|
14
|
+
manager.context.setQuery(query);
|
|
15
|
+
manager.context.setPage(page);
|
|
16
|
+
manager.context.setPageSize(pageSize);
|
|
17
|
+
const res = await manager.search();
|
|
18
|
+
setResults(res.items);
|
|
19
|
+
setTotal(res.total);
|
|
20
|
+
setLoading(false);
|
|
21
|
+
}, [query, page, pageSize, manager]);
|
|
22
|
+
return {
|
|
23
|
+
query,
|
|
24
|
+
setQuery,
|
|
25
|
+
page,
|
|
26
|
+
setPage,
|
|
27
|
+
pageSize,
|
|
28
|
+
setPageSize,
|
|
29
|
+
results,
|
|
30
|
+
total,
|
|
31
|
+
loading,
|
|
32
|
+
search
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare function useSearch(): {
|
|
2
|
+
query: import("vue").Ref<string, string>;
|
|
3
|
+
page: import("vue").Ref<number, number>;
|
|
4
|
+
pageSize: import("vue").Ref<number, number>;
|
|
5
|
+
results: import("vue").ComputedRef<any[]>;
|
|
6
|
+
total: import("vue").ComputedRef<number>;
|
|
7
|
+
loading: import("vue").ComputedRef<boolean>;
|
|
8
|
+
search: () => Promise<void>;
|
|
9
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { ref, computed } from 'vue';
|
|
2
|
+
import { useAlternateContext } from '@meeovi/core';
|
|
3
|
+
export function useSearch() {
|
|
4
|
+
const ctx = useAlternateContext();
|
|
5
|
+
const manager = ctx.searchManager;
|
|
6
|
+
const query = ref(manager.context.state.query);
|
|
7
|
+
const page = ref(manager.context.state.page);
|
|
8
|
+
const pageSize = ref(manager.context.state.pageSize);
|
|
9
|
+
const results = ref([]);
|
|
10
|
+
const total = ref(0);
|
|
11
|
+
const loading = ref(false);
|
|
12
|
+
async function run() {
|
|
13
|
+
loading.value = true;
|
|
14
|
+
manager.context.setQuery(query.value);
|
|
15
|
+
manager.context.setPage(page.value);
|
|
16
|
+
manager.context.setPageSize(pageSize.value);
|
|
17
|
+
const res = await manager.search();
|
|
18
|
+
results.value = res.items;
|
|
19
|
+
total.value = res.total;
|
|
20
|
+
loading.value = false;
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
query,
|
|
24
|
+
page,
|
|
25
|
+
pageSize,
|
|
26
|
+
results: computed(() => results.value),
|
|
27
|
+
total: computed(() => total.value),
|
|
28
|
+
loading: computed(() => loading.value),
|
|
29
|
+
search: run
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { createAlternateApp } from '@meeovi/core';
|
|
6
|
+
import searchModule from './module';
|
|
7
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
async function run() {
|
|
9
|
+
const [, , command, arg] = process.argv;
|
|
10
|
+
const defaultProvider = process.env.SEARCH_PROVIDER === 'meilisearch' ? 'meilisearch' : 'opensearch';
|
|
11
|
+
const app = createAlternateApp({
|
|
12
|
+
config: {
|
|
13
|
+
env: 'production',
|
|
14
|
+
search: {
|
|
15
|
+
defaultProvider,
|
|
16
|
+
providers: {
|
|
17
|
+
opensearch: {
|
|
18
|
+
endpoint: process.env.OPENSEARCH_ENDPOINT,
|
|
19
|
+
index: process.env.OPENSEARCH_INDEX
|
|
20
|
+
},
|
|
21
|
+
meilisearch: {
|
|
22
|
+
host: process.env.MEILI_HOST,
|
|
23
|
+
index: process.env.MEILI_INDEX,
|
|
24
|
+
apiKey: process.env.MEILI_KEY
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
modules: [searchModule]
|
|
30
|
+
});
|
|
31
|
+
const ctx = await app.start();
|
|
32
|
+
const search = ctx.getAdapter('search');
|
|
33
|
+
if (!search) {
|
|
34
|
+
console.error('No search adapter registered');
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
if (command === 'warmup') {
|
|
38
|
+
console.log('Warming up search provider...');
|
|
39
|
+
await search.search({ term: 'warmup' });
|
|
40
|
+
console.log('Warmup complete');
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (command === 'index') {
|
|
44
|
+
if (!arg) {
|
|
45
|
+
console.error('Missing file path: meeovi-search index <file.json>');
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
const filePath = path.resolve(process.cwd(), arg);
|
|
49
|
+
const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
50
|
+
console.log(`Indexing ${data.length} items...`);
|
|
51
|
+
if (search.id === 'search:opensearch') {
|
|
52
|
+
// TODO: implement bulk indexing
|
|
53
|
+
}
|
|
54
|
+
if (search.id === 'search:meilisearch') {
|
|
55
|
+
await fetch(`${process.env.MEILI_HOST}/indexes/${process.env.MEILI_INDEX}/documents`, {
|
|
56
|
+
method: 'POST',
|
|
57
|
+
headers: {
|
|
58
|
+
'Content-Type': 'application/json',
|
|
59
|
+
Authorization: `Bearer ${process.env.MEILI_KEY}`
|
|
60
|
+
},
|
|
61
|
+
body: JSON.stringify(data)
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
console.log('Indexing complete');
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
console.log(`Unknown command: ${command}`);
|
|
68
|
+
}
|
|
69
|
+
run();
|