@86d-app/search 0.0.3 → 0.0.6

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 (93) hide show
  1. package/.turbo/turbo-build.log +1 -0
  2. package/AGENTS.md +72 -0
  3. package/README.md +172 -30
  4. package/dist/__tests__/controllers.test.d.ts +2 -0
  5. package/dist/__tests__/controllers.test.d.ts.map +1 -0
  6. package/dist/__tests__/embedding-provider.test.d.ts +2 -0
  7. package/dist/__tests__/embedding-provider.test.d.ts.map +1 -0
  8. package/dist/__tests__/endpoint-security.test.d.ts +2 -0
  9. package/dist/__tests__/endpoint-security.test.d.ts.map +1 -0
  10. package/dist/__tests__/meilisearch-provider.test.d.ts +2 -0
  11. package/dist/__tests__/meilisearch-provider.test.d.ts.map +1 -0
  12. package/dist/__tests__/service-impl.test.d.ts +2 -0
  13. package/dist/__tests__/service-impl.test.d.ts.map +1 -0
  14. package/dist/admin/components/index.d.ts +2 -0
  15. package/dist/admin/components/index.d.ts.map +1 -0
  16. package/dist/admin/components/search-analytics.d.ts +2 -0
  17. package/dist/admin/components/search-analytics.d.ts.map +1 -0
  18. package/dist/admin/endpoints/analytics.d.ts +15 -0
  19. package/dist/admin/endpoints/analytics.d.ts.map +1 -0
  20. package/dist/admin/endpoints/bulk-index.d.ts +20 -0
  21. package/dist/admin/endpoints/bulk-index.d.ts.map +1 -0
  22. package/dist/admin/endpoints/click-analytics.d.ts +7 -0
  23. package/dist/admin/endpoints/click-analytics.d.ts.map +1 -0
  24. package/dist/admin/endpoints/get-settings.d.ts +17 -0
  25. package/dist/admin/endpoints/get-settings.d.ts.map +1 -0
  26. package/dist/admin/endpoints/index-manage.d.ts +26 -0
  27. package/dist/admin/endpoints/index-manage.d.ts.map +1 -0
  28. package/dist/admin/endpoints/index.d.ts +125 -0
  29. package/dist/admin/endpoints/index.d.ts.map +1 -0
  30. package/dist/admin/endpoints/popular.d.ts +10 -0
  31. package/dist/admin/endpoints/popular.d.ts.map +1 -0
  32. package/dist/admin/endpoints/synonyms.d.ts +30 -0
  33. package/dist/admin/endpoints/synonyms.d.ts.map +1 -0
  34. package/dist/admin/endpoints/zero-results.d.ts +10 -0
  35. package/dist/admin/endpoints/zero-results.d.ts.map +1 -0
  36. package/dist/embedding-provider.d.ts +28 -0
  37. package/dist/embedding-provider.d.ts.map +1 -0
  38. package/dist/index.d.ts +23 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/meilisearch-provider.d.ts +104 -0
  41. package/dist/meilisearch-provider.d.ts.map +1 -0
  42. package/dist/schema.d.ts +133 -0
  43. package/dist/schema.d.ts.map +1 -0
  44. package/dist/service-impl.d.ts +6 -0
  45. package/dist/service-impl.d.ts.map +1 -0
  46. package/dist/service.d.ts +127 -0
  47. package/dist/service.d.ts.map +1 -0
  48. package/dist/store/components/_hooks.d.ts +6 -0
  49. package/dist/store/components/_hooks.d.ts.map +1 -0
  50. package/dist/store/components/index.d.ts +10 -0
  51. package/dist/store/components/index.d.ts.map +1 -0
  52. package/dist/store/components/search-bar.d.ts +7 -0
  53. package/dist/store/components/search-bar.d.ts.map +1 -0
  54. package/dist/store/components/search-page.d.ts +4 -0
  55. package/dist/store/components/search-page.d.ts.map +1 -0
  56. package/dist/store/components/search-results.d.ts +9 -0
  57. package/dist/store/components/search-results.d.ts.map +1 -0
  58. package/dist/store/endpoints/click.d.ts +14 -0
  59. package/dist/store/endpoints/click.d.ts.map +1 -0
  60. package/dist/store/endpoints/index.d.ts +85 -0
  61. package/dist/store/endpoints/index.d.ts.map +1 -0
  62. package/dist/store/endpoints/recent.d.ts +15 -0
  63. package/dist/store/endpoints/recent.d.ts.map +1 -0
  64. package/dist/store/endpoints/search.d.ts +36 -0
  65. package/dist/store/endpoints/search.d.ts.map +1 -0
  66. package/dist/store/endpoints/store-search.d.ts +16 -0
  67. package/dist/store/endpoints/store-search.d.ts.map +1 -0
  68. package/dist/store/endpoints/suggest.d.ts +11 -0
  69. package/dist/store/endpoints/suggest.d.ts.map +1 -0
  70. package/package.json +3 -3
  71. package/src/__tests__/controllers.test.ts +1026 -0
  72. package/src/__tests__/embedding-provider.test.ts +195 -0
  73. package/src/__tests__/endpoint-security.test.ts +300 -0
  74. package/src/__tests__/meilisearch-provider.test.ts +400 -0
  75. package/src/__tests__/service-impl.test.ts +341 -8
  76. package/src/admin/components/search-analytics.tsx +120 -0
  77. package/src/admin/endpoints/bulk-index.ts +34 -0
  78. package/src/admin/endpoints/click-analytics.ts +16 -0
  79. package/src/admin/endpoints/get-settings.ts +56 -0
  80. package/src/admin/endpoints/index-manage.ts +4 -1
  81. package/src/admin/endpoints/index.ts +6 -0
  82. package/src/admin/endpoints/synonyms.ts +1 -1
  83. package/src/embedding-provider.ts +99 -0
  84. package/src/index.ts +60 -4
  85. package/src/meilisearch-provider.ts +239 -0
  86. package/src/schema.ts +15 -0
  87. package/src/service-impl.ts +605 -34
  88. package/src/service.ts +60 -1
  89. package/src/store/endpoints/click.ts +21 -0
  90. package/src/store/endpoints/index.ts +2 -0
  91. package/src/store/endpoints/search.ts +38 -10
  92. package/src/store/endpoints/suggest.ts +2 -2
  93. package/vitest.config.ts +2 -0
