@meeovi/layer-search 1.0.6 → 1.0.7
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.
- package/README.md +39 -5
- package/app/composables/bridges/searchkit-server.ts +51 -0
- package/app/composables/bridges/searchkit.ts +88 -0
- package/app/composables/index.ts +4 -1
- package/app/composables/module.ts +41 -0
- package/app/composables/utils/health.ts +13 -0
- package/app/plugins/search.js +1 -1
- package/app/utils/search/client.ts +40 -0
- package/dist/app/composables/adapter/meilisearch.d.ts +8 -0
- package/dist/app/composables/adapter/meilisearch.js +36 -0
- package/dist/app/composables/adapter/mock.d.ts +3 -0
- package/dist/app/composables/adapter/mock.js +19 -0
- package/dist/app/composables/adapter/opensearch.d.ts +8 -0
- package/dist/app/composables/adapter/opensearch.js +46 -0
- package/dist/app/composables/adapter/types.d.ts +12 -0
- package/dist/app/composables/adapter/types.js +1 -0
- package/dist/app/composables/bridges/instantsearch.d.ts +4 -0
- package/dist/app/composables/bridges/instantsearch.js +17 -0
- package/dist/app/composables/bridges/react.d.ts +12 -0
- package/dist/app/composables/bridges/react.js +34 -0
- package/dist/app/composables/bridges/vue.d.ts +9 -0
- package/dist/app/composables/bridges/vue.js +31 -0
- package/dist/app/composables/cli.d.ts +2 -0
- package/dist/app/composables/cli.js +69 -0
- package/dist/app/composables/config/schema.d.ts +5 -0
- package/dist/app/composables/config/schema.js +8 -0
- package/dist/app/composables/config.d.ts +7 -0
- package/dist/app/composables/config.js +11 -0
- package/dist/app/composables/core/Facets.d.ts +17 -0
- package/dist/app/composables/core/Facets.js +8 -0
- package/dist/app/composables/core/Filters.d.ts +18 -0
- package/dist/app/composables/core/Filters.js +11 -0
- package/dist/app/composables/core/Normalizers.d.ts +0 -0
- package/dist/app/composables/core/Normalizers.js +1 -0
- package/dist/app/composables/core/Pipeline.d.ts +8 -0
- package/dist/app/composables/core/Pipeline.js +16 -0
- package/dist/app/composables/core/QueryBuilder.d.ts +13 -0
- package/dist/app/composables/core/QueryBuilder.js +15 -0
- package/dist/app/composables/core/SearchContext.d.ts +18 -0
- package/dist/app/composables/core/SearchContext.js +38 -0
- package/dist/app/composables/core/SearchManager.d.ts +10 -0
- package/dist/app/composables/core/SearchManager.js +20 -0
- package/dist/app/composables/events.d.ts +11 -0
- package/dist/app/composables/events.js +1 -0
- package/dist/app/composables/index.d.ts +9 -0
- package/dist/app/composables/index.js +9 -0
- package/dist/app/composables/module.d.ts +17 -0
- package/dist/app/composables/module.js +39 -0
- package/dist/app/composables/types/api/global-search.d.ts +8 -0
- package/dist/app/composables/types/api/global-search.js +1 -0
- package/dist/app/composables/utils/normalizers.d.ts +1 -0
- package/dist/app/composables/utils/normalizers.js +6 -0
- package/dist/app/utils/search/client.d.ts +3 -0
- package/dist/app/utils/search/client.js +31 -0
- package/dist/layers/search/app/composables/adapter/meilisearch.d.ts +8 -0
- package/dist/layers/search/app/composables/adapter/meilisearch.js +36 -0
- package/dist/layers/search/app/composables/adapter/mock.d.ts +3 -0
- package/dist/layers/search/app/composables/adapter/mock.js +19 -0
- package/dist/layers/search/app/composables/adapter/opensearch.d.ts +8 -0
- package/dist/layers/search/app/composables/adapter/opensearch.js +46 -0
- package/dist/layers/search/app/composables/adapter/types.d.ts +12 -0
- package/dist/layers/search/app/composables/adapter/types.js +1 -0
- package/dist/layers/search/app/composables/bridges/instantsearch.d.ts +4 -0
- package/dist/layers/search/app/composables/bridges/instantsearch.js +17 -0
- package/dist/layers/search/app/composables/bridges/react.d.ts +12 -0
- package/dist/layers/search/app/composables/bridges/react.js +34 -0
- package/dist/layers/search/app/composables/bridges/searchkit-server.d.ts +3 -0
- package/dist/layers/search/app/composables/bridges/searchkit-server.js +44 -0
- package/dist/layers/search/app/composables/bridges/searchkit.d.ts +21 -0
- package/dist/layers/search/app/composables/bridges/searchkit.js +60 -0
- package/dist/layers/search/app/composables/bridges/vue.d.ts +9 -0
- package/dist/layers/search/app/composables/bridges/vue.js +31 -0
- package/dist/layers/search/app/composables/cli.d.ts +2 -0
- package/dist/layers/search/app/composables/cli.js +69 -0
- package/dist/layers/search/app/composables/config/schema.d.ts +5 -0
- package/dist/layers/search/app/composables/config/schema.js +8 -0
- package/dist/layers/search/app/composables/config.d.ts +7 -0
- package/dist/layers/search/app/composables/config.js +11 -0
- package/dist/layers/search/app/composables/core/Facets.d.ts +17 -0
- package/dist/layers/search/app/composables/core/Facets.js +8 -0
- package/dist/layers/search/app/composables/core/Filters.d.ts +18 -0
- package/dist/layers/search/app/composables/core/Filters.js +11 -0
- package/dist/layers/search/app/composables/core/Normalizers.d.ts +0 -0
- package/dist/layers/search/app/composables/core/Normalizers.js +1 -0
- package/dist/layers/search/app/composables/core/Pipeline.d.ts +8 -0
- package/dist/layers/search/app/composables/core/Pipeline.js +16 -0
- package/dist/layers/search/app/composables/core/QueryBuilder.d.ts +13 -0
- package/dist/layers/search/app/composables/core/QueryBuilder.js +15 -0
- package/dist/layers/search/app/composables/core/SearchContext.d.ts +18 -0
- package/dist/layers/search/app/composables/core/SearchContext.js +38 -0
- package/dist/layers/search/app/composables/core/SearchManager.d.ts +10 -0
- package/dist/layers/search/app/composables/core/SearchManager.js +20 -0
- package/dist/layers/search/app/composables/events.d.ts +11 -0
- package/dist/layers/search/app/composables/events.js +1 -0
- package/dist/layers/search/app/composables/index.d.ts +12 -0
- package/dist/layers/search/app/composables/index.js +12 -0
- package/dist/layers/search/app/composables/module.d.ts +17 -0
- package/dist/layers/search/app/composables/module.js +73 -0
- package/dist/layers/search/app/composables/types/api/global-search.d.ts +8 -0
- package/dist/layers/search/app/composables/types/api/global-search.js +1 -0
- package/dist/layers/search/app/composables/utils/health.d.ts +11 -0
- package/dist/layers/search/app/composables/utils/health.js +11 -0
- package/dist/layers/search/app/composables/utils/normalizers.d.ts +1 -0
- package/dist/layers/search/app/composables/utils/normalizers.js +6 -0
- package/dist/layers/search/app/utils/search/client.d.ts +3 -0
- package/dist/layers/search/app/utils/search/client.js +31 -0
- package/dist/layers/search/nuxt.config.d.ts +2 -0
- package/dist/layers/search/nuxt.config.js +7 -0
- package/dist/layers/search/test/runtime-adapter.spec.d.ts +1 -0
- package/dist/layers/search/test/runtime-adapter.spec.js +49 -0
- package/dist/nuxt.config.d.ts +2 -0
- package/dist/nuxt.config.js +7 -0
- package/dist/packages/core/src/adapters/auth.d.ts +9 -0
- package/dist/packages/core/src/adapters/auth.js +1 -0
- package/dist/packages/core/src/adapters/cart.d.ts +22 -0
- package/dist/packages/core/src/adapters/cart.js +1 -0
- package/dist/packages/core/src/adapters/catalog.d.ts +17 -0
- package/dist/packages/core/src/adapters/catalog.js +1 -0
- package/dist/packages/core/src/adapters/common.d.ts +9 -0
- package/dist/packages/core/src/adapters/common.js +1 -0
- package/dist/packages/core/src/adapters/lists.d.ts +17 -0
- package/dist/packages/core/src/adapters/lists.js +1 -0
- package/dist/packages/core/src/adapters/search.d.ts +21 -0
- package/dist/packages/core/src/adapters/search.js +1 -0
- package/dist/packages/core/src/plugins/defineAdapter.d.ts +2 -0
- package/dist/packages/core/src/plugins/defineAdapter.js +3 -0
- package/dist/packages/core/src/plugins/defineModule.d.ts +2 -0
- package/dist/packages/core/src/plugins/defineModule.js +3 -0
- package/dist/packages/core/src/plugins/registry.d.ts +14 -0
- package/dist/packages/core/src/plugins/registry.js +46 -0
- package/dist/packages/core/src/runtime/app.d.ts +2 -0
- package/dist/packages/core/src/runtime/app.js +20 -0
- package/dist/packages/core/src/runtime/context.d.ts +9 -0
- package/dist/packages/core/src/runtime/context.js +11 -0
- package/dist/packages/core/src/runtime/hooks.d.ts +5 -0
- package/dist/packages/core/src/runtime/hooks.js +18 -0
- package/dist/packages/core/src/runtime/lifecycle.d.ts +4 -0
- package/dist/packages/core/src/runtime/lifecycle.js +5 -0
- package/dist/packages/core/src/types/adapters.d.ts +14 -0
- package/dist/packages/core/src/types/adapters.js +1 -0
- package/dist/packages/core/src/types/app.d.ts +13 -0
- package/dist/packages/core/src/types/app.js +1 -0
- package/dist/packages/core/src/types/config.d.ts +8 -0
- package/dist/packages/core/src/types/config.js +1 -0
- package/dist/packages/core/src/types/events.d.ts +20 -0
- package/dist/packages/core/src/types/events.js +22 -0
- package/dist/packages/core/src/types/module.d.ts +15 -0
- package/dist/packages/core/src/types/module.js +1 -0
- package/package.json +9 -4
- package/test/runtime-adapter.spec.ts +61 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { graphql, buildSchema } from 'graphql';
|
|
2
|
+
const schema = buildSchema(`
|
|
3
|
+
type Hit { id: ID, title: String, description: String, price: Float }
|
|
4
|
+
type SearchResult {
|
|
5
|
+
items: [Hit]
|
|
6
|
+
total: Int
|
|
7
|
+
page: Int
|
|
8
|
+
pageSize: Int
|
|
9
|
+
}
|
|
10
|
+
type Query {
|
|
11
|
+
search(term: String, page: Int, pageSize: Int, filters: String): SearchResult
|
|
12
|
+
}
|
|
13
|
+
`);
|
|
14
|
+
function parseFilters(filters) {
|
|
15
|
+
if (!filters)
|
|
16
|
+
return {};
|
|
17
|
+
const entries = String(filters).split(' AND ').map((s) => s.split(':'));
|
|
18
|
+
return Object.fromEntries(entries.map(([k, v]) => [k, v?.replace(/^"|"$/g, '')]));
|
|
19
|
+
}
|
|
20
|
+
export function createSearchkitGraphQLHandler(manager) {
|
|
21
|
+
const root = {
|
|
22
|
+
search: async ({ term, page, pageSize, filters }) => {
|
|
23
|
+
manager.context.setQuery(term || '');
|
|
24
|
+
manager.context.setPage(page || 1);
|
|
25
|
+
manager.context.setPageSize(pageSize || manager.context.state.pageSize);
|
|
26
|
+
if (filters) {
|
|
27
|
+
manager.context.state.filters = parseFilters(filters);
|
|
28
|
+
}
|
|
29
|
+
const res = await manager.search();
|
|
30
|
+
return {
|
|
31
|
+
items: res.items,
|
|
32
|
+
total: res.total,
|
|
33
|
+
page: res.page,
|
|
34
|
+
pageSize: res.pageSize
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
return async function handler(req, res) {
|
|
39
|
+
const { query, variables } = req.body || {};
|
|
40
|
+
const result = await graphql({ schema, source: query, rootValue: root, variableValues: variables });
|
|
41
|
+
res.json(result);
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export default createSearchkitGraphQLHandler;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { SearchManager } from '../core/SearchManager';
|
|
2
|
+
type InstantSearchRequest = {
|
|
3
|
+
indexName: string;
|
|
4
|
+
params: Record<string, any>;
|
|
5
|
+
};
|
|
6
|
+
type InstantSearchResult = {
|
|
7
|
+
results: {
|
|
8
|
+
hits: any[];
|
|
9
|
+
nbHits: number;
|
|
10
|
+
page: number;
|
|
11
|
+
hitsPerPage: number;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
export declare function createSearchkitBridge(manager: SearchManager): {
|
|
15
|
+
search(requests: InstantSearchRequest[]): Promise<InstantSearchResult[]>;
|
|
16
|
+
searchForFacetValues(indexName: string, params: any): Promise<{
|
|
17
|
+
facetHits: any;
|
|
18
|
+
exhaustiveFacetsCount: boolean;
|
|
19
|
+
}>;
|
|
20
|
+
};
|
|
21
|
+
export default createSearchkitBridge;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
function mapRequestToQuery(params, existing) {
|
|
2
|
+
const q = existing ?? {};
|
|
3
|
+
if (typeof params.query === 'string')
|
|
4
|
+
q.term = params.query;
|
|
5
|
+
if (typeof params.page === 'number')
|
|
6
|
+
q.page = params.page + 1; // instantsearch pages are 0-based
|
|
7
|
+
if (typeof params.hitsPerPage === 'number')
|
|
8
|
+
q.pageSize = params.hitsPerPage;
|
|
9
|
+
// Map simple filters from InstantSearch `facets` or `filters` param
|
|
10
|
+
if (params.filters && typeof params.filters === 'string') {
|
|
11
|
+
// basic parsing: "field:value AND other:value"
|
|
12
|
+
const entries = params.filters.split(' AND ').map((s) => s.split(':'));
|
|
13
|
+
q.filters = Object.fromEntries(entries.map(([k, v]) => [k, v.replace(/^"|"$/g, '')]));
|
|
14
|
+
}
|
|
15
|
+
return q;
|
|
16
|
+
}
|
|
17
|
+
export function createSearchkitBridge(manager) {
|
|
18
|
+
return {
|
|
19
|
+
// InstantSearch client "search" method
|
|
20
|
+
async search(requests) {
|
|
21
|
+
// Support single-index or multi-index by mapping each request to a search call
|
|
22
|
+
const results = [];
|
|
23
|
+
for (const req of requests) {
|
|
24
|
+
const params = req.params || {};
|
|
25
|
+
const built = mapRequestToQuery(params);
|
|
26
|
+
// apply to manager context
|
|
27
|
+
manager.context.setQuery(built.term || '');
|
|
28
|
+
manager.context.setPage(built.page || 1);
|
|
29
|
+
manager.context.setPageSize(built.pageSize || manager.context.state.pageSize);
|
|
30
|
+
if (built.filters) {
|
|
31
|
+
// clear and set simple filters
|
|
32
|
+
manager.context.state.filters = built.filters;
|
|
33
|
+
}
|
|
34
|
+
const res = await manager.search();
|
|
35
|
+
results.push({
|
|
36
|
+
results: {
|
|
37
|
+
hits: res.items,
|
|
38
|
+
nbHits: res.total,
|
|
39
|
+
page: (res.page || 1) - 1,
|
|
40
|
+
hitsPerPage: res.pageSize || manager.context.state.pageSize
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return results;
|
|
45
|
+
},
|
|
46
|
+
// Minimal support for Searchkit/InstantSearch facet search
|
|
47
|
+
async searchForFacetValues(indexName, params) {
|
|
48
|
+
// Map to a query with term for facet matching
|
|
49
|
+
const built = mapRequestToQuery({ query: params.facetQuery });
|
|
50
|
+
manager.context.setQuery(built.term || '');
|
|
51
|
+
manager.context.setPage(1);
|
|
52
|
+
const res = await manager.search();
|
|
53
|
+
return {
|
|
54
|
+
facetHits: (res.items || []).map((item) => ({ value: item[params.attribute], count: 0 })),
|
|
55
|
+
exhaustiveFacetsCount: true
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export default createSearchkitBridge;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare function useSearch(): {
|
|
2
|
+
query: import("vue").Ref<string, string>;
|
|
3
|
+
page: import("vue").Ref<number, number>;
|
|
4
|
+
pageSize: import("vue").Ref<number, number>;
|
|
5
|
+
results: import("vue").ComputedRef<any[]>;
|
|
6
|
+
total: import("vue").ComputedRef<number>;
|
|
7
|
+
loading: import("vue").ComputedRef<boolean>;
|
|
8
|
+
search: () => Promise<void>;
|
|
9
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { ref, computed } from 'vue';
|
|
2
|
+
import { useAlternateContext } from '@meeovi/core';
|
|
3
|
+
export function useSearch() {
|
|
4
|
+
const ctx = useAlternateContext();
|
|
5
|
+
const manager = ctx.searchManager;
|
|
6
|
+
const query = ref(manager.context.state.query);
|
|
7
|
+
const page = ref(manager.context.state.page);
|
|
8
|
+
const pageSize = ref(manager.context.state.pageSize);
|
|
9
|
+
const results = ref([]);
|
|
10
|
+
const total = ref(0);
|
|
11
|
+
const loading = ref(false);
|
|
12
|
+
async function run() {
|
|
13
|
+
loading.value = true;
|
|
14
|
+
manager.context.setQuery(query.value);
|
|
15
|
+
manager.context.setPage(page.value);
|
|
16
|
+
manager.context.setPageSize(pageSize.value);
|
|
17
|
+
const res = await manager.search();
|
|
18
|
+
results.value = res.items;
|
|
19
|
+
total.value = res.total;
|
|
20
|
+
loading.value = false;
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
query,
|
|
24
|
+
page,
|
|
25
|
+
pageSize,
|
|
26
|
+
results: computed(() => results.value),
|
|
27
|
+
total: computed(() => total.value),
|
|
28
|
+
loading: computed(() => loading.value),
|
|
29
|
+
search: run
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { createAlternateApp } from '@meeovi/core';
|
|
6
|
+
import searchModule from './module';
|
|
7
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
async function run() {
|
|
9
|
+
const [, , command, arg] = process.argv;
|
|
10
|
+
const defaultProvider = process.env.SEARCH_PROVIDER === 'meilisearch' ? 'meilisearch' : 'opensearch';
|
|
11
|
+
const app = createAlternateApp({
|
|
12
|
+
config: {
|
|
13
|
+
env: 'production',
|
|
14
|
+
search: {
|
|
15
|
+
defaultProvider,
|
|
16
|
+
providers: {
|
|
17
|
+
opensearch: {
|
|
18
|
+
endpoint: process.env.OPENSEARCH_ENDPOINT,
|
|
19
|
+
index: process.env.OPENSEARCH_INDEX
|
|
20
|
+
},
|
|
21
|
+
meilisearch: {
|
|
22
|
+
host: process.env.MEILI_HOST,
|
|
23
|
+
index: process.env.MEILI_INDEX,
|
|
24
|
+
apiKey: process.env.MEILI_KEY
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
modules: [searchModule]
|
|
30
|
+
});
|
|
31
|
+
const ctx = await app.start();
|
|
32
|
+
const search = ctx.getAdapter('search');
|
|
33
|
+
if (!search) {
|
|
34
|
+
console.error('No search adapter registered');
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
if (command === 'warmup') {
|
|
38
|
+
console.log('Warming up search provider...');
|
|
39
|
+
await search.search({ term: 'warmup' });
|
|
40
|
+
console.log('Warmup complete');
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (command === 'index') {
|
|
44
|
+
if (!arg) {
|
|
45
|
+
console.error('Missing file path: meeovi-search index <file.json>');
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
const filePath = path.resolve(process.cwd(), arg);
|
|
49
|
+
const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
50
|
+
console.log(`Indexing ${data.length} items...`);
|
|
51
|
+
if (search.id === 'search:opensearch') {
|
|
52
|
+
// TODO: implement bulk indexing
|
|
53
|
+
}
|
|
54
|
+
if (search.id === 'search:meilisearch') {
|
|
55
|
+
await fetch(`${process.env.MEILI_HOST}/indexes/${process.env.MEILI_INDEX}/documents`, {
|
|
56
|
+
method: 'POST',
|
|
57
|
+
headers: {
|
|
58
|
+
'Content-Type': 'application/json',
|
|
59
|
+
Authorization: `Bearer ${process.env.MEILI_KEY}`
|
|
60
|
+
},
|
|
61
|
+
body: JSON.stringify(data)
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
console.log('Indexing complete');
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
console.log(`Unknown command: ${command}`);
|
|
68
|
+
}
|
|
69
|
+
run();
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export function validateSearchConfig(config) {
|
|
2
|
+
if (!config.defaultProvider) {
|
|
3
|
+
throw new Error('[@meeovi/search] Missing defaultProvider');
|
|
4
|
+
}
|
|
5
|
+
if (!config.providers[config.defaultProvider]) {
|
|
6
|
+
throw new Error(`[@meeovi/search] Provider "${config.defaultProvider}" not found in config.providers`);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare const Facets: {
|
|
2
|
+
terms(field: string): {
|
|
3
|
+
type: string;
|
|
4
|
+
field: string;
|
|
5
|
+
};
|
|
6
|
+
range(field: string, ranges: {
|
|
7
|
+
from?: number;
|
|
8
|
+
to?: number;
|
|
9
|
+
}[]): {
|
|
10
|
+
type: string;
|
|
11
|
+
field: string;
|
|
12
|
+
ranges: {
|
|
13
|
+
from?: number;
|
|
14
|
+
to?: number;
|
|
15
|
+
}[];
|
|
16
|
+
};
|
|
17
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare const Filters: {
|
|
2
|
+
term(field: string, value: string): {
|
|
3
|
+
type: string;
|
|
4
|
+
field: string;
|
|
5
|
+
value: string;
|
|
6
|
+
};
|
|
7
|
+
range(field: string, min?: number, max?: number): {
|
|
8
|
+
type: string;
|
|
9
|
+
field: string;
|
|
10
|
+
min: number | undefined;
|
|
11
|
+
max: number | undefined;
|
|
12
|
+
};
|
|
13
|
+
boolean(field: string, value: boolean): {
|
|
14
|
+
type: string;
|
|
15
|
+
field: string;
|
|
16
|
+
value: boolean;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export class SearchPipeline {
|
|
2
|
+
before = [];
|
|
3
|
+
after = [];
|
|
4
|
+
useBefore(fn) {
|
|
5
|
+
this.before.push(fn);
|
|
6
|
+
}
|
|
7
|
+
useAfter(fn) {
|
|
8
|
+
this.after.push(fn);
|
|
9
|
+
}
|
|
10
|
+
runBefore(query) {
|
|
11
|
+
return this.before.reduce((q, fn) => fn(q), query);
|
|
12
|
+
}
|
|
13
|
+
runAfter(result) {
|
|
14
|
+
return this.after.reduce((r, fn) => fn(r), result);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { SearchContext } from './SearchContext';
|
|
2
|
+
export interface BuiltSearchQuery {
|
|
3
|
+
term: string;
|
|
4
|
+
page: number;
|
|
5
|
+
pageSize: number;
|
|
6
|
+
sort?: string;
|
|
7
|
+
filters: Record<string, any>;
|
|
8
|
+
}
|
|
9
|
+
export declare class QueryBuilder {
|
|
10
|
+
private context;
|
|
11
|
+
constructor(context: SearchContext);
|
|
12
|
+
build(): BuiltSearchQuery;
|
|
13
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export class QueryBuilder {
|
|
2
|
+
context;
|
|
3
|
+
constructor(context) {
|
|
4
|
+
this.context = context;
|
|
5
|
+
}
|
|
6
|
+
build() {
|
|
7
|
+
return {
|
|
8
|
+
term: this.context.state.query,
|
|
9
|
+
page: this.context.state.page,
|
|
10
|
+
pageSize: this.context.state.pageSize,
|
|
11
|
+
sort: this.context.state.sort,
|
|
12
|
+
filters: this.context.state.filters
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface SearchContextState {
|
|
2
|
+
query: string;
|
|
3
|
+
page: number;
|
|
4
|
+
pageSize: number;
|
|
5
|
+
sort?: string;
|
|
6
|
+
filters: Record<string, any>;
|
|
7
|
+
}
|
|
8
|
+
export declare class SearchContext {
|
|
9
|
+
state: SearchContextState;
|
|
10
|
+
constructor(initial?: Partial<SearchContextState>);
|
|
11
|
+
setQuery(query: string): void;
|
|
12
|
+
setPage(page: number): void;
|
|
13
|
+
setPageSize(size: number): void;
|
|
14
|
+
setSort(sort: string | undefined): void;
|
|
15
|
+
setFilter(key: string, value: any): void;
|
|
16
|
+
removeFilter(key: string): void;
|
|
17
|
+
reset(): void;
|
|
18
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export class SearchContext {
|
|
2
|
+
state;
|
|
3
|
+
constructor(initial) {
|
|
4
|
+
this.state = {
|
|
5
|
+
query: '',
|
|
6
|
+
page: 1,
|
|
7
|
+
pageSize: 20,
|
|
8
|
+
filters: {},
|
|
9
|
+
...initial
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
setQuery(query) {
|
|
13
|
+
this.state.query = query;
|
|
14
|
+
}
|
|
15
|
+
setPage(page) {
|
|
16
|
+
this.state.page = page;
|
|
17
|
+
}
|
|
18
|
+
setPageSize(size) {
|
|
19
|
+
this.state.pageSize = size;
|
|
20
|
+
}
|
|
21
|
+
setSort(sort) {
|
|
22
|
+
this.state.sort = sort;
|
|
23
|
+
}
|
|
24
|
+
setFilter(key, value) {
|
|
25
|
+
this.state.filters[key] = value;
|
|
26
|
+
}
|
|
27
|
+
removeFilter(key) {
|
|
28
|
+
delete this.state.filters[key];
|
|
29
|
+
}
|
|
30
|
+
reset() {
|
|
31
|
+
this.state = {
|
|
32
|
+
query: '',
|
|
33
|
+
page: 1,
|
|
34
|
+
pageSize: 20,
|
|
35
|
+
filters: {}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { SearchAdapter } from '@meeovi/core';
|
|
2
|
+
import { SearchContext, type SearchContextState } from './SearchContext';
|
|
3
|
+
import { SearchPipeline } from './Pipeline';
|
|
4
|
+
export declare class SearchManager<TItem = any> {
|
|
5
|
+
context: SearchContext;
|
|
6
|
+
pipeline: SearchPipeline;
|
|
7
|
+
adapter: SearchAdapter<TItem>;
|
|
8
|
+
constructor(adapter: SearchAdapter<TItem>, initial?: Partial<SearchContextState>);
|
|
9
|
+
search(): Promise<any>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { SearchContext } from './SearchContext';
|
|
2
|
+
import { QueryBuilder } from './QueryBuilder';
|
|
3
|
+
import { SearchPipeline } from './Pipeline';
|
|
4
|
+
export class SearchManager {
|
|
5
|
+
context;
|
|
6
|
+
pipeline;
|
|
7
|
+
adapter;
|
|
8
|
+
constructor(adapter, initial) {
|
|
9
|
+
this.context = new SearchContext(initial);
|
|
10
|
+
this.pipeline = new SearchPipeline();
|
|
11
|
+
this.adapter = adapter;
|
|
12
|
+
}
|
|
13
|
+
async search() {
|
|
14
|
+
const builder = new QueryBuilder(this.context);
|
|
15
|
+
let query = builder.build();
|
|
16
|
+
query = this.pipeline.runBefore(query);
|
|
17
|
+
const result = await this.adapter.search(query);
|
|
18
|
+
return this.pipeline.runAfter(result);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export * from './module';
|
|
2
|
+
export * from './adapter/types';
|
|
3
|
+
export * from './core/SearchManager';
|
|
4
|
+
export * from './core/SearchContext';
|
|
5
|
+
export * from './core/Filters';
|
|
6
|
+
export * from './core/Facets';
|
|
7
|
+
export * from './bridges/instantsearch';
|
|
8
|
+
export * from './bridges/vue';
|
|
9
|
+
export * from './bridges/react';
|
|
10
|
+
export * from './bridges/searchkit';
|
|
11
|
+
export * from './bridges/searchkit-server';
|
|
12
|
+
export * from './utils/health';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export * from './module';
|
|
2
|
+
export * from './adapter/types';
|
|
3
|
+
export * from './core/SearchManager';
|
|
4
|
+
export * from './core/SearchContext';
|
|
5
|
+
export * from './core/Filters';
|
|
6
|
+
export * from './core/Facets';
|
|
7
|
+
export * from './bridges/instantsearch';
|
|
8
|
+
export * from './bridges/vue';
|
|
9
|
+
export * from './bridges/react';
|
|
10
|
+
export * from './bridges/searchkit';
|
|
11
|
+
export * from './bridges/searchkit-server';
|
|
12
|
+
export * from './utils/health';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type AlternateContext } from '@meeovi/core';
|
|
2
|
+
import { type SearchModuleConfig } from './config/schema';
|
|
3
|
+
import type { MeeoviSearchItem } from './adapter/types';
|
|
4
|
+
declare module '@meeovi/core' {
|
|
5
|
+
interface AlternateConfig {
|
|
6
|
+
search?: SearchModuleConfig;
|
|
7
|
+
}
|
|
8
|
+
interface AlternateContext {
|
|
9
|
+
searchManager?: import('./core/SearchManager').SearchManager<MeeoviSearchItem>;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
declare const _default: {
|
|
13
|
+
id: string;
|
|
14
|
+
adapters: {};
|
|
15
|
+
setup(ctx: AlternateContext): Promise<void>;
|
|
16
|
+
};
|
|
17
|
+
export default _default;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { defineAlternateModule, useAlternateEventBus } from '@meeovi/core';
|
|
2
|
+
import { validateSearchConfig } from './config/schema';
|
|
3
|
+
import { createOpenSearchAdapter } from './adapter/opensearch';
|
|
4
|
+
import { createMeilisearchAdapter } from './adapter/meilisearch';
|
|
5
|
+
import { SearchManager } from './core/SearchManager';
|
|
6
|
+
export default defineAlternateModule({
|
|
7
|
+
id: 'search',
|
|
8
|
+
adapters: {},
|
|
9
|
+
async setup(ctx) {
|
|
10
|
+
const bus = useAlternateEventBus();
|
|
11
|
+
const config = ctx.config.search;
|
|
12
|
+
if (!config)
|
|
13
|
+
return;
|
|
14
|
+
validateSearchConfig(config);
|
|
15
|
+
if (config.defaultProvider === 'opensearch') {
|
|
16
|
+
const providerConfig = config.providers.opensearch;
|
|
17
|
+
if (providerConfig) {
|
|
18
|
+
this.adapters = {
|
|
19
|
+
search: createOpenSearchAdapter(providerConfig)
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (config.defaultProvider === 'meilisearch') {
|
|
24
|
+
const providerConfig = config.providers.meilisearch;
|
|
25
|
+
if (providerConfig) {
|
|
26
|
+
this.adapters = {
|
|
27
|
+
search: createMeilisearchAdapter(providerConfig)
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const adapter = ctx.getAdapter('search');
|
|
32
|
+
if (adapter) {
|
|
33
|
+
ctx.searchManager = new SearchManager(adapter);
|
|
34
|
+
}
|
|
35
|
+
// Ensure search manager exists now if adapter already registered
|
|
36
|
+
if (!ctx.searchManager && ctx.getAdapter('search')) {
|
|
37
|
+
const runtimeAdapter = ctx.getAdapter('search');
|
|
38
|
+
if (runtimeAdapter) {
|
|
39
|
+
ctx.searchManager = new SearchManager(runtimeAdapter);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// Listen for app readiness and for runtime adapter registrations so
|
|
43
|
+
// new adapters (registered by other modules) will auto-create the
|
|
44
|
+
// search manager when they become available.
|
|
45
|
+
bus.on('app:ready', () => {
|
|
46
|
+
if (!ctx.searchManager) {
|
|
47
|
+
const runtimeAdapter = ctx.getAdapter('search');
|
|
48
|
+
if (runtimeAdapter) {
|
|
49
|
+
ctx.searchManager = new SearchManager(runtimeAdapter);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
console.info('[@meeovi/search] Search module initialized');
|
|
53
|
+
});
|
|
54
|
+
// Optional runtime event: if other modules emit `adapter:registered`
|
|
55
|
+
// with a `{ key }` payload, respond and initialize when the `search`
|
|
56
|
+
// adapter becomes available.
|
|
57
|
+
// This is defensive — the core registry does not emit this by default,
|
|
58
|
+
// but some runtimes may choose to.
|
|
59
|
+
bus.on('adapter:registered', (payload) => {
|
|
60
|
+
try {
|
|
61
|
+
if (payload?.key === 'search' && !ctx.searchManager) {
|
|
62
|
+
const runtimeAdapter = ctx.getAdapter('search');
|
|
63
|
+
if (runtimeAdapter) {
|
|
64
|
+
ctx.searchManager = new SearchManager(runtimeAdapter);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch (e) {
|
|
69
|
+
/* noop */
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { SearchAdapter } from '@meeovi/core';
|
|
2
|
+
export declare function checkAdapterHealth(adapter: SearchAdapter<any>): Promise<{
|
|
3
|
+
ok: boolean;
|
|
4
|
+
total: number;
|
|
5
|
+
error?: undefined;
|
|
6
|
+
} | {
|
|
7
|
+
ok: boolean;
|
|
8
|
+
error: any;
|
|
9
|
+
total?: undefined;
|
|
10
|
+
}>;
|
|
11
|
+
export default checkAdapterHealth;
|