@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.
Files changed (150) hide show
  1. package/README.md +39 -5
  2. package/app/composables/bridges/searchkit-server.ts +51 -0
  3. package/app/composables/bridges/searchkit.ts +88 -0
  4. package/app/composables/index.ts +4 -1
  5. package/app/composables/module.ts +41 -0
  6. package/app/composables/utils/health.ts +13 -0
  7. package/app/plugins/search.js +1 -1
  8. package/app/utils/search/client.ts +40 -0
  9. package/dist/app/composables/adapter/meilisearch.d.ts +8 -0
  10. package/dist/app/composables/adapter/meilisearch.js +36 -0
  11. package/dist/app/composables/adapter/mock.d.ts +3 -0
  12. package/dist/app/composables/adapter/mock.js +19 -0
  13. package/dist/app/composables/adapter/opensearch.d.ts +8 -0
  14. package/dist/app/composables/adapter/opensearch.js +46 -0
  15. package/dist/app/composables/adapter/types.d.ts +12 -0
  16. package/dist/app/composables/adapter/types.js +1 -0
  17. package/dist/app/composables/bridges/instantsearch.d.ts +4 -0
  18. package/dist/app/composables/bridges/instantsearch.js +17 -0
  19. package/dist/app/composables/bridges/react.d.ts +12 -0
  20. package/dist/app/composables/bridges/react.js +34 -0
  21. package/dist/app/composables/bridges/vue.d.ts +9 -0
  22. package/dist/app/composables/bridges/vue.js +31 -0
  23. package/dist/app/composables/cli.d.ts +2 -0
  24. package/dist/app/composables/cli.js +69 -0
  25. package/dist/app/composables/config/schema.d.ts +5 -0
  26. package/dist/app/composables/config/schema.js +8 -0
  27. package/dist/app/composables/config.d.ts +7 -0
  28. package/dist/app/composables/config.js +11 -0
  29. package/dist/app/composables/core/Facets.d.ts +17 -0
  30. package/dist/app/composables/core/Facets.js +8 -0
  31. package/dist/app/composables/core/Filters.d.ts +18 -0
  32. package/dist/app/composables/core/Filters.js +11 -0
  33. package/dist/app/composables/core/Normalizers.d.ts +0 -0
  34. package/dist/app/composables/core/Normalizers.js +1 -0
  35. package/dist/app/composables/core/Pipeline.d.ts +8 -0
  36. package/dist/app/composables/core/Pipeline.js +16 -0
  37. package/dist/app/composables/core/QueryBuilder.d.ts +13 -0
  38. package/dist/app/composables/core/QueryBuilder.js +15 -0
  39. package/dist/app/composables/core/SearchContext.d.ts +18 -0
  40. package/dist/app/composables/core/SearchContext.js +38 -0
  41. package/dist/app/composables/core/SearchManager.d.ts +10 -0
  42. package/dist/app/composables/core/SearchManager.js +20 -0
  43. package/dist/app/composables/events.d.ts +11 -0
  44. package/dist/app/composables/events.js +1 -0
  45. package/dist/app/composables/index.d.ts +9 -0
  46. package/dist/app/composables/index.js +9 -0
  47. package/dist/app/composables/module.d.ts +17 -0
  48. package/dist/app/composables/module.js +39 -0
  49. package/dist/app/composables/types/api/global-search.d.ts +8 -0
  50. package/dist/app/composables/types/api/global-search.js +1 -0
  51. package/dist/app/composables/utils/normalizers.d.ts +1 -0
  52. package/dist/app/composables/utils/normalizers.js +6 -0
  53. package/dist/app/utils/search/client.d.ts +3 -0
  54. package/dist/app/utils/search/client.js +31 -0
  55. package/dist/layers/search/app/composables/adapter/meilisearch.d.ts +8 -0
  56. package/dist/layers/search/app/composables/adapter/meilisearch.js +36 -0
  57. package/dist/layers/search/app/composables/adapter/mock.d.ts +3 -0
  58. package/dist/layers/search/app/composables/adapter/mock.js +19 -0
  59. package/dist/layers/search/app/composables/adapter/opensearch.d.ts +8 -0
  60. package/dist/layers/search/app/composables/adapter/opensearch.js +46 -0
  61. package/dist/layers/search/app/composables/adapter/types.d.ts +12 -0
  62. package/dist/layers/search/app/composables/adapter/types.js +1 -0
  63. package/dist/layers/search/app/composables/bridges/instantsearch.d.ts +4 -0
  64. package/dist/layers/search/app/composables/bridges/instantsearch.js +17 -0
  65. package/dist/layers/search/app/composables/bridges/react.d.ts +12 -0
  66. package/dist/layers/search/app/composables/bridges/react.js +34 -0
  67. package/dist/layers/search/app/composables/bridges/searchkit-server.d.ts +3 -0
  68. package/dist/layers/search/app/composables/bridges/searchkit-server.js +44 -0
  69. package/dist/layers/search/app/composables/bridges/searchkit.d.ts +21 -0
  70. package/dist/layers/search/app/composables/bridges/searchkit.js +60 -0
  71. package/dist/layers/search/app/composables/bridges/vue.d.ts +9 -0
  72. package/dist/layers/search/app/composables/bridges/vue.js +31 -0
  73. package/dist/layers/search/app/composables/cli.d.ts +2 -0
  74. package/dist/layers/search/app/composables/cli.js +69 -0
  75. package/dist/layers/search/app/composables/config/schema.d.ts +5 -0
  76. package/dist/layers/search/app/composables/config/schema.js +8 -0
  77. package/dist/layers/search/app/composables/config.d.ts +7 -0
  78. package/dist/layers/search/app/composables/config.js +11 -0
  79. package/dist/layers/search/app/composables/core/Facets.d.ts +17 -0
  80. package/dist/layers/search/app/composables/core/Facets.js +8 -0
  81. package/dist/layers/search/app/composables/core/Filters.d.ts +18 -0
  82. package/dist/layers/search/app/composables/core/Filters.js +11 -0
  83. package/dist/layers/search/app/composables/core/Normalizers.d.ts +0 -0
  84. package/dist/layers/search/app/composables/core/Normalizers.js +1 -0
  85. package/dist/layers/search/app/composables/core/Pipeline.d.ts +8 -0
  86. package/dist/layers/search/app/composables/core/Pipeline.js +16 -0
  87. package/dist/layers/search/app/composables/core/QueryBuilder.d.ts +13 -0
  88. package/dist/layers/search/app/composables/core/QueryBuilder.js +15 -0
  89. package/dist/layers/search/app/composables/core/SearchContext.d.ts +18 -0
  90. package/dist/layers/search/app/composables/core/SearchContext.js +38 -0
  91. package/dist/layers/search/app/composables/core/SearchManager.d.ts +10 -0
  92. package/dist/layers/search/app/composables/core/SearchManager.js +20 -0
  93. package/dist/layers/search/app/composables/events.d.ts +11 -0
  94. package/dist/layers/search/app/composables/events.js +1 -0
  95. package/dist/layers/search/app/composables/index.d.ts +12 -0
  96. package/dist/layers/search/app/composables/index.js +12 -0
  97. package/dist/layers/search/app/composables/module.d.ts +17 -0
  98. package/dist/layers/search/app/composables/module.js +73 -0
  99. package/dist/layers/search/app/composables/types/api/global-search.d.ts +8 -0
  100. package/dist/layers/search/app/composables/types/api/global-search.js +1 -0
  101. package/dist/layers/search/app/composables/utils/health.d.ts +11 -0
  102. package/dist/layers/search/app/composables/utils/health.js +11 -0
  103. package/dist/layers/search/app/composables/utils/normalizers.d.ts +1 -0
  104. package/dist/layers/search/app/composables/utils/normalizers.js +6 -0
  105. package/dist/layers/search/app/utils/search/client.d.ts +3 -0
  106. package/dist/layers/search/app/utils/search/client.js +31 -0
  107. package/dist/layers/search/nuxt.config.d.ts +2 -0
  108. package/dist/layers/search/nuxt.config.js +7 -0
  109. package/dist/layers/search/test/runtime-adapter.spec.d.ts +1 -0
  110. package/dist/layers/search/test/runtime-adapter.spec.js +49 -0
  111. package/dist/nuxt.config.d.ts +2 -0
  112. package/dist/nuxt.config.js +7 -0
  113. package/dist/packages/core/src/adapters/auth.d.ts +9 -0
  114. package/dist/packages/core/src/adapters/auth.js +1 -0
  115. package/dist/packages/core/src/adapters/cart.d.ts +22 -0
  116. package/dist/packages/core/src/adapters/cart.js +1 -0
  117. package/dist/packages/core/src/adapters/catalog.d.ts +17 -0
  118. package/dist/packages/core/src/adapters/catalog.js +1 -0
  119. package/dist/packages/core/src/adapters/common.d.ts +9 -0
  120. package/dist/packages/core/src/adapters/common.js +1 -0
  121. package/dist/packages/core/src/adapters/lists.d.ts +17 -0
  122. package/dist/packages/core/src/adapters/lists.js +1 -0
  123. package/dist/packages/core/src/adapters/search.d.ts +21 -0
  124. package/dist/packages/core/src/adapters/search.js +1 -0
  125. package/dist/packages/core/src/plugins/defineAdapter.d.ts +2 -0
  126. package/dist/packages/core/src/plugins/defineAdapter.js +3 -0
  127. package/dist/packages/core/src/plugins/defineModule.d.ts +2 -0
  128. package/dist/packages/core/src/plugins/defineModule.js +3 -0
  129. package/dist/packages/core/src/plugins/registry.d.ts +14 -0
  130. package/dist/packages/core/src/plugins/registry.js +46 -0
  131. package/dist/packages/core/src/runtime/app.d.ts +2 -0
  132. package/dist/packages/core/src/runtime/app.js +20 -0
  133. package/dist/packages/core/src/runtime/context.d.ts +9 -0
  134. package/dist/packages/core/src/runtime/context.js +11 -0
  135. package/dist/packages/core/src/runtime/hooks.d.ts +5 -0
  136. package/dist/packages/core/src/runtime/hooks.js +18 -0
  137. package/dist/packages/core/src/runtime/lifecycle.d.ts +4 -0
  138. package/dist/packages/core/src/runtime/lifecycle.js +5 -0
  139. package/dist/packages/core/src/types/adapters.d.ts +14 -0
  140. package/dist/packages/core/src/types/adapters.js +1 -0
  141. package/dist/packages/core/src/types/app.d.ts +13 -0
  142. package/dist/packages/core/src/types/app.js +1 -0
  143. package/dist/packages/core/src/types/config.d.ts +8 -0
  144. package/dist/packages/core/src/types/config.js +1 -0
  145. package/dist/packages/core/src/types/events.d.ts +20 -0
  146. package/dist/packages/core/src/types/events.js +22 -0
  147. package/dist/packages/core/src/types/module.d.ts +15 -0
  148. package/dist/packages/core/src/types/module.js +1 -0
  149. package/package.json +9 -4
  150. 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 module for the Alternate Framework. It provides a unified, typed search API with pluggable adapters (Meilisearch, OpenSearch, mock adapters), a small CLI for indexing/warmup, and event hooks.
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
- const search = app.context.getAdapter('search')
55
- const results = await search.search({ term: 'shoes' })
56
- console.log(results.items)
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
@@ -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
@@ -1,5 +1,5 @@
1
1
  import { defineNuxtPlugin } from '#imports'
2
- import { getSearchClient, getIndexName } from 'searchkit'
2
+ import { getSearchClient, getIndexName } from '../utils/search/client'
3
3
 
4
4
  export default defineNuxtPlugin((nuxtApp) => {
5
5
  let searchClient
@@ -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,3 @@
1
+ import { type SearchAdapterConfig } from '@meeovi/core';
2
+ import type { MeeoviSearchItem } from './types';
3
+ export declare function createMockSearchAdapter(items?: MeeoviSearchItem[]): import("@meeovi/core").AlternateAdapter<SearchAdapterConfig>;
@@ -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,4 @@
1
+ import type { SearchManager } from '../core/SearchManager';
2
+ export declare function createInstantSearchBridge(manager: SearchManager): {
3
+ searchFunction(helper: any): Promise<void>;
4
+ };
@@ -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,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -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();