package/src/service.ts CHANGED
@@ -29,9 +29,37 @@ export interface SearchSynonym {
29
29
  createdAt: Date;
30
30
  }
31
31
 
32
+ export interface SearchClick {
33
+ id: string;
34
+ queryId: string;
35
+ term: string;
36
+ entityType: string;
37
+ entityId: string;
38
+ position: number;
39
+ clickedAt: Date;
40
+ }
41
+
42
+ export type SearchSortField =
43
+ | "relevance"
44
+ | "newest"
45
+ | "oldest"
46
+ | "title_asc"
47
+ | "title_desc";
48
+
49
+ export interface SearchFacets {
50
+ entityTypes: Array<{ type: string; count: number }>;
51
+ tags: Array<{ tag: string; count: number }>;
52
+ }
53
+
32
54
  export interface SearchResult {
33
55
  item: SearchIndexItem;
34
56
  score: number;
57
+ highlights?: SearchHighlight | undefined;
58
+ }
59
+
60
+ export interface SearchHighlight {
61
+ title?: string | undefined;
62
+ body?: string | undefined;
35
63
  }
36
64
 
37
65
  export interface SearchAnalyticsSummary {
@@ -40,6 +68,8 @@ export interface SearchAnalyticsSummary {
40
68
  avgResultCount: number;
41
69
  zeroResultCount: number;
42
70
  zeroResultRate: number;
71
+ clickThroughRate: number;
72
+ avgClickPosition: number;
43
73
  }
44
74
 
45
75
  export interface PopularTerm {
@@ -60,16 +90,37 @@ export interface SearchController extends ModuleController {
60
90
  metadata?: Record<string, unknown> | undefined;
61
91
  }): Promise<SearchIndexItem>;
62
92
 
93
+ bulkIndex(
94
+ items: Array<{
95
+ entityType: string;
96
+ entityId: string;
97
+ title: string;
98
+ body?: string | undefined;
99
+ tags?: string[] | undefined;
100
+ url: string;
101
+ image?: string | undefined;
102
+ metadata?: Record<string, unknown> | undefined;
103
+ }>,
104
+ ): Promise<{ indexed: number; errors: number }>;
105
+
63
106
  removeFromIndex(entityType: string, entityId: string): Promise<boolean>;
64
107
 
65
108
  search(
66
109
  query: string,
67
110
  options?: {
68
111
  entityType?: string | undefined;
112
+ tags?: string[] | undefined;
113
+ sort?: SearchSortField | undefined;
114
+ fuzzy?: boolean | undefined;
69
115
  limit?: number | undefined;
70
116
  skip?: number | undefined;
71
117
  },
72
- ): Promise<{ results: SearchResult[]; total: number }>;
118
+ ): Promise<{
119
+ results: SearchResult[];
120
+ total: number;
121
+ facets: SearchFacets;
122
+ didYouMean?: string | undefined;
123
+ }>;
73
124
 
74
125
  suggest(prefix: string, limit?: number): Promise<string[]>;
75
126
 
@@ -79,6 +130,14 @@ export interface SearchController extends ModuleController {
79
130
  sessionId?: string | undefined,
80
131
  ): Promise<SearchQuery>;
81
132
 
133
+ recordClick(params: {
134
+ queryId: string;
135
+ term: string;
136
+ entityType: string;
137
+ entityId: string;
138
+ position: number;
139
+ }): Promise<SearchClick>;
140
+
82
141
  getRecentQueries(sessionId: string, limit?: number): Promise<SearchQuery[]>;
83
142
 
84
143
  getPopularTerms(limit?: number): Promise<PopularTerm[]>;
