@meeovi/layer-search 1.0.7 → 1.0.8

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 +31 -0
  2. package/app/components/features/autocomplete.vue +63 -0
  3. package/app/components/features/searchkitSearch.vue +115 -0
  4. package/app/components/molecules/pagination.vue +17 -0
  5. package/app/components/molecules/resultList.vue +25 -0
  6. package/app/composables/useSearchkit.ts +217 -0
  7. package/app/plugins/search.js +83 -0
  8. package/app/utils/env.ts +28 -0
  9. package/app/utils/search/client.ts +22 -7
  10. package/package.json +1 -2
  11. package/dist/app/composables/adapter/meilisearch.d.ts +0 -8
  12. package/dist/app/composables/adapter/meilisearch.js +0 -36
  13. package/dist/app/composables/adapter/mock.d.ts +0 -3
  14. package/dist/app/composables/adapter/mock.js +0 -19
  15. package/dist/app/composables/adapter/opensearch.d.ts +0 -8
  16. package/dist/app/composables/adapter/opensearch.js +0 -46
  17. package/dist/app/composables/adapter/types.d.ts +0 -12
  18. package/dist/app/composables/adapter/types.js +0 -1
  19. package/dist/app/composables/bridges/instantsearch.d.ts +0 -4
  20. package/dist/app/composables/bridges/instantsearch.js +0 -17
  21. package/dist/app/composables/bridges/react.d.ts +0 -12
  22. package/dist/app/composables/bridges/react.js +0 -34
  23. package/dist/app/composables/bridges/vue.d.ts +0 -9
  24. package/dist/app/composables/bridges/vue.js +0 -31
  25. package/dist/app/composables/cli.d.ts +0 -2
  26. package/dist/app/composables/cli.js +0 -69
  27. package/dist/app/composables/config/schema.d.ts +0 -5
  28. package/dist/app/composables/config/schema.js +0 -8
  29. package/dist/app/composables/config.d.ts +0 -7
  30. package/dist/app/composables/config.js +0 -11
  31. package/dist/app/composables/core/Facets.d.ts +0 -17
  32. package/dist/app/composables/core/Facets.js +0 -8
  33. package/dist/app/composables/core/Filters.d.ts +0 -18
  34. package/dist/app/composables/core/Filters.js +0 -11
  35. package/dist/app/composables/core/Normalizers.d.ts +0 -0
  36. package/dist/app/composables/core/Normalizers.js +0 -1
  37. package/dist/app/composables/core/Pipeline.d.ts +0 -8
  38. package/dist/app/composables/core/Pipeline.js +0 -16
  39. package/dist/app/composables/core/QueryBuilder.d.ts +0 -13
  40. package/dist/app/composables/core/QueryBuilder.js +0 -15
  41. package/dist/app/composables/core/SearchContext.d.ts +0 -18
  42. package/dist/app/composables/core/SearchContext.js +0 -38
  43. package/dist/app/composables/core/SearchManager.d.ts +0 -10
  44. package/dist/app/composables/core/SearchManager.js +0 -20
  45. package/dist/app/composables/events.d.ts +0 -11
  46. package/dist/app/composables/events.js +0 -1
  47. package/dist/app/composables/index.d.ts +0 -9
  48. package/dist/app/composables/index.js +0 -9
  49. package/dist/app/composables/module.d.ts +0 -17
  50. package/dist/app/composables/module.js +0 -39
  51. package/dist/app/composables/types/api/global-search.d.ts +0 -8
  52. package/dist/app/composables/types/api/global-search.js +0 -1
  53. package/dist/app/composables/utils/normalizers.d.ts +0 -1
  54. package/dist/app/composables/utils/normalizers.js +0 -6
  55. package/dist/app/utils/search/client.d.ts +0 -3
  56. package/dist/app/utils/search/client.js +0 -31
  57. package/dist/layers/search/app/composables/adapter/meilisearch.d.ts +0 -8
  58. package/dist/layers/search/app/composables/adapter/meilisearch.js +0 -36
  59. package/dist/layers/search/app/composables/adapter/mock.d.ts +0 -3
  60. package/dist/layers/search/app/composables/adapter/mock.js +0 -19
  61. package/dist/layers/search/app/composables/adapter/opensearch.d.ts +0 -8
  62. package/dist/layers/search/app/composables/adapter/opensearch.js +0 -46
  63. package/dist/layers/search/app/composables/adapter/types.d.ts +0 -12
  64. package/dist/layers/search/app/composables/adapter/types.js +0 -1
  65. package/dist/layers/search/app/composables/bridges/instantsearch.d.ts +0 -4
  66. package/dist/layers/search/app/composables/bridges/instantsearch.js +0 -17
  67. package/dist/layers/search/app/composables/bridges/react.d.ts +0 -12
  68. package/dist/layers/search/app/composables/bridges/react.js +0 -34
  69. package/dist/layers/search/app/composables/bridges/searchkit-server.d.ts +0 -3
  70. package/dist/layers/search/app/composables/bridges/searchkit-server.js +0 -44
  71. package/dist/layers/search/app/composables/bridges/searchkit.d.ts +0 -21
  72. package/dist/layers/search/app/composables/bridges/searchkit.js +0 -60
  73. package/dist/layers/search/app/composables/bridges/vue.d.ts +0 -9
  74. package/dist/layers/search/app/composables/bridges/vue.js +0 -31
  75. package/dist/layers/search/app/composables/cli.d.ts +0 -2
  76. package/dist/layers/search/app/composables/cli.js +0 -69
  77. package/dist/layers/search/app/composables/config/schema.d.ts +0 -5
  78. package/dist/layers/search/app/composables/config/schema.js +0 -8
  79. package/dist/layers/search/app/composables/config.d.ts +0 -7
  80. package/dist/layers/search/app/composables/config.js +0 -11
  81. package/dist/layers/search/app/composables/core/Facets.d.ts +0 -17
  82. package/dist/layers/search/app/composables/core/Facets.js +0 -8
  83. package/dist/layers/search/app/composables/core/Filters.d.ts +0 -18
  84. package/dist/layers/search/app/composables/core/Filters.js +0 -11
  85. package/dist/layers/search/app/composables/core/Normalizers.d.ts +0 -0
  86. package/dist/layers/search/app/composables/core/Normalizers.js +0 -1
  87. package/dist/layers/search/app/composables/core/Pipeline.d.ts +0 -8
  88. package/dist/layers/search/app/composables/core/Pipeline.js +0 -16
  89. package/dist/layers/search/app/composables/core/QueryBuilder.d.ts +0 -13
  90. package/dist/layers/search/app/composables/core/QueryBuilder.js +0 -15
  91. package/dist/layers/search/app/composables/core/SearchContext.d.ts +0 -18
  92. package/dist/layers/search/app/composables/core/SearchContext.js +0 -38
  93. package/dist/layers/search/app/composables/core/SearchManager.d.ts +0 -10
  94. package/dist/layers/search/app/composables/core/SearchManager.js +0 -20
  95. package/dist/layers/search/app/composables/events.d.ts +0 -11
  96. package/dist/layers/search/app/composables/events.js +0 -1
  97. package/dist/layers/search/app/composables/index.d.ts +0 -12
  98. package/dist/layers/search/app/composables/index.js +0 -12
  99. package/dist/layers/search/app/composables/module.d.ts +0 -17
  100. package/dist/layers/search/app/composables/module.js +0 -73
  101. package/dist/layers/search/app/composables/types/api/global-search.d.ts +0 -8
  102. package/dist/layers/search/app/composables/types/api/global-search.js +0 -1
  103. package/dist/layers/search/app/composables/utils/health.d.ts +0 -11
  104. package/dist/layers/search/app/composables/utils/health.js +0 -11
  105. package/dist/layers/search/app/composables/utils/normalizers.d.ts +0 -1
  106. package/dist/layers/search/app/composables/utils/normalizers.js +0 -6
  107. package/dist/layers/search/app/utils/search/client.d.ts +0 -3
  108. package/dist/layers/search/app/utils/search/client.js +0 -31
  109. package/dist/layers/search/nuxt.config.d.ts +0 -2
  110. package/dist/layers/search/nuxt.config.js +0 -7
  111. package/dist/layers/search/test/runtime-adapter.spec.d.ts +0 -1
  112. package/dist/layers/search/test/runtime-adapter.spec.js +0 -49
  113. package/dist/nuxt.config.d.ts +0 -2
  114. package/dist/nuxt.config.js +0 -7
  115. package/dist/packages/core/src/adapters/auth.d.ts +0 -9
  116. package/dist/packages/core/src/adapters/auth.js +0 -1
  117. package/dist/packages/core/src/adapters/cart.d.ts +0 -22
  118. package/dist/packages/core/src/adapters/cart.js +0 -1
  119. package/dist/packages/core/src/adapters/catalog.d.ts +0 -17
  120. package/dist/packages/core/src/adapters/catalog.js +0 -1
  121. package/dist/packages/core/src/adapters/common.d.ts +0 -9
  122. package/dist/packages/core/src/adapters/common.js +0 -1
  123. package/dist/packages/core/src/adapters/lists.d.ts +0 -17
  124. package/dist/packages/core/src/adapters/lists.js +0 -1
  125. package/dist/packages/core/src/adapters/search.d.ts +0 -21
  126. package/dist/packages/core/src/adapters/search.js +0 -1
  127. package/dist/packages/core/src/plugins/defineAdapter.d.ts +0 -2
  128. package/dist/packages/core/src/plugins/defineAdapter.js +0 -3
  129. package/dist/packages/core/src/plugins/defineModule.d.ts +0 -2
  130. package/dist/packages/core/src/plugins/defineModule.js +0 -3
  131. package/dist/packages/core/src/plugins/registry.d.ts +0 -14
  132. package/dist/packages/core/src/plugins/registry.js +0 -46
  133. package/dist/packages/core/src/runtime/app.d.ts +0 -2
  134. package/dist/packages/core/src/runtime/app.js +0 -20
  135. package/dist/packages/core/src/runtime/context.d.ts +0 -9
  136. package/dist/packages/core/src/runtime/context.js +0 -11
  137. package/dist/packages/core/src/runtime/hooks.d.ts +0 -5
  138. package/dist/packages/core/src/runtime/hooks.js +0 -18
  139. package/dist/packages/core/src/runtime/lifecycle.d.ts +0 -4
  140. package/dist/packages/core/src/runtime/lifecycle.js +0 -5
  141. package/dist/packages/core/src/types/adapters.d.ts +0 -14
  142. package/dist/packages/core/src/types/adapters.js +0 -1
  143. package/dist/packages/core/src/types/app.d.ts +0 -13
  144. package/dist/packages/core/src/types/app.js +0 -1
  145. package/dist/packages/core/src/types/config.d.ts +0 -8
  146. package/dist/packages/core/src/types/config.js +0 -1
  147. package/dist/packages/core/src/types/events.d.ts +0 -20
  148. package/dist/packages/core/src/types/events.js +0 -22
  149. package/dist/packages/core/src/types/module.d.ts +0 -15
  150. package/dist/packages/core/src/types/module.js +0 -1
