@porulle/adapter-meilisearch 0.10.8 → 0.13.0
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.
|
@@ -16,6 +16,7 @@ interface MeiliIndexLike {
|
|
|
16
16
|
facetDistribution?: Record<string, Record<string, number>>;
|
|
17
17
|
}>;
|
|
18
18
|
updateFilterableAttributes(attributes: string[]): Promise<unknown>;
|
|
19
|
+
getFilterableAttributes?(): Promise<unknown>;
|
|
19
20
|
}
|
|
20
21
|
interface MeiliClientLike {
|
|
21
22
|
index(uid: string): MeiliIndexLike;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAwB,KAAK,aAAa,EAAE,KAAK,cAAc,EAA4E,MAAM,eAAe,CAAC;AAExK,UAAU,cAAc;IACtB,YAAY,CAAC,SAAS,EAAE,cAAc,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC5D,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACjD,MAAM,CACJ,KAAK,EAAE,MAAM,EACb,OAAO,CAAC,EAAE;QACR,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;QAC3B,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;QAClB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;KACjC,GACA,OAAO,CAAC;QACT,IAAI,EAAE,KAAK,CAAC,cAAc,GAAG;YAAE,aAAa,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;QACzD,kBAAkB,CAAC,EAAE,MAAM,CAAC;QAC5B,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;KAC5D,CAAC,CAAC;IACH,0BAA0B,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACnE,uBAAuB,CAAC,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;CAC9C;AAED,UAAU,eAAe;IACvB,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,cAAc,CAAC;CACpC;AAED,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;IAChC,MAAM,CAAC,EAAE,eAAe,CAAC;CAC1B;AAoFD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,yBAAyB,GAAG,aAAa,CAyHpF"}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { MeiliSearch } from "meilisearch";
|
|
2
2
|
import { Err, Ok } from "@porulle/core";
|
|
3
|
+
const SAFE_ATTRIBUTE_NAME = /^[A-Za-z0-9_-]+$/;
|
|
3
4
|
function toFilter(params) {
|
|
4
5
|
const filters = [];
|
|
6
|
+
if (params.filters?.organizationId) {
|
|
7
|
+
filters.push(`organizationId = ${JSON.stringify(params.filters.organizationId)}`);
|
|
8
|
+
}
|
|
5
9
|
if (params.filters?.type) {
|
|
6
10
|
filters.push(`type = \"${params.filters.type}\"`);
|
|
7
11
|
}
|
|
@@ -14,8 +18,33 @@ function toFilter(params) {
|
|
|
14
18
|
if (params.filters?.brand) {
|
|
15
19
|
filters.push(`brands = \"${params.filters.brand}\"`);
|
|
16
20
|
}
|
|
21
|
+
for (const [name, requested] of Object.entries(params.filters?.attributes ?? {})) {
|
|
22
|
+
const values = Array.isArray(requested) ? requested : [requested];
|
|
23
|
+
if (values.length === 0 || !SAFE_ATTRIBUTE_NAME.test(name)) {
|
|
24
|
+
filters.push('id = ""');
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
const expressions = values.map((value) => `attributes.${name} = ${JSON.stringify(value)}`);
|
|
28
|
+
if (expressions.length > 1) {
|
|
29
|
+
filters.push(`(${expressions.join(" OR ")})`);
|
|
30
|
+
}
|
|
31
|
+
else if (expressions.length === 1) {
|
|
32
|
+
filters.push(expressions[0]);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
17
35
|
return filters;
|
|
18
36
|
}
|
|
37
|
+
function normalizeFacets(facets) {
|
|
38
|
+
return Object.fromEntries(Object.entries(facets).map(([name, values]) => [
|
|
39
|
+
name.startsWith("attributes.") ? name.slice("attributes.".length) : name,
|
|
40
|
+
values,
|
|
41
|
+
]));
|
|
42
|
+
}
|
|
43
|
+
function toFacetName(name) {
|
|
44
|
+
if (["type", "status", "category", "categories", "brand", "brands"].includes(name))
|
|
45
|
+
return name;
|
|
46
|
+
return name.startsWith("attributes.") ? name : `attributes.${name}`;
|
|
47
|
+
}
|
|
19
48
|
function normalizeSearchResult(params, result) {
|
|
20
49
|
const page = params.page ?? 1;
|
|
21
50
|
const limit = params.limit ?? 20;
|
|
@@ -28,19 +57,37 @@ function normalizeSearchResult(params, result) {
|
|
|
28
57
|
total: result.estimatedTotalHits ?? result.hits.length,
|
|
29
58
|
page,
|
|
30
59
|
limit,
|
|
31
|
-
facets: result.facetDistribution ?? {},
|
|
60
|
+
facets: normalizeFacets(result.facetDistribution ?? {}),
|
|
32
61
|
};
|
|
33
62
|
}
|
|
34
63
|
export function meilisearchAdapter(options) {
|
|
35
64
|
const client = options.client ?? new MeiliSearch({ host: options.host, ...(options.apiKey ? { apiKey: options.apiKey } : {}) });
|
|
36
65
|
const indexName = options.indexName ?? "catalog";
|
|
37
|
-
const filterable = options.filterableAttributes ?? ["type", "status", "categories", "brands"];
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
66
|
+
const filterable = options.filterableAttributes ?? ["organizationId", "type", "status", "categories", "brands"];
|
|
67
|
+
const attributeNames = new Set();
|
|
68
|
+
let configuredFilterable;
|
|
69
|
+
async function ensureFilterable(index, documents) {
|
|
70
|
+
for (const document of documents) {
|
|
71
|
+
for (const name of Object.keys(document.attributes ?? {})) {
|
|
72
|
+
if (SAFE_ATTRIBUTE_NAME.test(name))
|
|
73
|
+
attributeNames.add(name);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (configuredFilterable === undefined && index.getFilterableAttributes) {
|
|
77
|
+
const stored = await index.getFilterableAttributes();
|
|
78
|
+
configuredFilterable = Array.isArray(stored)
|
|
79
|
+
? stored.filter((name) => typeof name === "string")
|
|
80
|
+
: [];
|
|
81
|
+
}
|
|
82
|
+
const nextFilterable = [
|
|
83
|
+
...(configuredFilterable ?? []),
|
|
84
|
+
...filterable,
|
|
85
|
+
...[...attributeNames].map((name) => `attributes.${name}`),
|
|
86
|
+
].filter((name, position, list) => list.indexOf(name) === position);
|
|
87
|
+
if (configuredFilterable?.length === nextFilterable.length && configuredFilterable.every((name, index) => name === nextFilterable[index]))
|
|
41
88
|
return;
|
|
42
|
-
await index.updateFilterableAttributes(
|
|
43
|
-
|
|
89
|
+
await index.updateFilterableAttributes(nextFilterable);
|
|
90
|
+
configuredFilterable = nextFilterable;
|
|
44
91
|
}
|
|
45
92
|
return {
|
|
46
93
|
providerId: "meilisearch",
|
|
@@ -49,7 +96,7 @@ export function meilisearchAdapter(options) {
|
|
|
49
96
|
if (documents.length === 0)
|
|
50
97
|
return Ok(undefined);
|
|
51
98
|
const index = client.index(indexName);
|
|
52
|
-
await ensureFilterable(index);
|
|
99
|
+
await ensureFilterable(index, documents);
|
|
53
100
|
await index.addDocuments(documents);
|
|
54
101
|
return Ok(undefined);
|
|
55
102
|
}
|
|
@@ -82,9 +129,11 @@ export function meilisearchAdapter(options) {
|
|
|
82
129
|
const offset = (page - 1) * limit;
|
|
83
130
|
const filters = toFilter(params);
|
|
84
131
|
const index = client.index(indexName);
|
|
132
|
+
const requestedFacets = (params.facets ?? ["type", "status", "categories", "brands"]).map(toFacetName);
|
|
133
|
+
const facets = requestedFacets.filter((name, position, list) => list.indexOf(name) === position);
|
|
85
134
|
const result = await index.search(params.query, {
|
|
86
135
|
...(filters.length > 0 ? { filter: filters } : {}),
|
|
87
|
-
facets
|
|
136
|
+
facets,
|
|
88
137
|
limit,
|
|
89
138
|
offset,
|
|
90
139
|
});
|
|
@@ -100,8 +149,14 @@ export function meilisearchAdapter(options) {
|
|
|
100
149
|
async suggest(params) {
|
|
101
150
|
try {
|
|
102
151
|
const index = client.index(indexName);
|
|
152
|
+
const suggestFilters = [
|
|
153
|
+
...(params.organizationId
|
|
154
|
+
? [`organizationId = ${JSON.stringify(params.organizationId)}`]
|
|
155
|
+
: []),
|
|
156
|
+
...(params.type ? [`type = \"${params.type}\"`] : []),
|
|
157
|
+
];
|
|
103
158
|
const result = await index.search(params.prefix, {
|
|
104
|
-
...(
|
|
159
|
+
...(suggestFilters.length > 0 ? { filter: suggestFilters } : {}),
|
|
105
160
|
attributesToRetrieve: ["title"],
|
|
106
161
|
limit: params.limit ?? 10,
|
|
107
162
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@porulle/adapter-meilisearch",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"meilisearch": "^0.54.0",
|
|
15
|
-
"@porulle/core": "0.
|
|
15
|
+
"@porulle/core": "0.13.0"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
18
|
"@types/node": "^24.5.2",
|
package/src/index.ts
CHANGED
|
@@ -19,6 +19,7 @@ interface MeiliIndexLike {
|
|
|
19
19
|
facetDistribution?: Record<string, Record<string, number>>;
|
|
20
20
|
}>;
|
|
21
21
|
updateFilterableAttributes(attributes: string[]): Promise<unknown>;
|
|
22
|
+
getFilterableAttributes?(): Promise<unknown>;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
interface MeiliClientLike {
|
|
@@ -33,9 +34,15 @@ export interface MeilisearchAdapterOptions {
|
|
|
33
34
|
client?: MeiliClientLike;
|
|
34
35
|
}
|
|
35
36
|
|
|
37
|
+
const SAFE_ATTRIBUTE_NAME = /^[A-Za-z0-9_-]+$/;
|
|
38
|
+
|
|
36
39
|
function toFilter(params: SearchQueryParams): string[] {
|
|
37
40
|
const filters: string[] = [];
|
|
38
41
|
|
|
42
|
+
if (params.filters?.organizationId) {
|
|
43
|
+
filters.push(`organizationId = ${JSON.stringify(params.filters.organizationId)}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
39
46
|
if (params.filters?.type) {
|
|
40
47
|
filters.push(`type = \"${params.filters.type}\"`);
|
|
41
48
|
}
|
|
@@ -52,9 +59,39 @@ function toFilter(params: SearchQueryParams): string[] {
|
|
|
52
59
|
filters.push(`brands = \"${params.filters.brand}\"`);
|
|
53
60
|
}
|
|
54
61
|
|
|
62
|
+
for (const [name, requested] of Object.entries(params.filters?.attributes ?? {})) {
|
|
63
|
+
const values = Array.isArray(requested) ? requested : [requested];
|
|
64
|
+
if (values.length === 0 || !SAFE_ATTRIBUTE_NAME.test(name)) {
|
|
65
|
+
filters.push('id = ""');
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const expressions = values.map((value) => `attributes.${name} = ${JSON.stringify(value)}`);
|
|
69
|
+
if (expressions.length > 1) {
|
|
70
|
+
filters.push(`(${expressions.join(" OR ")})`);
|
|
71
|
+
} else if (expressions.length === 1) {
|
|
72
|
+
filters.push(expressions[0]!);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
55
76
|
return filters;
|
|
56
77
|
}
|
|
57
78
|
|
|
79
|
+
function normalizeFacets(
|
|
80
|
+
facets: Record<string, Record<string, number>>,
|
|
81
|
+
): Record<string, Record<string, number>> {
|
|
82
|
+
return Object.fromEntries(
|
|
83
|
+
Object.entries(facets).map(([name, values]) => [
|
|
84
|
+
name.startsWith("attributes.") ? name.slice("attributes.".length) : name,
|
|
85
|
+
values,
|
|
86
|
+
]),
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function toFacetName(name: string): string {
|
|
91
|
+
if (["type", "status", "category", "categories", "brand", "brands"].includes(name)) return name;
|
|
92
|
+
return name.startsWith("attributes.") ? name : `attributes.${name}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
58
95
|
function normalizeSearchResult(
|
|
59
96
|
params: SearchQueryParams,
|
|
60
97
|
result: {
|
|
@@ -75,7 +112,7 @@ function normalizeSearchResult(
|
|
|
75
112
|
total: result.estimatedTotalHits ?? result.hits.length,
|
|
76
113
|
page,
|
|
77
114
|
limit,
|
|
78
|
-
facets: result.facetDistribution ?? {},
|
|
115
|
+
facets: normalizeFacets(result.facetDistribution ?? {}),
|
|
79
116
|
};
|
|
80
117
|
}
|
|
81
118
|
|
|
@@ -83,14 +120,31 @@ export function meilisearchAdapter(options: MeilisearchAdapterOptions): SearchAd
|
|
|
83
120
|
const client: MeiliClientLike =
|
|
84
121
|
options.client ?? new MeiliSearch({ host: options.host, ...(options.apiKey ? { apiKey: options.apiKey } : {}) });
|
|
85
122
|
const indexName = options.indexName ?? "catalog";
|
|
86
|
-
const filterable = options.filterableAttributes ?? ["type", "status", "categories", "brands"];
|
|
123
|
+
const filterable = options.filterableAttributes ?? ["organizationId", "type", "status", "categories", "brands"];
|
|
87
124
|
|
|
88
|
-
|
|
125
|
+
const attributeNames = new Set<string>();
|
|
126
|
+
let configuredFilterable: string[] | undefined;
|
|
89
127
|
|
|
90
|
-
async function ensureFilterable(index: MeiliIndexLike): Promise<void> {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
128
|
+
async function ensureFilterable(index: MeiliIndexLike, documents: SearchDocument[]): Promise<void> {
|
|
129
|
+
for (const document of documents) {
|
|
130
|
+
for (const name of Object.keys(document.attributes ?? {})) {
|
|
131
|
+
if (SAFE_ATTRIBUTE_NAME.test(name)) attributeNames.add(name);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (configuredFilterable === undefined && index.getFilterableAttributes) {
|
|
135
|
+
const stored = await index.getFilterableAttributes();
|
|
136
|
+
configuredFilterable = Array.isArray(stored)
|
|
137
|
+
? stored.filter((name): name is string => typeof name === "string")
|
|
138
|
+
: [];
|
|
139
|
+
}
|
|
140
|
+
const nextFilterable = [
|
|
141
|
+
...(configuredFilterable ?? []),
|
|
142
|
+
...filterable,
|
|
143
|
+
...[...attributeNames].map((name) => `attributes.${name}`),
|
|
144
|
+
].filter((name, position, list) => list.indexOf(name) === position);
|
|
145
|
+
if (configuredFilterable?.length === nextFilterable.length && configuredFilterable.every((name, index) => name === nextFilterable[index])) return;
|
|
146
|
+
await index.updateFilterableAttributes(nextFilterable);
|
|
147
|
+
configuredFilterable = nextFilterable;
|
|
94
148
|
}
|
|
95
149
|
|
|
96
150
|
return {
|
|
@@ -100,7 +154,7 @@ export function meilisearchAdapter(options: MeilisearchAdapterOptions): SearchAd
|
|
|
100
154
|
try {
|
|
101
155
|
if (documents.length === 0) return Ok(undefined);
|
|
102
156
|
const index = client.index(indexName);
|
|
103
|
-
await ensureFilterable(index);
|
|
157
|
+
await ensureFilterable(index, documents);
|
|
104
158
|
await index.addDocuments(documents);
|
|
105
159
|
return Ok(undefined);
|
|
106
160
|
} catch (error) {
|
|
@@ -133,9 +187,11 @@ export function meilisearchAdapter(options: MeilisearchAdapterOptions): SearchAd
|
|
|
133
187
|
const filters = toFilter(params);
|
|
134
188
|
|
|
135
189
|
const index = client.index(indexName);
|
|
190
|
+
const requestedFacets = (params.facets ?? ["type", "status", "categories", "brands"]).map(toFacetName);
|
|
191
|
+
const facets = requestedFacets.filter((name, position, list) => list.indexOf(name) === position);
|
|
136
192
|
const result = await index.search(params.query, {
|
|
137
193
|
...(filters.length > 0 ? { filter: filters } : {}),
|
|
138
|
-
facets
|
|
194
|
+
facets,
|
|
139
195
|
limit,
|
|
140
196
|
offset,
|
|
141
197
|
});
|
|
@@ -152,8 +208,14 @@ export function meilisearchAdapter(options: MeilisearchAdapterOptions): SearchAd
|
|
|
152
208
|
async suggest(params: SearchSuggestParams): Promise<Result<string[]>> {
|
|
153
209
|
try {
|
|
154
210
|
const index = client.index(indexName);
|
|
211
|
+
const suggestFilters = [
|
|
212
|
+
...(params.organizationId
|
|
213
|
+
? [`organizationId = ${JSON.stringify(params.organizationId)}`]
|
|
214
|
+
: []),
|
|
215
|
+
...(params.type ? [`type = \"${params.type}\"`] : []),
|
|
216
|
+
];
|
|
155
217
|
const result = await index.search(params.prefix, {
|
|
156
|
-
...(
|
|
218
|
+
...(suggestFilters.length > 0 ? { filter: suggestFilters } : {}),
|
|
157
219
|
attributesToRetrieve: ["title"],
|
|
158
220
|
limit: params.limit ?? 10,
|
|
159
221
|
});
|
package/dist/src/index.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAwB,KAAK,aAAa,EAAE,KAAK,cAAc,EAA4E,MAAM,eAAe,CAAC;AAExK,UAAU,cAAc;IACtB,YAAY,CAAC,SAAS,EAAE,cAAc,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC5D,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACjD,MAAM,CACJ,KAAK,EAAE,MAAM,EACb,OAAO,CAAC,EAAE;QACR,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;QAC3B,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;QAClB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;KACjC,GACA,OAAO,CAAC;QACT,IAAI,EAAE,KAAK,CAAC,cAAc,GAAG;YAAE,aAAa,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;QACzD,kBAAkB,CAAC,EAAE,MAAM,CAAC;QAC5B,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;KAC5D,CAAC,CAAC;IACH,0BAA0B,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACpE;AAED,UAAU,eAAe;IACvB,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,cAAc,CAAC;CACpC;AAED,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;IAChC,MAAM,CAAC,EAAE,eAAe,CAAC;CAC1B;AAgDD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,yBAAyB,GAAG,aAAa,CAgGpF"}
|