@meeovi/layer-search 1.0.3

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 (86) hide show
  1. package/README.md +181 -0
  2. package/app/components/README.md +3 -0
  3. package/app/components/atoms/BaseButton.vue +36 -0
  4. package/app/components/atoms/BaseCard.vue +13 -0
  5. package/app/components/atoms/BaseCheckbox.vue +51 -0
  6. package/app/components/atoms/BaseLogo.vue +19 -0
  7. package/app/components/atoms/BaseText.vue +17 -0
  8. package/app/components/atoms/BaseTitle.vue +23 -0
  9. package/app/components/atoms/DiscordIcon.vue +14 -0
  10. package/app/components/atoms/GithubIcon.vue +14 -0
  11. package/app/components/atoms/HalfSolidStarIcon.vue +5 -0
  12. package/app/components/atoms/SelectArrow.vue +5 -0
  13. package/app/components/atoms/SolidStarIcon.vue +5 -0
  14. package/app/components/atoms/StarIcon.vue +5 -0
  15. package/app/components/atoms/TwitterIcon.vue +14 -0
  16. package/app/components/atoms/WebIcon.vue +12 -0
  17. package/app/components/atoms/XIcon.vue +5 -0
  18. package/app/components/features/aiSearch.vue +0 -0
  19. package/app/components/features/allSearch.vue +0 -0
  20. package/app/components/features/autocomplete.vue +0 -0
  21. package/app/components/features/imageSearch.vue +0 -0
  22. package/app/components/features/videoSearch.vue +0 -0
  23. package/app/components/filters/filters.vue +0 -0
  24. package/app/components/molecules/BaseSelect.vue +53 -0
  25. package/app/components/molecules/PageNumber.vue +54 -0
  26. package/app/components/molecules/RangeSlider.vue +37 -0
  27. package/app/components/molecules/SearchInput.vue +32 -0
  28. package/app/components/molecules/SocialLink.vue +42 -0
  29. package/app/components/molecules/StarRating.vue +48 -0
  30. package/app/components/organisms/LoadingIndicator.vue +12 -0
  31. package/app/components/organisms/MeiliSearchBar.vue +15 -0
  32. package/app/components/organisms/MeiliSearchFacetFilter.vue +116 -0
  33. package/app/components/organisms/MeiliSearchLoadingProvider.vue +29 -0
  34. package/app/components/organisms/MeiliSearchPagination.vue +40 -0
  35. package/app/components/organisms/MeiliSearchProvider.vue +51 -0
  36. package/app/components/organisms/MeiliSearchRangeFilter.vue +52 -0
  37. package/app/components/organisms/MeiliSearchRatingFilter.vue +47 -0
  38. package/app/components/organisms/MeiliSearchResults.vue +35 -0
  39. package/app/components/organisms/MeiliSearchSorting.vue +23 -0
  40. package/app/components/organisms/MeiliSearchStats.vue +13 -0
  41. package/app/components/organisms/ProductCard.vue +80 -0
  42. package/app/components/organisms/TheNavbar.vue +71 -0
  43. package/app/components/results/audioSearch.vue +7 -0
  44. package/app/components/results/booksSearch.vue +7 -0
  45. package/app/components/results/financeSearch.vue +93 -0
  46. package/app/components/results/imageSearch.vue +7 -0
  47. package/app/components/results/musicSearch.vue +93 -0
  48. package/app/components/results/newsSearch.vue +93 -0
  49. package/app/components/results/spaceSearch.vue +93 -0
  50. package/app/components/results/spacesSearch.vue +7 -0
  51. package/app/components/results/travelSearch.vue +93 -0
  52. package/app/components/results/videoSearch.vue +7 -0
  53. package/app/components/search.vue +87 -0
  54. package/app/components/templates/HomeTemplate.vue +44 -0
  55. package/app/components/widgets/ClearRefinements.vue +27 -0
  56. package/app/components/widgets/NoResults.vue +125 -0
  57. package/app/components/widgets/PriceSlider.css +58 -0
  58. package/app/composables/adapter/meilisearch.ts +58 -0
  59. package/app/composables/adapter/mock.ts +34 -0
  60. package/app/composables/adapter/opensearch.ts +66 -0
  61. package/app/composables/adapter/types.ts +14 -0
  62. package/app/composables/bridges/instantsearch.ts +20 -0
  63. package/app/composables/bridges/react.ts +40 -0
  64. package/app/composables/bridges/vue.ts +38 -0
  65. package/app/composables/cli.ts +85 -0
  66. package/app/composables/config/schema.ts +16 -0
  67. package/app/composables/config.ts +20 -0
  68. package/app/composables/core/Facets.ts +9 -0
  69. package/app/composables/core/Filters.ts +13 -0
  70. package/app/composables/core/Normalizers.ts +0 -0
  71. package/app/composables/core/Pipeline.ts +20 -0
  72. package/app/composables/core/QueryBuilder.ts +27 -0
  73. package/app/composables/core/SearchContext.ts +54 -0
  74. package/app/composables/core/SearchManager.ts +27 -0
  75. package/app/composables/events.ts +6 -0
  76. package/app/composables/index.ts +9 -0
  77. package/app/composables/module.ts +72 -0
  78. package/app/composables/types/api/global-search.ts +8 -0
  79. package/app/composables/types.d.ts +12 -0
  80. package/app/composables/utils/normalizers.ts +6 -0
  81. package/app/pages/results.vue +85 -0
  82. package/app/plugins/instantsearch.js +35 -0
  83. package/app/plugins/search.js +20 -0
  84. package/nuxt.config.ts +11 -0
  85. package/package.json +43 -0
  86. package/tsconfig.json +14 -0