package/README.md CHANGED
@@ -118,6 +118,8 @@ Example `search` config in your app:
118
118
 
119
119
  The module validates that `defaultProvider` and the referenced provider configuration exist, and that required fields for each adapter are present.
120
120
 
121
+ 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`.
122
+
121
123
  ## UI Integrations (InstantSearch / Searchkit)
122
124
 
123
125
  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.
@@ -177,6 +179,35 @@ Environment variables supported (example for Meilisearch):
177
179
  - `MEILI_INDEX=products`
178
180
  - `MEILI_KEY=masterKey`
179
181
 
182
+ Searchkit / Search provider environment variables
183
+
184
+ - `SEARCHKIT_HOST` or `NUXT_PUBLIC_SEARCHKIT_HOST` — full host URL (e.g. `https://search.example.com`)
185
+ - Alternatively compose with:
186
+ - `SEARCHKIT_PROTOCOL` / `NUXT_PUBLIC_SEARCHKIT_PROTOCOL` (defaults to `http`)
187
+ - `SEARCHKIT_HOSTNAME` / `NUXT_PUBLIC_SEARCHKIT_HOSTNAME` (e.g. `search.example.com`)
188
+ - `SEARCHKIT_PORT` / `NUXT_PUBLIC_SEARCHKIT_PORT` (e.g. `9200`)
189
+ - Optional API key: `SEARCHKIT_API_KEY` or `NUXT_PUBLIC_SEARCHKIT_API_KEY`
190
+
191
+ Notes:
192
+ - Use the `NUXT_PUBLIC_` prefix for values that must be available in client-side code (public build). Keep API keys server-only when possible.
193
+ - The plugin logs a runtime validation message on startup if search provider configuration is missing, with examples of env vars to set.
194
+
195
+ Layer env conventions
196
+
197
+ - 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.
198
+ - Example `.env` entries in your main app to configure the Search layer:
199
+
200
+ ```
201
+ SEARCHKIT_HOST=https://search.example.com
202
+ SEARCHKIT_API_KEY=server-only-key
203
+ # or compose:
204
+ SEARCHKIT_PROTOCOL=https
205
+ SEARCHKIT_HOSTNAME=search.example.com
206
+ SEARCHKIT_PORT=9200
207
+ ```
208
+
209
+ 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.
210
+
180
211
  ## Testing
