@meeovi/layer-search 1.1.3 → 1.1.4

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.
@@ -0,0 +1,40 @@
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
+ emit('input', v)
25
+ }
26
+
27
+ function onEnter() {
28
+ emit('search')
29
+ }
30
+
31
+ function onSearch() {
32
+ emit('search')
33
+ }
34
+ </script>
35
+
36
+ <style scoped>
37
+ .search-input { display:flex; gap:8px; align-items:center }
38
+ .search-input-field { padding:8px; border:1px solid #ccc; }
39
+ .search-input-button { padding:8px 12px }
40
+ </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>
@@ -1,4 +1,5 @@
1
1
  import { ref, computed } from 'vue'
2
+ export type SearchHit = Record<string, any>
2
3
  import { useNuxtApp } from 'nuxt/app'
3
4
 
4
5
  export function useSearchkit() {
@@ -8,7 +9,7 @@ export function useSearchkit() {
8
9
  const helpers: any = nuxt.$searchHelpers || {}
9
10
 
10
11
  const query = ref('')
11
- const hits = ref<any[]>([])
12
+ const hits = ref<SearchHit[]>([])
12
13
  const total = ref(0)
13
14
  const loading = ref(false)
14
15
  const page = ref(1)
@@ -0,0 +1,108 @@
1
+ import { defineNuxtPlugin, useRuntimeConfig } from 'nuxt/app'
2
+
3
+ export default defineNuxtPlugin(() => {
4
+ const config = useRuntimeConfig() as any
5
+ const endpoint: string | undefined = config.search?.endpoint || process.env.SEARCH_ENDPOINT
6
+ const indexName: string = config.search?.indexName || process.env.SEARCH_INDEX_NAME || 'default'
7
+
8
+ // Simple helpers that consumers may rely on
9
+ const helpers = {
10
+ mapAggregations(aggs: any) {
11
+ return aggs || {}
12
+ },
13
+ async semanticRerank(hits: any[], _query: string) {
14
+ // no-op rerank: return input
15
+ return hits
16
+ },
17
+ }
18
+
19
+ // HTTP-backed ES-compatible client when endpoint is configured
20
+ const httpClient = endpoint
21
+ ? {
22
+ async search(requests: Array<any>) {
23
+ const base = endpoint.replace(/\/+$/, '')
24
+ return Promise.all(
25
+ requests.map(async (req: any) => {
26
+ const idx = req.indexName || indexName
27
+ // Build a basic ES query body from params
28
+ let body: any = {}
29
+
30
+ // If params contains q/size/from, map to simple query_string
31
+ if (req.params) {
32
+ const p = req.params
33
+ const q = (p.q || (p.params && p.params.q)) || '*'
34
+ const size = p.size || (p.params && p.params.size) || 10
35
+ const from = p.from || 0
36
+ if (!req.body) body = {}
37
+ if (q && q !== '*') {
38
+ body.query = { query_string: { query: q } }
39
+ } else if (!req.body) {
40
+ body.query = { match_all: {} }
41
+ }
42
+ body.size = size
43
+ body.from = from
44
+ }
45
+
46
+ if (req.body) {
47
+ body = { ...body, ...req.body }
48
+ }
49
+
50
+ // fetch with retries and exponential backoff
51
+ async function fetchWithRetry(url: string, init: RequestInit, retries = 2, backoff = 200): Promise<any> {
52
+ try {
53
+ const res = await fetch(url, init)
54
+ if (!res.ok) {
55
+ const text = await res.text().catch(() => '')
56
+ if (retries > 0) {
57
+ await new Promise((r) => setTimeout(r, backoff))
58
+ return fetchWithRetry(url, init, retries - 1, backoff * 2)
59
+ }
60
+ return { hits: [], error: `HTTP ${res.status}: ${text}` }
61
+ }
62
+ return await res.json()
63
+ } catch (err) {
64
+ if (retries > 0) {
65
+ await new Promise((r) => setTimeout(r, backoff))
66
+ return fetchWithRetry(url, init, retries - 1, backoff * 2)
67
+ }
68
+ return { hits: [], error: String(err) }
69
+ }
70
+ }
71
+
72
+ const resJson = await fetchWithRetry(`${base}/${encodeURIComponent(idx)}/_search`, {
73
+ method: 'POST',
74
+ headers: { 'content-type': 'application/json' },
75
+ body: JSON.stringify(body),
76
+ })
77
+ // Ensure a consistent shape on error
78
+ if (!resJson) return { hits: [] }
79
+ return resJson
80
+ })
81
+ )
82
+ },
83
+ }
84
+ : null
85
+
86
+ // In-memory fallback client for environments without an endpoint
87
+ const stubClient = {
88
+ async search(requests: Array<any>) {
89
+ return requests.map((req: any) => {
90
+ const q = (req.params && (req.params.q || (req.params.params && req.params.params.q))) || '*'
91
+ const hits = [
92
+ { _source: { title: q && q !== '*' ? `Result for ${q}` : 'Sample result', description: 'Stubbed result' } },
93
+ ]
94
+ return { hits: { hits }, aggregations: {}, nbHits: hits.length }
95
+ })
96
+ },
97
+ }
98
+
99
+ const client = httpClient || stubClient
100
+
101
+ return {
102
+ provide: {
103
+ $searchClient: client,
104
+ $searchIndexName: indexName,
105
+ $searchHelpers: helpers,
106
+ },
107
+ }
108
+ })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meeovi/layer-search",
3
- "version": "1.1.3",
3
+ "version": "1.1.4",
4
4
  "description": "Powerful search module for the Alternate Framework.",
5
5
  "keywords": [
6
6
  "meeovi",