@@ -0,0 +1,125 @@
1
+ <template>
2
+ <div v-if="state && state.results && state.results.nbHits === 0">
3
+ <div class="hits-empty-state">
4
+ <svg
5
+ xmlns="http://www.w3.org/2000/svg"
6
+ xmlns:xlink="http://www.w3.org/1999/xlink"
7
+ width="138"
8
+ height="138"
9
+ class="hits-empty-state-image"
10
+ >
11
+ <defs>
12
+ <linearGradient id="c" x1="50%" x2="50%" y1="100%" y2="0%">
13
+ <stop offset="0%" stop-color="#F5F5FA" />
14
+ <stop offset="100%" stop-color="#FFF" />
15
+ </linearGradient>
16
+ <path
17
+ id="b"
18
+ d="M68.71 114.25a45.54 45.54 0 1 1 0-91.08 45.54 45.54 0 0 1 0 91.08z"
19
+ />
20
+ <filter
21
+ id="a"
22
+ width="140.6%"
23
+ height="140.6%"
24
+ x="-20.3%"
25
+ y="-15.9%"
26
+ filterUnits="objectBoundingBox"
27
+ >
28
+ <feOffset dy="4" in="SourceAlpha" result="shadowOffsetOuter1" />
29
+ <feGaussianBlur
30
+ in="shadowOffsetOuter1"
31
+ result="shadowBlurOuter1"
32
+ stdDeviation="5.5"
33
+ />
34
+ <feColorMatrix
35
+ in="shadowBlurOuter1"
36
+ result="shadowMatrixOuter1"
37
+ values="0 0 0 0 0.145098039 0 0 0 0 0.17254902 0 0 0 0 0.380392157 0 0 0 0.15 0"
38
+ />
39
+ <feOffset dy="2" in="SourceAlpha" result="shadowOffsetOuter2" />
40
+ <feGaussianBlur
41
+ in="shadowOffsetOuter2"
42
+ result="shadowBlurOuter2"
43
+ stdDeviation="1.5"
44
+ />
45
+ <feColorMatrix
46
+ in="shadowBlurOuter2"
47
+ result="shadowMatrixOuter2"
48
+ values="0 0 0 0 0.364705882 0 0 0 0 0.392156863 0 0 0 0 0.580392157 0 0 0 0.2 0"
49
+ />
50
+ <feMerge>
51
+ <feMergeNode in="shadowMatrixOuter1" />
52
+ <feMergeNode in="shadowMatrixOuter2" />
53
+ </feMerge>
54
+ </filter>
55
+ </defs>
56
+ <g fill="none" fill-rule="evenodd">
57
+ <circle
58
+ cx="68.85"
59
+ cy="68.85"
60
+ r="68.85"
61
+ fill="#5468FF"
62
+ opacity=".07"
63
+ />
64
+ <circle
65
+ cx="68.85"
66
+ cy="68.85"
67
+ r="52.95"
68
+ fill="#5468FF"
69
+ opacity=".08"
70
+ />
71
+ <use fill="#000" filter="url(#a)" xlink:href="#b" />
72
+ <use fill="url(#c)" xlink:href="#b" />
73
+ <path
74
+ d="M76.01 75.44c5-5 5.03-13.06.07-18.01a12.73 12.73 0 0 0-18 .07c-5 4.99-5.03 13.05-.07 18a12.73 12.73 0 0 0 18-.06zm2.5 2.5a16.28 16.28 0 0 1-23.02.09A16.29 16.29 0 0 1 55.57 55a16.28 16.28 0 0 1 23.03-.1 16.28 16.28 0 0 1-.08 23.04zm1.08-1.08l-2.15 2.16 8.6 8.6 2.16-2.15-8.6-8.6z"
75
+ fill="#5369FF"
76
+ />
77
+ </g>
78
+ </svg>
79
+
80
+ <p class="hits-empty-state-title">
81
+ Sorry, we can&apos;t find any matches to your query!
82
+ </p>
83
+ <p class="hits-empty-state-description">
84
+ {{
85
+ state.results.getRefinements().length > 0
86
+ ? 'Try to reset your applied filters.'
87
+ : 'Please try another query.'
88
+ }}
89
+ </p>
90
+
91
+ <NuxtLinkis-clear-refinements>
92
+ <template #resetLabel>
93
+ <div class="clear-filters">
94
+ <svg
95
+ xmlns="http://www.w3.org/2000/svg"
96
+ width="11"
97
+ height="11"
98
+ viewBox="0 0 11 11"
99
+ >
100
+ <g fill="none" fill-rule="evenodd">
101
+ <path d="M0 0h11v11H0z" />
102
+ <path
103
+ fill="#000"
104
+ fill-rule="nonzero"
105
+ d="M8.26 2.75a3.896 3.896 0 1 0 1.102 3.262l.007-.056a.49.49 0 0 1 .485-.456c.253 0 .451.206.437.457 0 0 .012-.109-.006.061a4.813 4.813 0 1 1-1.348-3.887v-.987a.458.458 0 1 1 .917.002v2.062a.459.459 0 0 1-.459.459H7.334a.458.458 0 1 1-.002-.917h.928z"
106
+ />
107
+ </g>
108
+ </svg>
109
+ Clear filters
110
+ </div>
111
+ </template>
112
+ </ais-clear-refinements>
113
+ </div>
114
+ </div>
115
+ </template>
116
+
117
+ <script>
118
+ import { connectHits } from 'instantsearch.js/es/connectors';
119
+ import { createWidgetMixin } from 'vue-instantsearch';
120
+
121
+ export default {
122
+ name: 'AppNoResults',
123
+ mixins: [createWidgetMixin({ connector: connectHits })],
124
+ };
125
+ </script>
@@ -0,0 +1,58 @@
1
+ .vue-slider {
2
+ display: flex;
3
+ justify-content: center;
4
+ margin-top: 1.5rem;
5
+ }
6
+
7
+ .vue-slider-rail {
8
+ background-color: rgba(65, 66, 71, 0.08);
9
+ border-radius: 3px;
10
+ cursor: pointer;
11
+ height: 4px;
12
+ width: calc(100% - 10px);
13
+ }
14
+
15
+ .vue-slider-process {
16
+ background-color: #e2a400;
17
+ border-radius: 3px;
18
+ }
19
+
20
+ .vue-slider-dot-tooltip {
21
+ cursor: grab;
22
+ display: flex;
23
+ font-size: 0.75rem;
24
+ font-weight: bold;
25
+ margin-top: 5px;
26
+ }
27
+
28
+ .vue-slider-dot-tooltip::before {
29
+ color: #e2a400;
30
+ content: '$';
31
+ font-size: 0.6;
32
+ margin-right: 4px;
33
+ }
34
+
35
+ .vue-slider-dot-tooltip-text .vue-slider-dot-tooltip-inner {
36
+ cursor: -webkit-grabbing;
37
+ cursor: -moz-grabbing;
38
+ text-align: center;
39
+ white-space: nowrap;
40
+ }
41
+
42
+ .vue-slider-dot-handle {
43
+ background-image: linear-gradient(to top, #f5f5fa, #fff);
44
+ border-radius: 50%;
45
+ box-shadow: 0 4px 11px 0 rgba(37, 44, 97, 0.15),
46
+ 0 2px 3px 0 rgba(93, 100, 148, 0.2);
47
+ cursor: -webkit-grabbing;
48
+ cursor: -moz-grabbing;
49
+ height: 100%;
50
+ width: 100%;
51
+ }
52
+
53
+ @media (max-width: 899px) {
54
+ .vue-slider-dot {
55
+ height: 24px !important;
56
+ width: 24px !important;
57
+ }
58
+ }
@@ -0,0 +1,58 @@
1
+ import {
2
+ defineAlternateAdapter,
3
+ type SearchAdapter,
4
+ type SearchAdapterConfig,
5
+ type SearchResult
6
+ } from '@meeovi/core'
7
+
8
+ import type { MeeoviSearchItem } from './types'
9
+ import type { BuiltSearchQuery } from '../core/QueryBuilder'
10
+
11
+ interface MeiliConfig extends SearchAdapterConfig {
12
+ host: string
13
+ apiKey?: string
14
+ index: string
15
+ }
16
+
17
+ export function createMeilisearchAdapter(config: MeiliConfig) {
18
+ const headers: Record<string, string> = {
19
+ 'Content-Type': 'application/json'
20
+ }
21
+
22
+ if (config.apiKey) {
23
+ headers['Authorization'] = `Bearer ${config.apiKey}`
24
+ }
25
+
26
+ const adapter: SearchAdapter<MeeoviSearchItem> = {
27
+ id: 'search:meilisearch',
28
+ type: 'search',
29
+ config,
30
+
31
+ async search(query: BuiltSearchQuery): Promise<SearchResult<MeeoviSearchItem>> {
32
+ const params = new URLSearchParams({
33
+ q: query.term,
34
+ offset: String((query.page - 1) * query.pageSize),
35
+ limit: String(query.pageSize)
36
+ })
37
+
38
+ const res = await fetch(`${config.host}/indexes/${config.index}/search?${params}`, {
39
+ method: 'POST',
40
+ headers,
41
+ body: JSON.stringify({
42
+ filter: Object.entries(query.filters).map(([field, value]) => `${field} = ${value}`)
43
+ })
44
+ })
45
+
46
+ const json = await res.json()
47
+
48
+ return {
49
+ items: json.hits as MeeoviSearchItem[],
50
+ total: json.estimatedTotalHits ?? json.hits.length,
51
+ page: query.page,
52
+ pageSize: query.pageSize
53
+ }
54
+ }
55
+ }
56
+
57
+ return defineAlternateAdapter(adapter)
58
+ }
@@ -0,0 +1,34 @@
1
+ import {
2
+ defineAlternateAdapter,
3
+ type SearchAdapter,
4
+ type SearchAdapterConfig,
5
+ type SearchResult
6
+ } from '@meeovi/core'
7
+
8
+ import type { MeeoviSearchItem } from './types'
9
+ import type { BuiltSearchQuery } from '../core/QueryBuilder'
10
+
11
+ export function createMockSearchAdapter(items: MeeoviSearchItem[] = []) {
12
+ const cfg: SearchAdapterConfig = { provider: 'mock' }
13
+
14
+ const adapter: SearchAdapter<MeeoviSearchItem> = {
15
+ id: 'search:mock',
16
+ type: 'search',
17
+ config: cfg,
18
+
19
+ async search(query: BuiltSearchQuery): Promise<SearchResult<MeeoviSearchItem>> {
20
+ const filtered = items.filter((item) =>
21
+ item.title.toLowerCase().includes(query.term.toLowerCase())
22
+ )
23
+
24
+ return {
25
+ items: filtered,
26
+ total: filtered.length,
27
+ page: 1,
28
+ pageSize: filtered.length
29
+ }
30
+ }
31
+ }
32
+
33
+ return defineAlternateAdapter(adapter)
34
+ }
@@ -0,0 +1,66 @@
1
+ import {
2
+ defineAlternateAdapter,
3
+ type SearchAdapter,
4
+ type SearchAdapterConfig,
5
+ type SearchResult
6
+ } from '@meeovi/core'
7
+
8
+ import type { MeeoviSearchItem } from './types'
9
+ import type { BuiltSearchQuery } from '../core/QueryBuilder'
10
+ import { normalizeOpenSearchHit } from '../utils/normalizers'
11
+
12
+ interface OpenSearchConfig extends SearchAdapterConfig {
13
+ endpoint: string
14
+ index: string
15
+ apiKey?: string
16
+ }
17
+
18
+ export function createOpenSearchAdapter(config: OpenSearchConfig) {
19
+ const adapter: SearchAdapter<MeeoviSearchItem> = {
20
+ id: 'search:opensearch',
21
+ type: 'search',
22
+ config,
23
+
24
+ async search(query: BuiltSearchQuery): Promise<SearchResult<MeeoviSearchItem>> {
25
+ const body: any = {
26
+ query: {
27
+ bool: {
28
+ must: [
29
+ {
30
+ multi_match: {
31
+ query: query.term,
32
+ fields: ['title^3', 'description', 'tags']
33
+ }
34
+ }
35
+ ],
36
+ filter: Object.entries(query.filters).map(([field, value]) => ({
37
+ term: { [field]: value }
38
+ }))
39
+ }
40
+ },
41
+ from: (query.page - 1) * query.pageSize,
42
+ size: query.pageSize
43
+ }
44
+
45
+ const res = await fetch(`${config.endpoint}/${config.index}/_search`, {
46
+ method: 'POST',
47
+ headers: {
48
+ 'Content-Type': 'application/json',
49
+ ...(config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : {})
50
+ },
51
+ body: JSON.stringify(body)
52
+ })
53
+
54
+ const json = await res.json()
55
+
56
+ return {
57
+ items: json.hits.hits.map(normalizeOpenSearchHit),
58
+ total: json.hits.total.value,
59
+ page: query.page,
60
+ pageSize: query.pageSize
61
+ }
62
+ }
63
+ }
64
+
65
+ return defineAlternateAdapter(adapter)
66
+ }
@@ -0,0 +1,14 @@
1
+ import type { SearchAdapter, SearchResult } from '@meeovi/core'
2
+ import type { BuiltSearchQuery } from '../core/QueryBuilder'
3
+
4
+ export interface MeeoviSearchItem {
5
+ id: string
6
+ title: string
7
+ description?: string
8
+ price?: number
9
+ [key: string]: unknown
10
+ }
11
+
12
+ export type MeeoviSearchAdapter = SearchAdapter<MeeoviSearchItem> & {
13
+ search(query: BuiltSearchQuery): Promise<SearchResult<MeeoviSearchItem>>
14
+ }
@@ -0,0 +1,20 @@
1
+ import type { SearchManager } from '../core/SearchManager'
2
+
3
+ export function createInstantSearchBridge(manager: SearchManager) {
4
+ return {
5
+ searchFunction(helper: any) {
6
+ manager.context.setQuery(helper.state.query || '')
7
+ manager.context.setPage(helper.state.page || 1)
8
+ // map filters if needed from helper.state
9
+
10
+ return manager.search().then((result) => {
11
+ helper.setResults({
12
+ hits: result.items,
13
+ nbHits: result.total,
14
+ page: result.page - 1,
15
+ hitsPerPage: result.pageSize
16
+ })
17
+ })
18
+ }
19
+ }
20
+ }
@@ -0,0 +1,40 @@
1
+ import { useState, useCallback } from 'react'
2
+ import { useAlternateContext } from '@meeovi/core'
3
+ import type { SearchManager } from '../core/SearchManager'
4
+
5
+ export function useReactSearch() {
6
+ const ctx = useAlternateContext() as any
7
+ const manager = ctx.searchManager as SearchManager
8
+
9
+ const [query, setQuery] = useState(manager.context.state.query)
10
+ const [page, setPage] = useState(manager.context.state.page)
11
+ const [pageSize, setPageSize] = useState(manager.context.state.pageSize)
12
+ const [results, setResults] = useState<any[]>([])
13
+ const [total, setTotal] = useState(0)
14
+ const [loading, setLoading] = useState(false)
15
+
16
+ const search = useCallback(async () => {
17
+ setLoading(true)
18
+ manager.context.setQuery(query)
19
+ manager.context.setPage(page)
20
+ manager.context.setPageSize(pageSize)
21
+
22
+ const res = await manager.search()
23
+ setResults(res.items)
24
+ setTotal(res.total)
25
+ setLoading(false)
26
+ }, [query, page, pageSize, manager])
27
+
28
+ return {
29
+ query,
30
+ setQuery,
31
+ page,
32
+ setPage,
33
+ pageSize,
34
+ setPageSize,
35
+ results,
36
+ total,
37
+ loading,
38
+ search
39
+ }
40
+ }
@@ -0,0 +1,38 @@
1
+ import { ref, computed } from 'vue'
2
+ import { useAlternateContext } from '@meeovi/core'
3
+ import type { SearchManager } from '../core/SearchManager'
4
+
5
+ export function useSearch() {
6
+ const ctx = useAlternateContext() as any
7
+ const manager = ctx.searchManager as SearchManager
8
+
9
+ const query = ref(manager.context.state.query)
10
+ const page = ref(manager.context.state.page)
11
+ const pageSize = ref(manager.context.state.pageSize)
12
+
13
+ const results = ref<any[]>([])
14
+ const total = ref(0)
15
+ const loading = ref(false)
16
+
17
+ async function run() {
18
+ loading.value = true
19
+ manager.context.setQuery(query.value)
20
+ manager.context.setPage(page.value)
21
+ manager.context.setPageSize(pageSize.value)
22
+
23
+ const res = await manager.search()
24
+ results.value = res.items
25
+ total.value = res.total
26
+ loading.value = false
27
+ }
28
+
29
+ return {
30
+ query,
31
+ page,
32
+ pageSize,
33
+ results: computed(() => results.value),
34
+ total: computed(() => total.value),
35
+ loading: computed(() => loading.value),
36
+ search: run
37
+ }
38
+ }
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'node:fs'
4
+ import path from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
6
+ import { createAlternateApp } from '@meeovi/core'
7
+ import searchModule from './module'
8
+
9
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
10
+
11
+ async function run() {
12
+ const [,, command, arg] = process.argv
13
+
14
+ const defaultProvider = process.env.SEARCH_PROVIDER === 'meilisearch' ? 'meilisearch' : 'opensearch'
15
+
16
+ const app = createAlternateApp({
17
+ config: {
18
+ env: 'production',
19
+ search: {
20
+ defaultProvider,
21
+ providers: {
22
+ opensearch: {
23
+ endpoint: process.env.OPENSEARCH_ENDPOINT,
24
+ index: process.env.OPENSEARCH_INDEX
25
+ },
26
+ meilisearch: {
27
+ host: process.env.MEILI_HOST,
28
+ index: process.env.MEILI_INDEX,
29
+ apiKey: process.env.MEILI_KEY
30
+ }
31
+ }
32
+ }
33
+ },
34
+ modules: [searchModule]
35
+ })
36
+
37
+ const ctx = await app.start()
38
+ const search = ctx.getAdapter('search')
39
+
40
+ if (!search) {
41
+ console.error('No search adapter registered')
42
+ process.exit(1)
43
+ }
44
+
45
+ if (command === 'warmup') {
46
+ console.log('Warming up search provider...')
47
+ await search.search({ term: 'warmup' })
48
+ console.log('Warmup complete')
49
+ return
50
+ }
51
+
52
+ if (command === 'index') {
53
+ if (!arg) {
54
+ console.error('Missing file path: meeovi-search index <file.json>')
55
+ process.exit(1)
56
+ }
57
+
58
+ const filePath = path.resolve(process.cwd(), arg)
59
+ const data = JSON.parse(fs.readFileSync(filePath, 'utf8'))
60
+
61
+ console.log(`Indexing ${data.length} items...`)
62
+
63
+ if (search.id === 'search:opensearch') {
64
+ // TODO: implement bulk indexing
65
+ }
66
+
67
+ if (search.id === 'search:meilisearch') {
68
+ await fetch(`${process.env.MEILI_HOST}/indexes/${process.env.MEILI_INDEX}/documents`, {
69
+ method: 'POST',
70
+ headers: {
71
+ 'Content-Type': 'application/json',
72
+ Authorization: `Bearer ${process.env.MEILI_KEY}`
73
+ },
74
+ body: JSON.stringify(data)
75
+ })
76
+ }
77
+
78
+ console.log('Indexing complete')
79
+ return
80
+ }
81
+
82
+ console.log(`Unknown command: ${command}`)
83
+ }
84
+
85
+ run()
@@ -0,0 +1,16 @@
1
+ export interface SearchModuleConfig {
2
+ defaultProvider: 'opensearch' | 'meilisearch'
3
+ providers: Record<string, unknown>
4
+ }
5
+
6
+ export function validateSearchConfig(config: SearchModuleConfig) {
7
+ if (!config.defaultProvider) {
8
+ throw new Error('[@meeovi/search] Missing defaultProvider')
9
+ }
10
+
11
+ if (!config.providers[config.defaultProvider]) {
12
+ throw new Error(
13
+ `[@meeovi/search] Provider "${config.defaultProvider}" not found in config.providers`
14
+ )
15
+ }
16
+ }
@@ -0,0 +1,20 @@
1
+
2
+ export interface SearchConfig {
3
+ searchProvider: string
4
+ searchUrl?: string
5
+ apiKey?: string
6
+ }
7
+
8
+ let config: SearchConfig = {
9
+ searchProvider: 'searchkit',
10
+ searchUrl: '',
11
+ apiKey: ''
12
+ }
13
+
14
+ export function setSearchConfig(newConfig: Partial<SearchConfig>) {
15
+ config = { ...config, ...newConfig }
16
+ }
17
+
18
+ export function getSearchConfig(): SearchConfig {
19
+ return config
20
+ }
@@ -0,0 +1,9 @@
1
+ export const Facets = {
2
+ terms(field: string) {
3
+ return { type: 'terms', field }
4
+ },
5
+
6
+ range(field: string, ranges: { from?: number; to?: number }[]) {
7
+ return { type: 'range', field, ranges }
8
+ }
9
+ }
@@ -0,0 +1,13 @@
1
+ export const Filters = {
2
+ term(field: string, value: string) {
3
+ return { type: 'term', field, value }
4
+ },
5
+
6
+ range(field: string, min?: number, max?: number) {
7
+ return { type: 'range', field, min, max }
8
+ },
9
+
10
+ boolean(field: string, value: boolean) {
11
+ return { type: 'boolean', field, value }
12
+ }
13
+ }
File without changes
@@ -0,0 +1,20 @@
1
+ export class SearchPipeline {
2
+ private before: ((q: any) => any)[] = []
3
+ private after: ((r: any) => any)[] = []
4
+
5
+ useBefore(fn: (query: any) => any) {
6
+ this.before.push(fn)
7
+ }
8
+
9
+ useAfter(fn: (result: any) => any) {
10
+ this.after.push(fn)
11
+ }
12
+
13
+ runBefore(query: any) {
14
+ return this.before.reduce((q, fn) => fn(q), query)
15
+ }
16
+
17
+ runAfter(result: any) {
18
+ return this.after.reduce((r, fn) => fn(r), result)
19
+ }
20
+ }
@@ -0,0 +1,27 @@
1
+ import { SearchContext } from './SearchContext'
2
+
3
+ export interface BuiltSearchQuery {
4
+ term: string
5
+ page: number
6
+ pageSize: number
7
+ sort?: string
8
+ filters: Record<string, any>
9
+ }
10
+
11
+ export class QueryBuilder {
12
+ private context: SearchContext
13
+
14
+ constructor(context: SearchContext) {
15
+ this.context = context
16
+ }
17
+
18
+ build(): BuiltSearchQuery {
19
+ return {
20
+ term: this.context.state.query,
21
+ page: this.context.state.page,
22
+ pageSize: this.context.state.pageSize,
23
+ sort: this.context.state.sort,
24
+ filters: this.context.state.filters
25
+ }
26
+ }
27
+ }