@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<
|
|
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
|
+
})
|