@@ -0,0 +1,21 @@
1
+ import { createStoreEndpoint, sanitizeText, z } from "@86d-app/core";
2
+ import type { SearchController } from "../../service";
3
+
4
+ export const clickEndpoint = createStoreEndpoint(
5
+ "/search/click",
6
+ {
7
+ method: "POST",
8
+ body: z.object({
9
+ queryId: z.string().min(1).max(200),
10
+ term: z.string().min(1).max(500).transform(sanitizeText),
11
+ entityType: z.string().min(1).max(100).transform(sanitizeText),
12
+ entityId: z.string().min(1).max(200),
13
+ position: z.number().int().min(0).max(1000),
14
+ }),
15
+ },
16
+ async (ctx) => {
17
+ const controller = ctx.context.controllers.search as SearchController;
18
+ const click = await controller.recordClick(ctx.body);
19
+ return { id: click.id };
20
+ },
21
+ );
@@ -1,3 +1,4 @@
1
+ import { clickEndpoint } from "./click";
1
2
  import { recentEndpoint } from "./recent";
2
3
  import { searchEndpoint } from "./search";
3
4
  import { storeSearch } from "./store-search";
@@ -8,4 +9,5 @@ export const storeEndpoints = {
8
9
  "/search": searchEndpoint,
9
10
  "/search/suggest": suggestEndpoint,
10
11
  "/search/recent": recentEndpoint,
12
+ "/search/click": clickEndpoint,
11
13
  };
@@ -1,25 +1,49 @@
1
- import { createStoreEndpoint, z } from "@86d-app/core";
2
- import type { SearchController } from "../../service";
1
+ import { createStoreEndpoint, sanitizeText, z } from "@86d-app/core";
2
+ import type { SearchController, SearchSortField } from "../../service";
3
+
4
+ const sortFields = [
5
+ "relevance",
6
+ "newest",
7
+ "oldest",
8
+ "title_asc",
9
+ "title_desc",
10
+ ] as const;
3
11
 
4
12
  export const searchEndpoint = createStoreEndpoint(
5
13
  "/search",
6
14
  {
7
15
  method: "GET",
8
16
  query: z.object({
9
- q: z.string().min(1).max(500),
10
- type: z.string().optional(),
17
+ q: z.string().min(1).max(500).transform(sanitizeText),
18
+ type: z.string().max(100).optional(),
19
+ tags: z.string().max(2000).optional(),
20
+ sort: z.enum(sortFields).optional(),
21
+ fuzzy: z.coerce.boolean().optional(),
11
22
  limit: z.coerce.number().int().min(1).max(100).optional(),
12
23
  skip: z.coerce.number().int().min(0).optional(),
13
- sessionId: z.string().optional(),
24
+ sessionId: z.string().max(200).optional(),
14
25
  }),
15
26
  },
16
27
  async (ctx) => {
17
28
  const controller = ctx.context.controllers.search as SearchController;
18
- const { results, total } = await controller.search(ctx.query.q, {
19
- entityType: ctx.query.type,
20
- limit: ctx.query.limit ?? 20,
21
- skip: ctx.query.skip ?? 0,
22
- });
29
+ const parsedTags = ctx.query.tags
30
+ ? ctx.query.tags
31
+ .split(",")
32
+ .map((t) => t.trim())
33
+ .filter(Boolean)
34
+ : undefined;
35
+
36
+ const { results, total, facets, didYouMean } = await controller.search(
37
+ ctx.query.q,
38
+ {
39
+ entityType: ctx.query.type,
40
+ tags: parsedTags,
41
+ sort: ctx.query.sort as SearchSortField | undefined,
42
+ fuzzy: ctx.query.fuzzy,
43
+ limit: ctx.query.limit ?? 20,
44
+ skip: ctx.query.skip ?? 0,
45
+ },
46
+ );
23
47
 
24
48
  // Record query for analytics (fire-and-forget)
25
49
  controller
@@ -34,9 +58,13 @@ export const searchEndpoint = createStoreEndpoint(
34
58
  title: r.item.title,
35
59
  url: r.item.url,
36
60
  image: r.item.image,
61
+ tags: r.item.tags,
37
62
  score: r.score,
63
+ highlights: r.highlights,
38
64
  })),
39
65
  total,
66
+ facets,
67
+ didYouMean,
40
68
  };
41
69
  },
42
70
  );
@@ -1,4 +1,4 @@
1
- import { createStoreEndpoint, z } from "@86d-app/core";
1
+ import { createStoreEndpoint, sanitizeText, z } from "@86d-app/core";
2
2
  import type { SearchController } from "../../service";
3
3
 
4
4
  export const suggestEndpoint = createStoreEndpoint(
@@ -6,7 +6,7 @@ export const suggestEndpoint = createStoreEndpoint(
6
6
  {
7
7
  method: "GET",
8
8
  query: z.object({
9
- q: z.string().min(1).max(200),
9
+ q: z.string().min(1).max(200).transform(sanitizeText),
10
10
  limit: z.coerce.number().int().min(1).max(20).optional(),
11
11
  }),
12
12
  },
@@ -0,0 +1,2 @@
1
+ import { defineConfig } from "vitest/config";
2
+ export default defineConfig({ test: { environment: "node" } });