@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.
Files changed (42) hide show
  1. package/README.md +269 -0
  2. package/app/components/README.md +3 -0
  3. package/app/components/features/autocomplete.vue +63 -0
  4. package/app/components/features/searchkitSearch.vue +115 -0
  5. package/app/components/filters/filters.vue +0 -0
  6. package/app/components/molecules/SearchInput.vue +39 -0
  7. package/app/components/molecules/pagination.vue +21 -0
  8. package/app/components/molecules/resultList.vue +48 -0
  9. package/app/components/search.vue +87 -0
  10. package/app/composables/adapter/mock.ts +26 -0
  11. package/app/composables/adapter/types.ts +21 -0
  12. package/app/composables/bridges/instantsearch.ts +21 -0
  13. package/app/composables/bridges/react.ts +39 -0
  14. package/app/composables/bridges/searchkit-server.ts +51 -0
  15. package/app/composables/bridges/searchkit.ts +88 -0
  16. package/app/composables/bridges/vue.ts +38 -0
  17. package/app/composables/cli.ts +70 -0
  18. package/app/composables/config/schema.ts +16 -0
  19. package/app/composables/config.ts +20 -0
  20. package/app/composables/core/Facets.ts +9 -0
  21. package/app/composables/core/Filters.ts +13 -0
  22. package/app/composables/core/Pipeline.ts +20 -0
  23. package/app/composables/core/QueryBuilder.ts +27 -0
  24. package/app/composables/core/SearchContext.ts +54 -0
  25. package/app/composables/core/SearchManager.ts +26 -0
  26. package/app/composables/events.ts +5 -0
  27. package/app/composables/index.ts +12 -0
  28. package/app/composables/module.ts +48 -0
  29. package/app/composables/types/api/global-search.ts +8 -0
  30. package/app/composables/types.d.ts +12 -0
  31. package/app/composables/useSearchkit.ts +218 -0
  32. package/app/composables/utils/health.ts +13 -0
  33. package/app/composables/utils/normalizers.ts +6 -0
  34. package/app/pages/results.vue +85 -0
  35. package/app/plugins/instantsearch.js +35 -0
  36. package/app/plugins/search.js +103 -0
  37. package/app/plugins/searchClient.ts +108 -0
  38. package/app/utils/env.ts +28 -0
  39. package/app/utils/search/client.ts +53 -0
  40. package/nuxt.config.ts +11 -0
  41. package/package.json +36 -0
  42. package/tsconfig.json +15 -0
