@86d-app/search 0.0.23 → 0.0.24
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/dist/modules/search/src/__tests__/admin-settings.test.js +262 -0
- package/dist/modules/search/src/__tests__/controllers.test.js +853 -0
- package/dist/modules/search/src/__tests__/embedding-provider.test.js +150 -0
- package/dist/modules/search/src/__tests__/endpoint-security.test.js +250 -0
- package/dist/modules/search/src/__tests__/meilisearch-provider.test.js +318 -0
- package/dist/modules/search/src/__tests__/service-impl.test.js +703 -0
- package/dist/modules/search/src/__tests__/store-endpoints.test.js +295 -0
- package/dist/{admin/components/index.d.ts → modules/search/src/admin/components/index.jsx} +0 -1
- package/dist/modules/search/src/admin/components/search-analytics.jsx +230 -0
- package/dist/modules/search/src/admin/endpoints/analytics.js +9 -0
- package/dist/modules/search/src/admin/endpoints/bulk-index.js +26 -0
- package/dist/modules/search/src/admin/endpoints/click-analytics.js +9 -0
- package/dist/modules/search/src/admin/endpoints/get-settings.js +97 -0
- package/dist/modules/search/src/admin/endpoints/index-manage.js +32 -0
- package/dist/modules/search/src/admin/endpoints/index.js +21 -0
- package/dist/modules/search/src/admin/endpoints/popular.js +11 -0
- package/dist/modules/search/src/admin/endpoints/synonyms.js +30 -0
- package/dist/modules/search/src/admin/endpoints/zero-results.js +11 -0
- package/dist/modules/search/src/embedding-provider.js +77 -0
- package/dist/modules/search/src/index.js +75 -0
- package/dist/modules/search/src/meilisearch-provider.js +138 -0
- package/dist/modules/search/src/schema.js +61 -0
- package/dist/modules/search/src/service-impl.js +770 -0
- package/dist/modules/search/src/service.js +1 -0
- package/dist/modules/search/src/store/components/_hooks.js +10 -0
- package/dist/modules/search/src/store/components/index.jsx +9 -0
- package/dist/modules/search/src/store/components/search-bar.jsx +91 -0
- package/dist/modules/search/src/store/components/search-page.jsx +17 -0
- package/dist/modules/search/src/store/components/search-results.jsx +51 -0
- package/dist/modules/search/src/store/endpoints/click.js +15 -0
- package/dist/modules/search/src/store/endpoints/index.js +12 -0
- package/dist/modules/search/src/store/endpoints/recent.js +18 -0
- package/dist/modules/search/src/store/endpoints/search.js +57 -0
- package/dist/modules/search/src/store/endpoints/store-search.js +33 -0
- package/dist/modules/search/src/store/endpoints/suggest.js +12 -0
- package/package.json +1 -1
- package/src/__tests__/admin-settings.test.ts +367 -0
- package/src/__tests__/store-endpoints.test.ts +392 -0
- package/src/admin/endpoints/get-settings.ts +77 -0
- package/dist/__tests__/controllers.test.d.ts +0 -2
- package/dist/__tests__/controllers.test.d.ts.map +0 -1
- package/dist/__tests__/embedding-provider.test.d.ts +0 -2
- package/dist/__tests__/embedding-provider.test.d.ts.map +0 -1
- package/dist/__tests__/endpoint-security.test.d.ts +0 -2
- package/dist/__tests__/endpoint-security.test.d.ts.map +0 -1
- package/dist/__tests__/meilisearch-provider.test.d.ts +0 -2
- package/dist/__tests__/meilisearch-provider.test.d.ts.map +0 -1
- package/dist/__tests__/service-impl.test.d.ts +0 -2
- package/dist/__tests__/service-impl.test.d.ts.map +0 -1
- package/dist/admin/components/index.d.ts.map +0 -1
- package/dist/admin/components/search-analytics.d.ts +0 -2
- package/dist/admin/components/search-analytics.d.ts.map +0 -1
- package/dist/admin/endpoints/analytics.d.ts +0 -15
- package/dist/admin/endpoints/analytics.d.ts.map +0 -1
- package/dist/admin/endpoints/bulk-index.d.ts +0 -20
- package/dist/admin/endpoints/bulk-index.d.ts.map +0 -1
- package/dist/admin/endpoints/click-analytics.d.ts +0 -7
- package/dist/admin/endpoints/click-analytics.d.ts.map +0 -1
- package/dist/admin/endpoints/get-settings.d.ts +0 -17
- package/dist/admin/endpoints/get-settings.d.ts.map +0 -1
- package/dist/admin/endpoints/index-manage.d.ts +0 -26
- package/dist/admin/endpoints/index-manage.d.ts.map +0 -1
- package/dist/admin/endpoints/index.d.ts +0 -125
- package/dist/admin/endpoints/index.d.ts.map +0 -1
- package/dist/admin/endpoints/popular.d.ts +0 -10
- package/dist/admin/endpoints/popular.d.ts.map +0 -1
- package/dist/admin/endpoints/synonyms.d.ts +0 -30
- package/dist/admin/endpoints/synonyms.d.ts.map +0 -1
- package/dist/admin/endpoints/zero-results.d.ts +0 -10
- package/dist/admin/endpoints/zero-results.d.ts.map +0 -1
- package/dist/embedding-provider.d.ts +0 -28
- package/dist/embedding-provider.d.ts.map +0 -1
- package/dist/index.d.ts +0 -23
- package/dist/index.d.ts.map +0 -1
- package/dist/meilisearch-provider.d.ts +0 -104
- package/dist/meilisearch-provider.d.ts.map +0 -1
- package/dist/schema.d.ts +0 -133
- package/dist/schema.d.ts.map +0 -1
- package/dist/service-impl.d.ts +0 -6
- package/dist/service-impl.d.ts.map +0 -1
- package/dist/service.d.ts +0 -127
- package/dist/service.d.ts.map +0 -1
- package/dist/store/components/_hooks.d.ts +0 -6
- package/dist/store/components/_hooks.d.ts.map +0 -1
- package/dist/store/components/index.d.ts +0 -10
- package/dist/store/components/index.d.ts.map +0 -1
- package/dist/store/components/search-bar.d.ts +0 -7
- package/dist/store/components/search-bar.d.ts.map +0 -1
- package/dist/store/components/search-page.d.ts +0 -4
- package/dist/store/components/search-page.d.ts.map +0 -1
- package/dist/store/components/search-results.d.ts +0 -9
- package/dist/store/components/search-results.d.ts.map +0 -1
- package/dist/store/endpoints/click.d.ts +0 -14
- package/dist/store/endpoints/click.d.ts.map +0 -1
- package/dist/store/endpoints/index.d.ts +0 -85
- package/dist/store/endpoints/index.d.ts.map +0 -1
- package/dist/store/endpoints/recent.d.ts +0 -15
- package/dist/store/endpoints/recent.d.ts.map +0 -1
- package/dist/store/endpoints/search.d.ts +0 -36
- package/dist/store/endpoints/search.d.ts.map +0 -1
- package/dist/store/endpoints/store-search.d.ts +0 -16
- package/dist/store/endpoints/store-search.d.ts.map +0 -1
- package/dist/store/endpoints/suggest.d.ts +0 -11
- package/dist/store/endpoints/suggest.d.ts.map +0 -1
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import { createMockDataService } from "@86d-app/core/test-utils";
|
|
2
|
+
import { beforeEach, describe, expect, it } from "vitest";
|
|
3
|
+
import type { SearchResult } from "../service";
|
|
4
|
+
import { createSearchController } from "../service-impl";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Store endpoint integration tests for the search module.
|
|
8
|
+
*
|
|
9
|
+
* These tests verify the business logic in store-facing endpoints:
|
|
10
|
+
*
|
|
11
|
+
* 1. search: comma-separated tag parsing, response shaping (strips
|
|
12
|
+
* internal fields like body/metadata), fire-and-forget analytics,
|
|
13
|
+
* facets and didYouMean pass-through
|
|
14
|
+
* 2. suggest: prefix autocomplete, limit handling
|
|
15
|
+
* 3. click: analytics click recording
|
|
16
|
+
* 4. recent: session-scoped recent query history, response shaping
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
type DataService = ReturnType<typeof createMockDataService>;
|
|
20
|
+
|
|
21
|
+
// ── Simulate store endpoint logic ─────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Simulates the search endpoint: parses tags from comma-separated
|
|
25
|
+
* string, delegates to controller, shapes response by stripping
|
|
26
|
+
* internal fields.
|
|
27
|
+
*/
|
|
28
|
+
async function simulateSearch(
|
|
29
|
+
data: DataService,
|
|
30
|
+
query: {
|
|
31
|
+
q: string;
|
|
32
|
+
type?: string;
|
|
33
|
+
tags?: string;
|
|
34
|
+
sort?: "relevance" | "newest" | "oldest" | "title_asc" | "title_desc";
|
|
35
|
+
fuzzy?: boolean;
|
|
36
|
+
limit?: number;
|
|
37
|
+
skip?: number;
|
|
38
|
+
sessionId?: string;
|
|
39
|
+
},
|
|
40
|
+
) {
|
|
41
|
+
const controller = createSearchController(data);
|
|
42
|
+
const parsedTags = query.tags
|
|
43
|
+
? query.tags
|
|
44
|
+
.split(",")
|
|
45
|
+
.map((t) => t.trim())
|
|
46
|
+
.filter(Boolean)
|
|
47
|
+
: undefined;
|
|
48
|
+
|
|
49
|
+
const { results, total, facets, didYouMean } = await controller.search(
|
|
50
|
+
query.q,
|
|
51
|
+
{
|
|
52
|
+
entityType: query.type,
|
|
53
|
+
tags: parsedTags,
|
|
54
|
+
sort: query.sort,
|
|
55
|
+
fuzzy: query.fuzzy,
|
|
56
|
+
limit: query.limit ?? 20,
|
|
57
|
+
skip: query.skip ?? 0,
|
|
58
|
+
},
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// Fire-and-forget analytics recording
|
|
62
|
+
controller.recordQuery(query.q, total, query.sessionId).catch(() => {});
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
results: results.map((r: SearchResult) => ({
|
|
66
|
+
id: r.item.id,
|
|
67
|
+
entityType: r.item.entityType,
|
|
68
|
+
entityId: r.item.entityId,
|
|
69
|
+
title: r.item.title,
|
|
70
|
+
url: r.item.url,
|
|
71
|
+
image: r.item.image,
|
|
72
|
+
tags: r.item.tags,
|
|
73
|
+
score: r.score,
|
|
74
|
+
highlights: r.highlights,
|
|
75
|
+
})),
|
|
76
|
+
total,
|
|
77
|
+
facets,
|
|
78
|
+
didYouMean,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Simulates the suggest endpoint.
|
|
84
|
+
*/
|
|
85
|
+
async function simulateSuggest(data: DataService, q: string, limit?: number) {
|
|
86
|
+
const controller = createSearchController(data);
|
|
87
|
+
const suggestions = await controller.suggest(q, limit ?? 8);
|
|
88
|
+
return { suggestions };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Simulates the click endpoint.
|
|
93
|
+
*/
|
|
94
|
+
async function simulateClick(
|
|
95
|
+
data: DataService,
|
|
96
|
+
body: {
|
|
97
|
+
queryId: string;
|
|
98
|
+
term: string;
|
|
99
|
+
entityType: string;
|
|
100
|
+
entityId: string;
|
|
101
|
+
position: number;
|
|
102
|
+
},
|
|
103
|
+
) {
|
|
104
|
+
const controller = createSearchController(data);
|
|
105
|
+
const click = await controller.recordClick(body);
|
|
106
|
+
return { id: click.id };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Simulates the recent endpoint: returns shaped recent queries.
|
|
111
|
+
*/
|
|
112
|
+
async function simulateRecent(
|
|
113
|
+
data: DataService,
|
|
114
|
+
sessionId: string,
|
|
115
|
+
limit?: number,
|
|
116
|
+
) {
|
|
117
|
+
const controller = createSearchController(data);
|
|
118
|
+
const queries = await controller.getRecentQueries(sessionId, limit ?? 10);
|
|
119
|
+
return {
|
|
120
|
+
recent: queries.map((q) => ({
|
|
121
|
+
term: q.term,
|
|
122
|
+
resultCount: q.resultCount,
|
|
123
|
+
searchedAt: q.searchedAt,
|
|
124
|
+
})),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ── Helpers ───────────────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
async function seedIndexItems(data: DataService) {
|
|
131
|
+
const controller = createSearchController(data);
|
|
132
|
+
await controller.indexItem({
|
|
133
|
+
entityType: "product",
|
|
134
|
+
entityId: "prod-1",
|
|
135
|
+
title: "Red Widget",
|
|
136
|
+
body: "A fantastic red widget for everyday use",
|
|
137
|
+
tags: ["electronics", "sale"],
|
|
138
|
+
url: "/products/red-widget",
|
|
139
|
+
image: "/img/red-widget.jpg",
|
|
140
|
+
});
|
|
141
|
+
await controller.indexItem({
|
|
142
|
+
entityType: "product",
|
|
143
|
+
entityId: "prod-2",
|
|
144
|
+
title: "Blue Gadget",
|
|
145
|
+
body: "The best blue gadget on the market",
|
|
146
|
+
tags: ["electronics", "new"],
|
|
147
|
+
url: "/products/blue-gadget",
|
|
148
|
+
});
|
|
149
|
+
await controller.indexItem({
|
|
150
|
+
entityType: "page",
|
|
151
|
+
entityId: "page-1",
|
|
152
|
+
title: "About Us",
|
|
153
|
+
body: "Learn about our company",
|
|
154
|
+
tags: ["info"],
|
|
155
|
+
url: "/about",
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ── Tests ─────────────────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
describe("search store endpoints", () => {
|
|
162
|
+
let data: DataService;
|
|
163
|
+
|
|
164
|
+
beforeEach(() => {
|
|
165
|
+
data = createMockDataService();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// ── search ───────────────────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
describe("search", () => {
|
|
171
|
+
it("returns results matching query", async () => {
|
|
172
|
+
await seedIndexItems(data);
|
|
173
|
+
const result = await simulateSearch(data, { q: "widget" });
|
|
174
|
+
|
|
175
|
+
expect(result.total).toBeGreaterThan(0);
|
|
176
|
+
expect(result.results[0].title).toContain("Widget");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("shapes response by stripping internal fields", async () => {
|
|
180
|
+
await seedIndexItems(data);
|
|
181
|
+
const result = await simulateSearch(data, { q: "red" });
|
|
182
|
+
|
|
183
|
+
const firstResult = result.results[0];
|
|
184
|
+
// Should include these fields
|
|
185
|
+
expect(firstResult).toHaveProperty("id");
|
|
186
|
+
expect(firstResult).toHaveProperty("entityType");
|
|
187
|
+
expect(firstResult).toHaveProperty("entityId");
|
|
188
|
+
expect(firstResult).toHaveProperty("title");
|
|
189
|
+
expect(firstResult).toHaveProperty("url");
|
|
190
|
+
expect(firstResult).toHaveProperty("tags");
|
|
191
|
+
expect(firstResult).toHaveProperty("score");
|
|
192
|
+
// Should NOT include body or metadata (internal fields)
|
|
193
|
+
expect(firstResult).not.toHaveProperty("body");
|
|
194
|
+
expect(firstResult).not.toHaveProperty("metadata");
|
|
195
|
+
expect(firstResult).not.toHaveProperty("indexedAt");
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("filters by entity type", async () => {
|
|
199
|
+
await seedIndexItems(data);
|
|
200
|
+
const result = await simulateSearch(data, {
|
|
201
|
+
q: "about",
|
|
202
|
+
type: "page",
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
for (const r of result.results) {
|
|
206
|
+
expect(r.entityType).toBe("page");
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("parses comma-separated tags into array", async () => {
|
|
211
|
+
await seedIndexItems(data);
|
|
212
|
+
const result = await simulateSearch(data, {
|
|
213
|
+
q: "widget",
|
|
214
|
+
tags: "electronics, sale",
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Should find items tagged with electronics AND sale
|
|
218
|
+
expect(result.total).toBeGreaterThanOrEqual(0);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("handles tags with extra whitespace and empty segments", async () => {
|
|
222
|
+
await seedIndexItems(data);
|
|
223
|
+
|
|
224
|
+
// "electronics,, new, " should parse to ["electronics", "new"]
|
|
225
|
+
const result = await simulateSearch(data, {
|
|
226
|
+
q: "gadget",
|
|
227
|
+
tags: "electronics,, new, ",
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
expect(result.total).toBeGreaterThanOrEqual(0);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("returns empty results for no-match query", async () => {
|
|
234
|
+
await seedIndexItems(data);
|
|
235
|
+
const result = await simulateSearch(data, {
|
|
236
|
+
q: "xyznonexistent",
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
expect(result.results).toHaveLength(0);
|
|
240
|
+
expect(result.total).toBe(0);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("respects limit and skip pagination", async () => {
|
|
244
|
+
await seedIndexItems(data);
|
|
245
|
+
const page1 = await simulateSearch(data, {
|
|
246
|
+
q: "widget gadget about",
|
|
247
|
+
limit: 1,
|
|
248
|
+
skip: 0,
|
|
249
|
+
});
|
|
250
|
+
const page2 = await simulateSearch(data, {
|
|
251
|
+
q: "widget gadget about",
|
|
252
|
+
limit: 1,
|
|
253
|
+
skip: 1,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// Pages should return at most 1 result each
|
|
257
|
+
expect(page1.results.length).toBeLessThanOrEqual(1);
|
|
258
|
+
expect(page2.results.length).toBeLessThanOrEqual(1);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("returns facets with result counts", async () => {
|
|
262
|
+
await seedIndexItems(data);
|
|
263
|
+
const result = await simulateSearch(data, { q: "widget" });
|
|
264
|
+
|
|
265
|
+
expect(result.facets).toBeDefined();
|
|
266
|
+
expect(result.facets.entityTypes).toBeDefined();
|
|
267
|
+
expect(result.facets.tags).toBeDefined();
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("includes image field when present", async () => {
|
|
271
|
+
await seedIndexItems(data);
|
|
272
|
+
const result = await simulateSearch(data, { q: "red widget" });
|
|
273
|
+
|
|
274
|
+
const redWidget = result.results.find((r) => r.entityId === "prod-1");
|
|
275
|
+
if (redWidget) {
|
|
276
|
+
expect(redWidget.image).toBe("/img/red-widget.jpg");
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const blueGadget = result.results.find((r) => r.entityId === "prod-2");
|
|
280
|
+
if (blueGadget) {
|
|
281
|
+
expect(blueGadget.image).toBeUndefined();
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// ── suggest ──────────────────────────────────────────────────────
|
|
287
|
+
|
|
288
|
+
describe("suggest", () => {
|
|
289
|
+
it("returns suggestions for prefix", async () => {
|
|
290
|
+
await seedIndexItems(data);
|
|
291
|
+
const result = await simulateSuggest(data, "wid");
|
|
292
|
+
|
|
293
|
+
expect(result.suggestions).toBeDefined();
|
|
294
|
+
expect(Array.isArray(result.suggestions)).toBe(true);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("returns empty for no-match prefix", async () => {
|
|
298
|
+
await seedIndexItems(data);
|
|
299
|
+
const result = await simulateSuggest(data, "zzz");
|
|
300
|
+
expect(result.suggestions).toHaveLength(0);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("respects custom limit", async () => {
|
|
304
|
+
await seedIndexItems(data);
|
|
305
|
+
const result = await simulateSuggest(data, "a", 2);
|
|
306
|
+
expect(result.suggestions.length).toBeLessThanOrEqual(2);
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// ── click ────────────────────────────────────────────────────────
|
|
311
|
+
|
|
312
|
+
describe("click", () => {
|
|
313
|
+
it("records a click and returns id", async () => {
|
|
314
|
+
const result = await simulateClick(data, {
|
|
315
|
+
queryId: "q-1",
|
|
316
|
+
term: "widget",
|
|
317
|
+
entityType: "product",
|
|
318
|
+
entityId: "prod-1",
|
|
319
|
+
position: 0,
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
expect(result.id).toBeDefined();
|
|
323
|
+
expect(typeof result.id).toBe("string");
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it("records multiple clicks for same query", async () => {
|
|
327
|
+
const click1 = await simulateClick(data, {
|
|
328
|
+
queryId: "q-1",
|
|
329
|
+
term: "widget",
|
|
330
|
+
entityType: "product",
|
|
331
|
+
entityId: "prod-1",
|
|
332
|
+
position: 0,
|
|
333
|
+
});
|
|
334
|
+
const click2 = await simulateClick(data, {
|
|
335
|
+
queryId: "q-1",
|
|
336
|
+
term: "widget",
|
|
337
|
+
entityType: "product",
|
|
338
|
+
entityId: "prod-2",
|
|
339
|
+
position: 1,
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
expect(click1.id).not.toBe(click2.id);
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// ── recent ───────────────────────────────────────────────────────
|
|
347
|
+
|
|
348
|
+
describe("recent", () => {
|
|
349
|
+
it("returns empty for new session", async () => {
|
|
350
|
+
const result = await simulateRecent(data, "session-new");
|
|
351
|
+
expect(result.recent).toHaveLength(0);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it("returns shaped recent queries (term, resultCount, searchedAt only)", async () => {
|
|
355
|
+
const controller = createSearchController(data);
|
|
356
|
+
await controller.recordQuery("widgets", 5, "session-1");
|
|
357
|
+
await controller.recordQuery("gadgets", 3, "session-1");
|
|
358
|
+
|
|
359
|
+
const result = await simulateRecent(data, "session-1");
|
|
360
|
+
expect(result.recent.length).toBeGreaterThan(0);
|
|
361
|
+
|
|
362
|
+
const entry = result.recent[0];
|
|
363
|
+
expect(entry).toHaveProperty("term");
|
|
364
|
+
expect(entry).toHaveProperty("resultCount");
|
|
365
|
+
expect(entry).toHaveProperty("searchedAt");
|
|
366
|
+
// Should NOT include internal fields
|
|
367
|
+
expect(entry).not.toHaveProperty("id");
|
|
368
|
+
expect(entry).not.toHaveProperty("normalizedTerm");
|
|
369
|
+
expect(entry).not.toHaveProperty("sessionId");
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it("scopes queries to session", async () => {
|
|
373
|
+
const controller = createSearchController(data);
|
|
374
|
+
await controller.recordQuery("widgets", 5, "session-1");
|
|
375
|
+
await controller.recordQuery("gadgets", 3, "session-2");
|
|
376
|
+
|
|
377
|
+
const result = await simulateRecent(data, "session-1");
|
|
378
|
+
expect(result.recent).toHaveLength(1);
|
|
379
|
+
expect(result.recent[0].term).toBe("widgets");
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it("respects limit parameter", async () => {
|
|
383
|
+
const controller = createSearchController(data);
|
|
384
|
+
for (let i = 0; i < 5; i++) {
|
|
385
|
+
await controller.recordQuery(`term-${i}`, i, "session-1");
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const result = await simulateRecent(data, "session-1", 2);
|
|
389
|
+
expect(result.recent.length).toBeLessThanOrEqual(2);
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createAdminEndpoint } from "@86d-app/core";
|
|
2
|
+
import { MeiliSearchProvider } from "../../meilisearch-provider";
|
|
2
3
|
import type { SearchController } from "../../service";
|
|
3
4
|
|
|
4
5
|
function maskKey(key: string): string {
|
|
@@ -15,6 +16,31 @@ interface SearchModuleOptions {
|
|
|
15
16
|
embeddingModel?: string;
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
async function verifyEmbeddingConnection(
|
|
20
|
+
apiKey: string,
|
|
21
|
+
baseUrl: string,
|
|
22
|
+
): Promise<{ ok: true } | { ok: false; error: string }> {
|
|
23
|
+
try {
|
|
24
|
+
const res = await fetch(`${baseUrl}/models`, {
|
|
25
|
+
method: "GET",
|
|
26
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
27
|
+
});
|
|
28
|
+
if (res.ok) {
|
|
29
|
+
return { ok: true };
|
|
30
|
+
}
|
|
31
|
+
const text = await res.text().catch(() => "");
|
|
32
|
+
return {
|
|
33
|
+
ok: false,
|
|
34
|
+
error: `HTTP ${res.status}${text ? `: ${text.slice(0, 200)}` : ""}`,
|
|
35
|
+
};
|
|
36
|
+
} catch (err) {
|
|
37
|
+
return {
|
|
38
|
+
ok: false,
|
|
39
|
+
error: err instanceof Error ? err.message : "Connection failed",
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
18
44
|
export const getSettings = createAdminEndpoint(
|
|
19
45
|
"/admin/search/settings",
|
|
20
46
|
{ method: "GET" },
|
|
@@ -30,18 +56,69 @@ export const getSettings = createAdminEndpoint(
|
|
|
30
56
|
options?.openaiApiKey || options?.openrouterApiKey,
|
|
31
57
|
);
|
|
32
58
|
|
|
59
|
+
let meilisearchStatus: "connected" | "not_configured" | "error" =
|
|
60
|
+
"not_configured";
|
|
61
|
+
let meilisearchError: string | undefined;
|
|
62
|
+
let documentCount: number | undefined;
|
|
63
|
+
|
|
64
|
+
if (
|
|
65
|
+
meilisearchConfigured &&
|
|
66
|
+
options?.meilisearchHost &&
|
|
67
|
+
options.meilisearchApiKey
|
|
68
|
+
) {
|
|
69
|
+
const provider = new MeiliSearchProvider(
|
|
70
|
+
options.meilisearchHost,
|
|
71
|
+
options.meilisearchApiKey,
|
|
72
|
+
options.meilisearchIndexUid,
|
|
73
|
+
);
|
|
74
|
+
const healthy = await provider.isHealthy();
|
|
75
|
+
if (healthy) {
|
|
76
|
+
meilisearchStatus = "connected";
|
|
77
|
+
const stats = await provider.getStats();
|
|
78
|
+
if (stats) {
|
|
79
|
+
documentCount = stats.numberOfDocuments;
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
meilisearchStatus = "error";
|
|
83
|
+
meilisearchError = "MeiliSearch instance is not reachable";
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let embeddingStatus: "connected" | "not_configured" | "error" =
|
|
88
|
+
"not_configured";
|
|
89
|
+
let embeddingError: string | undefined;
|
|
90
|
+
|
|
91
|
+
if (embeddingConfigured) {
|
|
92
|
+
const apiKey = options?.openaiApiKey ?? options?.openrouterApiKey ?? "";
|
|
93
|
+
const baseUrl = options?.openrouterApiKey
|
|
94
|
+
? "https://openrouter.ai/api/v1"
|
|
95
|
+
: "https://api.openai.com/v1";
|
|
96
|
+
const result = await verifyEmbeddingConnection(apiKey, baseUrl);
|
|
97
|
+
if (result.ok) {
|
|
98
|
+
embeddingStatus = "connected";
|
|
99
|
+
} else {
|
|
100
|
+
embeddingStatus = "error";
|
|
101
|
+
embeddingError = result.error;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
33
105
|
const indexCount = await controller.getIndexCount();
|
|
34
106
|
|
|
35
107
|
return {
|
|
36
108
|
meilisearch: {
|
|
109
|
+
status: meilisearchStatus,
|
|
110
|
+
error: meilisearchError,
|
|
37
111
|
configured: meilisearchConfigured,
|
|
38
112
|
host: options?.meilisearchHost ?? null,
|
|
39
113
|
apiKey: options?.meilisearchApiKey
|
|
40
114
|
? maskKey(options.meilisearchApiKey)
|
|
41
115
|
: null,
|
|
42
116
|
indexUid: options?.meilisearchIndexUid ?? "search",
|
|
117
|
+
documentCount,
|
|
43
118
|
},
|
|
44
119
|
embeddings: {
|
|
120
|
+
status: embeddingStatus,
|
|
121
|
+
error: embeddingError,
|
|
45
122
|
configured: embeddingConfigured,
|
|
46
123
|
provider: options?.openaiApiKey
|
|
47
124
|
? "openai"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"controllers.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/controllers.test.ts"],"names":[],"mappings":""}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"embedding-provider.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/embedding-provider.test.ts"],"names":[],"mappings":""}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"endpoint-security.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/endpoint-security.test.ts"],"names":[],"mappings":""}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"meilisearch-provider.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/meilisearch-provider.test.ts"],"names":[],"mappings":""}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"service-impl.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/service-impl.test.ts"],"names":[],"mappings":""}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/admin/components/index.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"search-analytics.d.ts","sourceRoot":"","sources":["../../../src/admin/components/search-analytics.tsx"],"names":[],"mappings":"AA4FA,wBAAgB,eAAe,gCA+T9B"}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
export declare const analyticsEndpoint: import("better-call").StrictEndpoint<"/admin/search/analytics", {
|
|
2
|
-
method: "GET";
|
|
3
|
-
}, {
|
|
4
|
-
analytics: {
|
|
5
|
-
indexedItems: number;
|
|
6
|
-
totalQueries: number;
|
|
7
|
-
uniqueTerms: number;
|
|
8
|
-
avgResultCount: number;
|
|
9
|
-
zeroResultCount: number;
|
|
10
|
-
zeroResultRate: number;
|
|
11
|
-
clickThroughRate: number;
|
|
12
|
-
avgClickPosition: number;
|
|
13
|
-
};
|
|
14
|
-
}>;
|
|
15
|
-
//# sourceMappingURL=analytics.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"analytics.d.ts","sourceRoot":"","sources":["../../../src/admin/endpoints/analytics.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;EAW7B,CAAC"}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { z } from "@86d-app/core";
|
|
2
|
-
export declare const bulkIndex: import("better-call").StrictEndpoint<"/admin/search/index/bulk", {
|
|
3
|
-
method: "POST";
|
|
4
|
-
body: z.ZodObject<{
|
|
5
|
-
items: z.ZodArray<z.ZodObject<{
|
|
6
|
-
entityType: z.ZodString;
|
|
7
|
-
entityId: z.ZodString;
|
|
8
|
-
title: z.ZodString;
|
|
9
|
-
body: z.ZodOptional<z.ZodString>;
|
|
10
|
-
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
11
|
-
url: z.ZodString;
|
|
12
|
-
image: z.ZodOptional<z.ZodString>;
|
|
13
|
-
metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
14
|
-
}, z.core.$strip>>;
|
|
15
|
-
}, z.core.$strip>;
|
|
16
|
-
}, {
|
|
17
|
-
indexed: number;
|
|
18
|
-
errors: number;
|
|
19
|
-
}>;
|
|
20
|
-
//# sourceMappingURL=bulk-index.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"bulk-index.d.ts","sourceRoot":"","sources":["../../../src/admin/endpoints/bulk-index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,CAAC,EAAE,MAAM,eAAe,CAAC;AAGvD,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;EA8BrB,CAAC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"click-analytics.d.ts","sourceRoot":"","sources":["../../../src/admin/endpoints/click-analytics.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,sBAAsB;;;;;EAYlC,CAAC"}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
export declare const getSettings: import("better-call").StrictEndpoint<"/admin/search/settings", {
|
|
2
|
-
method: "GET";
|
|
3
|
-
}, {
|
|
4
|
-
meilisearch: {
|
|
5
|
-
configured: boolean;
|
|
6
|
-
host: string | null;
|
|
7
|
-
apiKey: string | null;
|
|
8
|
-
indexUid: string;
|
|
9
|
-
};
|
|
10
|
-
embeddings: {
|
|
11
|
-
configured: boolean;
|
|
12
|
-
provider: string | null;
|
|
13
|
-
model: string;
|
|
14
|
-
};
|
|
15
|
-
indexCount: number;
|
|
16
|
-
}>;
|
|
17
|
-
//# sourceMappingURL=get-settings.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"get-settings.d.ts","sourceRoot":"","sources":["../../../src/admin/endpoints/get-settings.ts"],"names":[],"mappings":"AAiBA,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;EAsCvB,CAAC"}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { z } from "@86d-app/core";
|
|
2
|
-
export declare const indexItem: import("better-call").StrictEndpoint<"/admin/search/index", {
|
|
3
|
-
method: "POST";
|
|
4
|
-
body: z.ZodObject<{
|
|
5
|
-
entityType: z.ZodString;
|
|
6
|
-
entityId: z.ZodString;
|
|
7
|
-
title: z.ZodString;
|
|
8
|
-
body: z.ZodOptional<z.ZodString>;
|
|
9
|
-
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
10
|
-
url: z.ZodString;
|
|
11
|
-
image: z.ZodOptional<z.ZodString>;
|
|
12
|
-
metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
13
|
-
}, z.core.$strip>;
|
|
14
|
-
}, {
|
|
15
|
-
item: import("../..").SearchIndexItem;
|
|
16
|
-
}>;
|
|
17
|
-
export declare const removeFromIndex: import("better-call").StrictEndpoint<"/admin/search/index/remove", {
|
|
18
|
-
method: "POST";
|
|
19
|
-
body: z.ZodObject<{
|
|
20
|
-
entityType: z.ZodString;
|
|
21
|
-
entityId: z.ZodString;
|
|
22
|
-
}, z.core.$strip>;
|
|
23
|
-
}, {
|
|
24
|
-
removed: boolean;
|
|
25
|
-
}>;
|
|
26
|
-
//# sourceMappingURL=index-manage.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index-manage.d.ts","sourceRoot":"","sources":["../../../src/admin/endpoints/index-manage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,CAAC,EAAE,MAAM,eAAe,CAAC;AAGvD,eAAO,MAAM,SAAS;;;;;;;;;;;;;;EAuBrB,CAAC;AAEF,eAAO,MAAM,eAAe;;;;;;;;EAiB3B,CAAC"}
|