181
212
 
182
213
  Use the mock adapter in tests to avoid external dependencies:
@@ -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>
@@ -0,0 +1,17 @@
1
+ <template>
2
+ <nav class="pagination" v-if="totalPages > 1">
3
+ <button :disabled="page <= 1" @click="$emit('change', page - 1)">Prev</button>
4
+ <span>Page {{ page }} / {{ totalPages }}</span>
5
+ <button :disabled="page >= totalPages" @click="$emit('change', page + 1)">Next</button>
6
+ </nav>
7
+ </template>
8
+
9
+ <script setup lang="ts">
10
+ const props = defineProps({ page: { type: Number, required: true }, totalPages: { type: Number, required: true } })
11
+ const emits = defineEmits(['change'])
12
+ </script>
13
+
14
+ <style scoped>
15
+ .pagination { display:flex; gap:8px; align-items:center; }
16
+ .pagination button[disabled] { opacity: 0.5 }
17
+ </style>
@@ -0,0 +1,25 @@
1
+ <template>
2
+ <div class="result-list">
3
+ <div v-if="loading">Loading…</div>
4
+ <div v-else>
5
+ <div class="hits" v-if="hits.length">
6
+ <div v-for="(hit, idx) in hits" :key="hit._meta || idx" class="hit-item">
7
+ <slot name="item" :hit="hit">
8
+ <h3>{{ hit.title || hit.name || hit.id }}</h3>
9
+ <p v-if="hit.description">{{ hit.description }}</p>
10
+ </slot>
11
+ </div>
12
+ </div>
13
+ <div v-else class="no-results">No results</div>
14
+ </div>
15
+ </div>
16
+ </template>
17
+
18
+ <script setup lang="ts">
19
+ const props = defineProps({ hits: { type: Array, default: () => [] }, loading: { type: Boolean, default: false } })
20
+ </script>
21
+
22
+ <style scoped>
23
+ .hit-item { padding: 12px; border-bottom: 1px solid #eee; }
24
+ .no-results { color: #777; padding: 12px; }
25
+ </style>
@@ -0,0 +1,217 @@
1
+ import { ref, computed } from 'vue'
2
+ import { useNuxtApp } from 'nuxt/app'
3
+
4
+ export function useSearchkit() {
5
+ const nuxt = useNuxtApp() as any
6
+ const client: any = nuxt.$searchClient
7
+ const indexName: string = (nuxt.$searchIndexName as string) || 'default'
8
+ const helpers: any = nuxt.$searchHelpers || {}
9
+
10
+ const query = ref('')
11
+ const hits = ref<any[]>([])
12
+ const total = ref(0)
13
+ const loading = ref(false)
14
+ const page = ref(1)
15
+ const perPage = ref(12)
16
+ const sortBy = ref<string | null>(null)
17
+ const facets = ref<Record<string, any>>({})
18
+ const geo = ref<{ lat?: number; lon?: number; distanceKm?: number }>({})
19
+ const semantic = ref(false)
20
+ const ranking = ref<'relevance' | 'newest' | 'popularity' | 'custom'>('relevance')
21
+
22
+ async function search() {
23
+ if (!client || typeof client.search !== 'function') {
24
+ throw new Error('Search client is not available or does not implement `search`')
25
+ }
26
+
27
+ loading.value = true
28
+ const from = (page.value - 1) * perPage.value
29
+
30
+ // Build params — aim for compatibility with Searchkit/Elasticsearch via instantsearch client.
31
+ const params: any = {
32
+ params: {
33
+ q: query.value || '*',
34
+ size: perPage.value,
35
+ from,
36
+ },
37
+ }
38
+
39
+ // Sorting strategies
40
+ if (ranking.value === 'newest') {
41
+ params.params.sort = 'created_at:desc'
42
+ } else if (ranking.value === 'popularity') {
43
+ params.params.sort = 'popularity:desc'
44
+ } else if (sortBy.value) {
45
+ params.params.sort = sortBy.value
46
+ }
47
+
48
+ // Geo filter: translate into an Elasticsearch geo_distance filter in body
49
+ if (geo.value.lat && geo.value.lon && geo.value.distanceKm) {
50
+ params.body = params.body || {}
51
+ params.body.query = params.body.query || { bool: { must: [{ match_all: {} }], filter: [] } }
52
+ const filter = { geo_distance: { distance: `${geo.value.distanceKm}km`, location: { lat: geo.value.lat, lon: geo.value.lon } } }
53
+ params.body.query.bool.filter.push(filter)
54
+ }
55
+
56
+ // Semantic hint flag — some backends will honour this to run semantic/rerank
57
+ if (semantic.value) {
58
+ params.params.semantic = true
59
+ }
60
+
61
+ try {
62
+ const results = await client.search([{ indexName, ...params }])
63
+ const res = results && results[0]
64
+ if (!res) {
65
+ hits.value = []
66
+ total.value = 0
67
+ facets.value = {}
68
+ return
69
+ }
70
+
71
+ // Best-effort parsing for common instantsearch-style responses
72
+ const rawHits = (res.hits && res.hits.hits) || (res.hits && res.hits) || res.hits
73
+ let parsedHits: any[] = []
74
+ if (Array.isArray(rawHits)) {
75
+ // ES style hits.hits
76
+ parsedHits = rawHits.map((h: any) => (h._source ? { ...h._source, _meta: h._id } : h))
77
+ } else if (res.hits && res.hits.hits) {
78
+ parsedHits = res.hits.hits.map((h: any) => (h._source ? { ...h._source, _meta: h._id } : h))
79
+ } else if (res.hits) {
80
+ parsedHits = res.hits
81
+ } else {
82
+ parsedHits = []
83
+ }
84
+
85
+ total.value = (res.hits && (res.hits.total?.value ?? res.hits.total)) || res.nbHits || 0
86
+
87
+ // Aggregations / facets parsing (normalize via plugin helper when available)
88
+ const rawAggs = res.aggregations || res.facets || {}
89
+ if (helpers && typeof helpers.mapAggregations === 'function') {
90
+ try {
91
+ facets.value = helpers.mapAggregations(rawAggs)
92
+ } catch (e) {
93
+ facets.value = rawAggs
94
+ }
95
+ } else {
96
+ facets.value = rawAggs
97
+ }
98
+
99
+ // If semantic rerank is enabled, attempt to call the plugin helper to reorder results.
100
+ if (semantic.value && helpers && typeof helpers.semanticRerank === 'function') {
101
+ try {
102
+ const reranked = await helpers.semanticRerank(parsedHits, query.value)
103
+ hits.value = Array.isArray(reranked) ? reranked : parsedHits
104
+ } catch (e) {
105
+ // fallback to parsedHits on error
106
+ hits.value = parsedHits
107
+ }
108
+ } else {
109
+ hits.value = parsedHits
110
+ }
111
+ } finally {
112
+ loading.value = false
113
+ }
114
+ }
115
+
116
+ async function autocomplete(input: string, limit = 6) {
117
+ if (!client) return []
118
+ try {
119
+ // Use a small search request to act as suggestions
120
+ const params = {
121
+ params: { q: input || '*', size: limit },
122
+ }
123
+ const result = await client.search([{ indexName, ...params }])
124
+ const r = result && result[0]
125
+ const suggestionHits = (r && ((r.hits && r.hits.hits) || r.hits)) || []
126
+ return Array.isArray(suggestionHits)
127
+ ? suggestionHits.map((h: any) => (h._source ? h._source : h))
128
+ : []
129
+ } catch (e) {
130
+ return []
131
+ }
132
+ }
133
+
134
+ function setPage(p: number) {
135
+ page.value = p
136
+ return search()
137
+ }
138
+
139
+ function setPerPage(n: number) {
140
+ perPage.value = n
141
+ page.value = 1
142
+ return search()
143
+ }
144
+
145
+ function setSort(sort: string | null) {
146
+ sortBy.value = sort
147
+ return search()
148
+ }
149
+
150
+ function setGeo(lat: number, lon: number, distanceKm = 50) {
151
+ geo.value = { lat, lon, distanceKm }
152
+ page.value = 1
153
+ return search()
154
+ }
155
+
156
+ function setSemantic(enabled: boolean) {
157
+ semantic.value = enabled
158
+ return search()
159
+ }
160
+
161
+ const totalPages = computed(() => Math.max(1, Math.ceil((total.value || 0) / perPage.value)))
162
+
163
+ // Provide a normalized facets structure for consumers: { facetName: [{ key, doc_count }, ...] }
164
+ const normalizedFacets = computed(() => {
165
+ const out: Record<string, Array<{ key: string; doc_count: number }>> = {}
166
+ try {
167
+ for (const k of Object.keys(facets.value || {})) {
168
+ const v = (facets.value as any)[k]
169
+ if (!v) {
170
+ out[k] = []
171
+ continue
172
+ }
173
+ if (Array.isArray(v)) {
174
+ out[k] = v.map((b: any) => ({ key: b.key ?? b.value ?? String(b), doc_count: b.doc_count ?? b.count ?? 0 }))
175
+ } else if (v.buckets) {
176
+ out[k] = v.buckets.map((b: any) => ({ key: b.key ?? b.value, doc_count: b.doc_count ?? 0 }))
177
+ } else if (v.terms) {
178
+ out[k] = v.terms.map((b: any) => ({ key: b.key ?? b.value, doc_count: b.doc_count ?? 0 }))
179
+ } else {
180
+ // generic object — try to map entries
181
+ out[k] = Object.keys(v).map((kk) => ({ key: kk, doc_count: (v as any)[kk] }))
182
+ }
183
+ }
184
+ } catch (e) {
185
+ return {}
186
+ }
187
+ return out
188
+ })
189
+
190
+ return {
191
+ // state
192
+ query,
193
+ hits,
194
+ total,
195
+ loading,
196
+ page,
197
+ perPage,
198
+ sortBy,
199
+ facets,
200
+ geo,
201
+ semantic,
202
+ ranking,
203
+ totalPages,
204
+
205
+ // actions
206
+ search,
207
+ autocomplete,
208
+ setPage,
209
+ setPerPage,
210
+ setSort,
211
+ setGeo,
212
+ setSemantic,
213
+ normalizedFacets,
214
+ }
215
+ }
216
+
217
+ export default useSearchkit
@@ -9,12 +9,95 @@ export default defineNuxtPlugin((nuxtApp) => {
9
9
  indexName = getIndexName()
10
10
  } catch (e) {
11
11
  // fallback placeholder to avoid build/runtime crash when not configured
12
+ // Provide a clearer validation message to help users wire env vars
12
13
  // eslint-disable-next-line no-console
13
14
  console.warn('Search client not configured:', e.message || e)
15
+ // Helpful runtime guidance
16
+ // eslint-disable-next-line no-console
17
+ console.info('Search layer requires configuring SEARCHKIT_HOST or SEARCHKIT_HOSTNAME/SEARCHKIT_PORT (or their NUXT_PUBLIC_ variants). Example:')
18
+ // eslint-disable-next-line no-console
19
+ console.info(' SEARCHKIT_HOST=https://search.example.com')
20
+ // eslint-disable-next-line no-console
21
+ console.info(' or:')
22
+ // eslint-disable-next-line no-console
23
+ console.info(' SEARCHKIT_PROTOCOL=https')
24
+ // eslint-disable-next-line no-console
25
+ console.info(' SEARCHKIT_HOSTNAME=search.example.com')
26
+ // eslint-disable-next-line no-console
27
+ console.info(' SEARCHKIT_PORT=9200')
14
28
  searchClient = { _client: 'searchkit-fallback' }
15
29
  indexName = 'default'
16
30
  }
17
31
 
32
+ // Helper: normalize aggregations/ facets into { name: buckets[] }
33
+ function mapAggregations(aggs) {
34
+ const mapped = {}
35
+ if (!aggs) return mapped
36
+ for (const key of Object.keys(aggs)) {
37
+ const a = aggs[key]
38
+ if (!a) continue
39
+ if (Array.isArray(a)) mapped[key] = a
40
+ else if (a.buckets) mapped[key] = a.buckets
41
+ else if (a.terms) mapped[key] = a.terms
42
+ else mapped[key] = a
43
+ }
44
+ return mapped
45
+ }
46
+
47
+ // Basic suggestion helper that performs a small search to return candidate documents
48
+ async function suggest(q, opts = {}) {
49
+ if (!searchClient || typeof searchClient.search !== 'function') return []
50
+ const size = opts.size || 6
51
+ const params = { params: { q: q || '*', size } }
52
+ try {
53
+ const resArr = await searchClient.search([{ indexName, ...params }])
54
+ const res = resArr && resArr[0]
55
+ const hits = (res && ((res.hits && res.hits.hits) || res.hits)) || []
56
+ return Array.isArray(hits) ? hits.map((h) => (h._source ? h._source : h)) : []
57
+ } catch (e) {
58
+ // eslint-disable-next-line no-console
59
+ console.warn('Suggest failed', e && e.message ? e.message : e)
60
+ return []
61
+ }
62
+ }
63
+
64
+ // Server-side semantic reranking: call embeddings API if configured, otherwise noop.
65
+ // Expects an embeddings API that can accept { query, docs } and return either
66
+ // { ranked: [ids...] } or { scores: [number...] } aligned with docs.
67
+ async function semanticRerank(hitsArray, queryStr) {
68
+ const url = process.env.NUXT_PUBLIC_EMBEDDINGS_API_URL || process.env.EMBEDDINGS_API_URL
69
+ const key = process.env.NUXT_PUBLIC_EMBEDDINGS_API_KEY || process.env.EMBEDDINGS_API_KEY
70
+ if (!url || !key) return hitsArray
71
+ try {
72
+ const docs = hitsArray.slice(0, 100).map((h) => ({ id: h._meta || h.id || h._id, text: h.title || h.name || h.description || '' }))
73
+ const body = { query: queryStr, docs }
74
+ const resp = await (globalThis.fetch || fetch)(url, {
75
+ method: 'POST',
76
+ headers: { 'content-type': 'application/json', authorization: `Bearer ${key}` },
77
+ body: JSON.stringify(body),
78
+ })
79
+ if (!resp.ok) return hitsArray
80
+ const json = await resp.json()
81
+ if (Array.isArray(json.ranked)) {
82
+ const idToHit = new Map(hitsArray.map((h) => [(h._meta || h.id || h._id), h]))
83
+ const ordered = json.ranked.map((id) => idToHit.get(id)).filter(Boolean)
84
+ const remaining = hitsArray.filter((h) => !ordered.includes(h))
85
+ return [...ordered, ...remaining]
86
+ }
87
+ if (Array.isArray(json.scores)) {
88
+ const paired = hitsArray.map((h, i) => ({ h, s: json.scores[i] ?? 0 }))
89
+ paired.sort((a, b) => b.s - a.s)
90
+ return paired.map((p) => p.h)
91
+ }
92
+ return hitsArray
93
+ } catch (e) {
94
+ // eslint-disable-next-line no-console
95
+ console.warn('semanticRerank failed', e && e.message ? e.message : e)
96
+ return hitsArray
97
+ }
98
+ }
99
+
18
100
  nuxtApp.provide('searchClient', searchClient)
19
101
  nuxtApp.provide('searchIndexName', indexName)
102
+ nuxtApp.provide('searchHelpers', { suggest, mapAggregations, semanticRerank })
20
103
  })
@@ -0,0 +1,28 @@
1
+ let shared: any = null
2
+ try {
3
+ // Try to use the centralized shared helper at runtime when available
4
+ // Use require to avoid TypeScript resolving the module at compile-time for this layer project
5
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
6
+ shared = require('../../../shared/app/utils/env')
7
+ } catch (e) {
8
+ shared = null
9
+ }
10
+
11
+ export function getEnv(key: string, fallback?: string): string | undefined {
12
+ if (shared && typeof shared.getEnv === 'function') return shared.getEnv(key, fallback)
13
+ if (!key) return fallback
14
+ const direct = process.env[key]
15
+ if (direct !== undefined) return direct
16
+ const publicKey = `NUXT_PUBLIC_${key}`
17
+ if (process.env[publicKey] !== undefined) return process.env[publicKey]
18
+ return fallback
19
+ }
20
+
21
+ export function getEnvBool(key: string, fallback = false): boolean {
22
+ const v = getEnv(key)
23
+ if (v === undefined) return fallback
24
+ const low = String(v).toLowerCase()
25
+ return ['1', 'true', 'yes', 'on'].includes(low)
26
+ }
27
+
28
+ export default { getEnv, getEnvBool }
@@ -2,6 +2,7 @@
2
2
  // This is intentionally minimal — it throws when no configuration is present
3
3
  // so the caller can fall back gracefully.
4
4
  import type { SearchClient } from 'instantsearch.js'
5
+ import { getEnv } from '../env'
5
6
 
6
7
  export function getIndexName(): string {
7
8
  return (
@@ -11,28 +12,42 @@ export function getIndexName(): string {
11
12
  )
12
13
  }
13
14
 
15
+ function buildHostFromParts(): string | null {
16
+ const explicit = getEnv('SEARCHKIT_HOST')
17
+ if (explicit) return explicit
18
+
19
+ // Allow composing host from protocol/hostname/port path
20
+ const protocol = (getEnv('SEARCHKIT_PROTOCOL') || 'http').replace(/:\/\//, '')
21
+ const hostname = getEnv('SEARCHKIT_HOSTNAME')
22
+ const port = getEnv('SEARCHKIT_PORT')
23
+ if (!hostname) return null
24
+ return `${protocol}://${hostname}${port ? `:${port}` : ''}`
25
+ }
26
+
14
27
  export function getSearchClient(): SearchClient {
15
- const host = process.env.NUXT_PUBLIC_SEARCHKIT_HOST || process.env.SEARCHKIT_HOST
28
+ const host = buildHostFromParts()
16
29
  if (!host) {
17
- throw new Error('Searchkit host not configured via SEARCHKIT_HOST or NUXT_PUBLIC_SEARCHKIT_HOST')
30
+ throw new Error('Searchkit host not configured via SEARCHKIT_HOST, SEARCHKIT_HOSTNAME, or NUXT_PUBLIC_SEARCHKIT_HOST')
18
31
  }
19
32
 
33
+ // Optionally pass an API key if configured
34
+ const apiKey = getEnv('SEARCHKIT_API_KEY')
35
+
20
36
  // Defer importing heavy searchkit/instantsearch client until runtime.
21
37
  // Consumers can replace this implementation with a provider-specific client.
22
38
  // Here we attempt to use @searchkit/instantsearch-client if available.
23
39
  // If not present, let the import fail so the plugin can fallback.
24
- // The actual creation API differs between versions; adjust as needed.
25
40
  // eslint-disable-next-line @typescript-eslint/no-var-requires
26
41
  const createClient = require('@searchkit/instantsearch-client')
27
42
  if (!createClient) {
28
43
  throw new Error('@searchkit/instantsearch-client is not installed')
29
44
  }
30
45
 
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.
46
+ // Create a minimal client. The factory API may vary; adapt to your Searchkit config.
34
47
  try {
35
- return createClient({ host })
48
+ const opts: any = { host }
49
+ if (apiKey) opts.apiKey = apiKey
50
+ return createClient(opts)
36
51
  } catch (e: any) {
37
52
  // rethrow with context
38
53
  throw new Error('Failed to create Searchkit client: ' + (e?.message || e))
package/package.json CHANGED
@@ -1,8 +1,7 @@
1
1
  {
2
2
  "name": "@meeovi/layer-search",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "description": "Powerful search module for the Alternate Framework.",
5
- "main": "./nuxt.config.ts",
6
5
  "keywords": [
7
6
  "meeovi",
8
7
  "search",