package/README.md ADDED
@@ -0,0 +1,269 @@
1
+ <!-- packages/search/README.md -->
2
+ # @mframework/layer-search
3
+
4
+ A modular, provider-agnostic search layer for the M 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
+
6
+ ## Features
7
+
8
+ - Plug-and-play adapters (Meilisearch, OpenSearch, Mock)
9
+ - Fully typed integration with `@mframework/core`
10
+ - Mock adapter for tests
11
+ - Small CLI for indexing and warmup
12
+ - Configuration validation and event hooks (`search:query`, `search:results`)
13
+
14
+ ## Installation
15
+
16
+ Using npm:
17
+
18
+ ```bash
19
+ npm install @mframework/layer-search
20
+ ```
21
+
22
+ Using pnpm:
23
+
24
+ ```bash
25
+ pnpm add @mframework/layer-search
26
+ ```
27
+
28
+ ## Quick Usage
29
+
30
+ Register the module with an M Framework app:
31
+
32
+ ```ts
33
+ import { createM FrameworkApp } from '@mframework/core'
34
+ import searchModule from '@mframework/layer-search'
35
+
36
+ const app = createM FrameworkApp({
37
+ config: {
38
+ search: {
39
+ defaultProvider: 'opensearch',
40
+ providers: {
41
+ opensearch: {
42
+ host: 'http://localhost:7700',
43
+ index: 'products',
44
+ apiKey: 'masterKey'
45
+ }
46
+ }
47
+ }
48
+ },
49
+ modules: [searchModule]
50
+ })
51
+
52
+ await app.start()
53
+
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
+ }
60
+ ```
61
+
62
+ ## Adapters
63
+
64
+ Adapters are intentionally provider-agnostic and may be supplied by external packages or by your application.
65
+ The core `@mframework/layer-search` layer does not bundle provider implementations; instead provide an adapter instance at startup
66
+ or register one at runtime so the search manager can be created.
67
+
68
+ Example (external adapter package or custom implementation):
69
+
70
+ ```ts
71
+ // import from an external adapter package or your own implementation
72
+ import { createMySearchAdapter } from 'my-search-adapter-package' /* @mframework/adapter-opensearch */
73
+
74
+ const adapter = createMySearchAdapter({ /* provider config */ })
75
+ ```
76
+
77
+ Register the adapter either when creating the M Framework app or at runtime (both shown below).
78
+
79
+ ## Configuration
80
+
81
+ Example `search` config in your app:
82
+
83
+ ```json
84
+ {
85
+ "search": {
86
+ "defaultProvider": "opensearch",
87
+ "providers": {
88
+ "opensearch": {
89
+ "endpoint": "https://my-opensearch.com",
90
+ "index": "products"
91
+ }
92
+ }
93
+ }
94
+ }
95
+ ```
96
+
97
+ The module validates that `defaultProvider` and the referenced provider configuration exist, and that required fields for each adapter are present.
98
+
99
+ Note: this repository includes a top-level `.env.example` with recommended variables for Search and other providers; you can manage layer credentials from your main app's `.env` file. See `../.env.example`.
100
+
101
+ ## UI Integrations (InstantSearch / Searchkit)
102
+
103
+ 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.
104
+
105
+ Example (client):
106
+
107
+ ```ts
108
+ import { createInstantSearchBridge } from '@mframework/layer-search'
109
+ // `manager` is the `SearchManager` instance available on the app context
110
+ const bridge = createInstantSearchBridge(manager)
111
+
112
+ const instantsearchClient = {
113
+ search(requests) {
114
+ return bridge.searchFunction({ state: requests[0].params, setResults: () => {} })
115
+ }
116
+ }
117
+ ```
118
+
119
+ Example (server - Express):
120
+
121
+ ```ts
122
+ import express from 'express'
123
+ import { createSearchkitGraphQLHandler } from '@mframework/layer-search'
124
+
125
+ const app = express()
126
+ app.use(express.json())
127
+ app.post('/graphql', createSearchkitGraphQLHandler(manager))
128
+ ```
129
+
130
+ 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.
131
+
132
+ ## Events
133
+
134
+ This module emits bus events that you can listen to:
135
+
136
+ ```ts
137
+ bus.on('search:query', ({ term }) => {
138
+ console.log('User searched for:', term)
139
+ })
140
+
141
+ bus.on('search:results', ({ term, total }) => {
142
+ console.log(`Search for "${term}" returned ${total} results`)
143
+ })
144
+ ```
145
+
146
+ **Registering external adapters**
147
+
148
+ There are two common ways to register a search adapter so the search layer can use it:
149
+
150
+ - Module-based (recommended at startup): create a small provider module that exposes the adapter via the module `adapters` property. The module registry will register the adapter before modules run.
151
+
152
+ ```ts
153
+ import { createM FrameworkApp } from '@mframework/core'
154
+ import searchModule from '@mframework/layer-search'
155
+ import { createMySearchAdapter } from 'my-search-adapter-package' /* @mframework/adapter-opensearch */
156
+
157
+ const myProviderModule = {
158
+ id: 'search-provider-my',
159
+ adapters: {
160
+ search: createMySearchAdapter({ /* config */ })
161
+ }
162
+ }
163
+
164
+ const app = createM FrameworkApp({
165
+ config: { /* ... */ },
166
+ modules: [searchModule, myProviderModule]
167
+ })
168
+
169
+ await app.start()
170
+ ```
171
+
172
+ - Runtime registration: register an adapter into the core module registry at runtime. This is useful for registering adapters from other modules or dynamic initialization.
173
+
174
+ ```ts
175
+ import { createM FrameworkApp } from '@mframework/core'
176
+ import searchModule from '@mframework/layer-search'
177
+ import { createMySearchAdapter } from 'my-search-adapter-package' /* @mframework/adapter-opensearch */
178
+
179
+ const app = createM FrameworkApp({ modules: [searchModule] })
180
+
181
+ // register adapter before or after `app.start()`
182
+ app.context.modules.registerAdapter('search', createMySearchAdapter({ /* config */ }))
183
+
184
+ await app.start()
185
+ ```
186
+
187
+ Notes:
188
+
189
+ - Many layers adopt a convention of emitting `adapter:registered` events; the search layer listens for adapter registrations and will initialize its `SearchManager` when a `search` adapter becomes available.
190
+ - If you publish adapters, prefer a small package such as `@your-org/adapter-mysearch` that exports a `createMySearchAdapter` factory so consumers can import and register it using one of the patterns above.
191
+
192
+
193
+ ## CLI
194
+
195
+ Included CLI commands:
196
+
197
+ - Warmup: `meeovi-search warmup`
198
+ - Index a JSON file: `meeovi-search index ./products.json`
199
+
200
+ Environment variables supported (example for Meilisearch):
201
+
202
+ - `SEARCH_PROVIDER=opensearch`
203
+ - `MEILI_HOST=http://localhost:7700`
204
+ - `MEILI_INDEX=products`
205
+ - `MEILI_KEY=masterKey`
206
+
207
+ Searchkit / Search provider environment variables
208
+
209
+ - `SEARCHKIT_HOST` or `NUXT_PUBLIC_SEARCHKIT_HOST` — full host URL (e.g. `https://search.example.com`)
210
+ - Alternatively compose with:
211
+ - `SEARCHKIT_PROTOCOL` / `NUXT_PUBLIC_SEARCHKIT_PROTOCOL` (defaults to `http`)
212
+ - `SEARCHKIT_HOSTNAME` / `NUXT_PUBLIC_SEARCHKIT_HOSTNAME` (e.g. `search.example.com`)
213
+ - `SEARCHKIT_PORT` / `NUXT_PUBLIC_SEARCHKIT_PORT` (e.g. `9200`)
214
+ - Optional API key: `SEARCHKIT_API_KEY` or `NUXT_PUBLIC_SEARCHKIT_API_KEY`
215
+
216
+ Notes:
217
+ - Use the `NUXT_PUBLIC_` prefix for values that must be available in client-side code (public build). Keep API keys server-only when possible.
218
+ - The plugin logs a runtime validation message on startup if search provider configuration is missing, with examples of env vars to set.
219
+
220
+ Layer env conventions
221
+
222
+ - All layers use the same environment lookup convention: the code checks `KEY` and falls back to `NUXT_PUBLIC_KEY` when appropriate. This lets you manage provider credentials and endpoints centrally from your main application's `.env` file.
223
+ - Example `.env` entries in your main app to configure the Search layer:
224
+
225
+ ```
226
+ SEARCHKIT_HOST=https://search.example.com
227
+ SEARCHKIT_API_KEY=server-only-key
228
+ # or compose:
229
+ SEARCHKIT_PROTOCOL=https
230
+ SEARCHKIT_HOSTNAME=search.example.com
231
+ SEARCHKIT_PORT=9200
232
+ ```
233
+
234
+ Because every layer follows this `KEY` / `NUXT_PUBLIC_KEY` pattern, you can place settings in the main app's `.env` and they will be available to the layer at runtime without editing layer files.
235
+
236
+ ## Testing
237
+
238
+ Use the mock adapter in tests to avoid external dependencies:
239
+
240
+ ```ts
241
+ import { createMockSearchAdapter } from '@mframework/layer-search'
242
+
243
+ const mock = createMockSearchAdapter([{ id: '1', title: 'Test Product' }])
244
+ const results = await mock.search({ term: 'test' })
245
+ ```
246
+
247
+ ## File structure
248
+
249
+ Typical layout:
250
+
251
+ ```
252
+ @mframework/layer-search
253
+ ├─ src/
254
+ │ ├─ index.ts
255
+ │ ├─ module.ts
256
+ │ ├─ adapter/
257
+ │ │ ├─ mock.ts
258
+ │ │ └─ types.ts
259
+ │ ├─ config/schema.ts
260
+ │ ├─ events.ts
261
+ │ └─ utils/normalizers.ts
262
+ ├─ cli.ts
263
+ ├─ package.json
264
+ └─ README.md
265
+ ```
266
+
267
+ ## License
268
+
269
+ MIT
@@ -0,0 +1,3 @@
1
+ ## Search Directory
2
+
3
+ For all search visual features are found within this directory.
@@ -0,0 +1,63 @@
1
+ <template>
2
+ <div class="autocomplete">
3
+ <input
4
+ v-model="input"
5
+ @input="onInput"
6
+ @keydown.enter.prevent="onSelectFirst"
7
+ :placeholder="placeholder"
8
+ class="autocomplete-input"
9
+ />
10
+ <ul v-if="suggestions.length" class="autocomplete-list">
11
+ <li v-for="(s, i) in suggestions" :key="i" @click="select(s)">
12
+ <div class="title">{{ s.title || s.name || s.label || s.id }}</div>
13
+ <div class="subtitle" v-if="s.description">{{ s.description }}</div>
14
+ </li>
15
+ </ul>
16
+ </div>
17
+ </template>
18
+
19
+ <script setup lang="ts">
20
+ import { ref, watch } from 'vue'
21
+ import useSearchkit from '../../composables/useSearchkit'
22
+
23
+ const props = defineProps({ placeholder: { type: String, default: 'Search...' } })
24
+ const emits = defineEmits(['select', 'input'])
25
+
26
+ const { autocomplete } = useSearchkit()
27
+ const input = ref('')
28
+ const suggestions = ref<any[]>([])
29
+ let timer: any = null
30
+
31
+ async function onInput() {
32
+ emits('input', input.value)
33
+ clearTimeout(timer)
34
+ timer = setTimeout(async () => {
35
+ if (!input.value || input.value.length < 1) {
36
+ suggestions.value = []
37
+ return
38
+ }
39
+ suggestions.value = await autocomplete(input.value, 6)
40
+ }, 180)
41
+ }
42
+
43
+ function select(item: any) {
44
+ emits('select', item)
45
+ suggestions.value = []
46
+ }
47
+
48
+ function onSelectFirst() {
49
+ if (suggestions.value.length) select(suggestions.value[0])
50
+ }
51
+
52
+ watch(input, (v) => {
53
+ if (!v) suggestions.value = []
54
+ })
55
+ </script>
56
+
57
+ <style scoped>
58
+ .autocomplete { position: relative; }
59
+ .autocomplete-list { position: absolute; left: 0; right: 0; background: white; z-index: 50; list-style: none; margin: 0; padding: 0; border: 1px solid #ddd; }
60
+ .autocomplete-list li { padding: 8px; cursor: pointer; }
61
+ .autocomplete-list li:hover { background: #f5f5f5; }
62
+ .autocomplete-input { width: 100%; padding: 8px; border: 1px solid #ccc; }
63
+ </style>
@@ -0,0 +1,115 @@
1
+ <template>
2
+ <div class="searchkit-search">
3
+ <div class="controls">
4
+ <SearchInput v-model="query" @search="onSearch" />
5
+ <autocomplete @input="onInput" @select="onSelect" />
6
+
7
+ <label>
8
+ <select v-model="ranking" @change="onRankingChange">
9
+ <option value="relevance">Relevance</option>
10
+ <option value="newest">Newest</option>
11
+ <option value="popularity">Popularity</option>
12
+ </select>
13
+ </label>
14
+
15
+ <label>
16
+ Semantic:
17
+ <input type="checkbox" v-model="semanticEnabled" @change="toggleSemantic" />
18
+ </label>
19
+
20
+ <label>
21
+ Per page:
22
+ <select v-model.number="perPage" @change="onPerPage">
23
+ <option :value="12">12</option>
24
+ <option :value="24">24</option>
25
+ <option :value="48">48</option>
26
+ </select>
27
+ </label>
28
+ </div>
29
+
30
+ <div class="filters">
31
+ <div v-for="(agg, key) in facets" :key="key" class="facet">
32
+ <strong>{{ key }}</strong>
33
+ <ul>
34
+ <li v-for="(b, idx) in bucketsFor(agg)" :key="idx">{{ b.key }} ({{ b.doc_count }})</li>
35
+ </ul>
36
+ </div>
37
+ </div>
38
+
39
+ <ResultList :hits="hits" :loading="loading">
40
+ <template #item="{ hit }">
41
+ <div>
42
+ <h3>{{ hit.title || hit.name }}</h3>
43
+ <p v-if="hit.description">{{ hit.description }}</p>
44
+ <small v-if="hit.location">{{ hit.location.lat }}, {{ hit.location.lon }}</small>
45
+ </div>
46
+ </template>
47
+ </ResultList>
48
+
49
+ <pagination :page="page" :totalPages="totalPages" @change="onPageChange" />
50
+ </div>
51
+ </template>
52
+
53
+ <script setup lang="ts">
54
+ import { watch, computed } from 'vue'
55
+ import useSearchkit from '../../composables/useSearchkit'
56
+ import SearchInput from '../molecules/SearchInput.vue'
57
+ import autocomplete from './autocomplete.vue'
58
+ import ResultList from '../molecules/resultList.vue'
59
+ import pagination from '../molecules/pagination.vue'
60
+
61
+ const { query, hits, loading, page, perPage, facets, totalPages, search, setPage, setPerPage, setSemantic, ranking } = useSearchkit()
62
+
63
+ const semanticEnabled = computed({ get: () => false, set: (v: boolean) => setSemantic(v) })
64
+
65
+ function onSearch() {
66
+ page.value = 1
67
+ return search()
68
+ }
69
+
70
+ function onInput(val: string) {
71
+ query.value = val
72
+ }
73
+
74
+ function onSelect(item: any) {
75
+ // if suggestion contains a query text or id, perform targeted search
76
+ query.value = item.query || item.title || item.name || query.value
77
+ search()
78
+ }
79
+
80
+ function onPageChange(p: number) {
81
+ return setPage(p)
82
+ }
83
+
84
+ function onPerPage(e: any) {
85
+ return setPerPage(parseInt(e.target.value || '12', 10))
86
+ }
87
+
88
+ function toggleSemantic(e: Event) {
89
+ const target = e.target as HTMLInputElement
90
+ return setSemantic(target.checked)
91
+ }
92
+
93
+ function onRankingChange(e: Event) {
94
+ const target = e.target as HTMLSelectElement
95
+ ranking.value = target.value as any
96
+ return search()
97
+ }
98
+
99
+ function bucketsFor(agg: any) {
100
+ return (agg && (agg.buckets || agg.terms || agg)) || []
101
+ }
102
+
103
+ // initialize
104
+ search()
105
+
106
+ watch(query, () => {
107
+ // do not auto-search on every keystroke by default; controlled via SearchInput
108
+ })
109
+ </script>
110
+
111
+ <style scoped>
112
+ .controls { display:flex; gap:12px; align-items:center; flex-wrap:wrap }
113
+ .filters { margin-top:12px; display:flex; gap:16px }
114
+ .facet { background:#fafafa; padding:8px; border:1px solid #eee }
115
+ </style>
File without changes
@@ -0,0 +1,39 @@
1
+ <template>
2
+ <div class="search-input">
3
+ <input
4
+ :placeholder="placeholder"
5
+ :value="modelValue"
6
+ @input="onInput"
7
+ @keydown.enter.prevent="onEnter"
8
+ class="search-input-field"
9
+ />
10
+ <button @click="onSearch" class="search-input-button">Search</button>
11
+ </div>
12
+ </template>
13
+
14
+ <script setup lang="ts">
15
+ const props = defineProps({
16
+ modelValue: { type: String, default: '' },
17
+ placeholder: { type: String, default: 'Search...' },
18
+ })
19
+ const emit = defineEmits(['update:modelValue', 'search'])
20
+
21
+ function onInput(e: Event) {
22
+ const v = (e.target as HTMLInputElement).value
23
+ emit('update:modelValue', v)
24
+ }
25
+
26
+ function onEnter() {
27
+ emit('search')
28
+ }
29
+
30
+ function onSearch() {
31
+ emit('search')
32
+ }
33
+ </script>
34
+
35
+ <style scoped>
36
+ .search-input { display:flex; gap:8px; align-items:center }
37
+ .search-input-field { padding:8px; border:1px solid #ccc; }
38
+ .search-input-button { padding:8px 12px }
39
+ </style>
@@ -0,0 +1,21 @@
1
+ <template>
2
+ <div class="pagination">
3
+ <button :disabled="page <= 1" @click="change(page - 1)">Prev</button>
4
+ <span class="page-info">Page {{ page }} / {{ totalPages }}</span>
5
+ <button :disabled="page >= totalPages" @click="change(page + 1)">Next</button>
6
+ </div>
7
+ </template>
8
+
9
+ <script setup lang="ts">
10
+ const props = defineProps({ page: { type: Number, default: 1 }, totalPages: { type: Number, default: 1 } })
11
+ const emit = defineEmits(['change'])
12
+
13
+ function change(p: number) {
14
+ emit('change', p)
15
+ }
16
+ </script>
17
+
18
+ <style scoped>
19
+ .pagination { display:flex; gap:12px; align-items:center }
20
+ .page-info { font-weight:600 }
21
+ </style>
@@ -0,0 +1,48 @@
1
+ <template>
2
+ <div class="result-list">
3
+ <div v-if="loading" class="loading">Loading…</div>
4
+ <div v-else>
5
+ <div v-if="!hits || hits.length === 0" class="empty">No results</div>
6
+ <div v-else>
7
+ <div v-for="(hit, idx) in hits" :key="idx" class="result-item">
8
+ <slot name="item" :hit="hit">
9
+ <pre>{{ hit }}</pre>
10
+ </slot>
11
+ </div>
12
+ </div>
13
+ </div>
14
+ </div>
15
+ </template>
16
+
17
+ <script setup lang="ts">
18
+ const props = defineProps({
19
+ hits: {
20
+ type: (Array as any) as () => Array<any>,
21
+ default: () => []
22
+ },
23
+ loading: {
24
+ type: Boolean,
25
+ default: false
26
+ }
27
+ })
28
+ </script>
29
+
30
+ <style scoped>
31
+ .result-list {
32
+ display: block
33
+ }
34
+
35
+ .result-item {
36
+ padding: 12px;
37
+ border-bottom: 1px solid #eee
38
+ }
39
+
40
+ .loading {
41
+ padding: 12px
42
+ }
43
+
44
+ .empty {
45
+ padding: 12px;
46
+ color: #666
47
+ }
48
+ </style>
@@ -0,0 +1,87 @@
1
+ <template>
2
+ <div class="searchField">
3
+ <div class="container">
4
+ <ais-instant-search :search-client="searchClient" :index-name="indexName">
5
+ <ais-configure :hits-per-page.camel="8" />
6
+ <div class="search-panel">
7
+ <div class="search-panel__filters">
8
+ <ais-panel>
9
+ <template v-slot:header>type</template>
10
+ <ais-refinement-list attribute="type" />
11
+ </ais-panel>
12
+
13
+ <ais-panel>
14
+ <template v-slot:header>actors</template>
15
+ <ais-refinement-list searchable attribute="actors" />
16
+ </ais-panel>
17
+ </div>
18
+
19
+ <div class="search-panel__results">
20
+ <div class="searchbox">
21
+ <ais-search-box placeholder="" />
22
+ <v-text-field v-if="isDev" v-model="searchQuery" placeholder="Debug: search input" class="debug-search-input"></v-text-field>
23
+ </div>
24
+ <ais-hits>
25
+ <template v-slot:item="{ item, index }">
26
+ <article @click="openResult(item)" style="cursor:pointer">
27
+ <h1>
28
+ <ais-highlight attribute="title" :hit="item" />
29
+ </h1>
30
+ <p>
31
+ <ais-snippet :hit="item" attribute="plot" />
32
+ </p>
33
+ </article>
34
+ </template>
35
+ </ais-hits>
36
+
37
+ <div class="pagination">
38
+ <ais-pagination />
39
+ </div>
40
+ </div>
41
+ </div>
42
+ </ais-instant-search>
43
+ </div>
44
+ </div>
45
+ </template>
46
+
47
+ <script setup lang="ts">
48
+ import {
49
+ useRouter
50
+ } from 'vue-router'
51
+
52
+ import {
53
+ type Ref,
54
+ ref,
55
+ watch
56
+ } from 'vue';
57
+ import Client from '@searchkit/instantsearch-client'
58
+
59
+ const searchClient = Client({
60
+ url: '/api/search'
61
+ })
62
+
63
+ const configDetails = useRuntimeConfig()
64
+
65
+ const router = useRouter()
66
+ const searchQuery = ref('');
67
+ const indexName = configDetails.public.indexName;
68
+ const isDev = process.env.NODE_ENV !== 'production'
69
+
70
+
71
+ if (process.env.NODE_ENV !== 'production') {
72
+ // eslint-disable-next-line no-console
73
+ console.debug('[search] searchClient', searchClient, 'has search method:', typeof searchClient.search)
74
+ }
75
+
76
+ function openResult(item: any) {
77
+ const id = item._id ?? item.id ?? '';
78
+ const title = item.title ?? '';
79
+ router.push({
80
+ path: '/results',
81
+ query: {
82
+ id,
83
+ title
84
+ }
85
+ });
86
+ }
87
+ </script>
@@ -0,0 +1,26 @@
1
+ import type { MeeoviSearchItem } from './types'
2
+ import type { BuiltSearchQuery } from '../core/QueryBuilder'
3
+
4
+ export function createMockSearchAdapter(items: MeeoviSearchItem[] = []) {
5
+ const cfg = { provider: 'mock' }
6
+
7
+ const adapter = {
8
+ id: 'search:mock',
9
+ type: 'search',
10
+ config: cfg,
11
+
12
+ async search(query: BuiltSearchQuery | any) {
13
+ const term = String((query && (query.term || query.params?.q)) || '')
14
+ const filtered = items.filter((item) => item.title?.toLowerCase().includes(term.toLowerCase()))
15
+
16
+ return {
17
+ items: filtered,
18
+ total: filtered.length,
19
+ page: 1,
20
+ pageSize: filtered.length
21
+ }
22
+ }
23
+ }
24
+
25
+ return adapter
26
+ }