@se-studio/search 1.0.1

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 (63) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +185 -0
  3. package/dist/api/index.d.ts +2 -0
  4. package/dist/api/index.d.ts.map +1 -0
  5. package/dist/api/index.js +2 -0
  6. package/dist/api/index.js.map +1 -0
  7. package/dist/api/route-handler.d.ts +53 -0
  8. package/dist/api/route-handler.d.ts.map +1 -0
  9. package/dist/api/route-handler.js +135 -0
  10. package/dist/api/route-handler.js.map +1 -0
  11. package/dist/client/index.d.ts +2 -0
  12. package/dist/client/index.d.ts.map +1 -0
  13. package/dist/client/index.js +2 -0
  14. package/dist/client/index.js.map +1 -0
  15. package/dist/client/search-client.d.ts +12 -0
  16. package/dist/client/search-client.d.ts.map +1 -0
  17. package/dist/client/search-client.js +72 -0
  18. package/dist/client/search-client.js.map +1 -0
  19. package/dist/debug.d.ts +2 -0
  20. package/dist/debug.d.ts.map +1 -0
  21. package/dist/debug.js +12 -0
  22. package/dist/debug.js.map +1 -0
  23. package/dist/hooks/index.d.ts +2 -0
  24. package/dist/hooks/index.d.ts.map +1 -0
  25. package/dist/hooks/index.js +2 -0
  26. package/dist/hooks/index.js.map +1 -0
  27. package/dist/hooks/useSearch.d.ts +26 -0
  28. package/dist/hooks/useSearch.d.ts.map +1 -0
  29. package/dist/hooks/useSearch.js +73 -0
  30. package/dist/hooks/useSearch.js.map +1 -0
  31. package/dist/index.d.ts +2 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +2 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/indexing/content-extractor.d.ts +20 -0
  36. package/dist/indexing/content-extractor.d.ts.map +1 -0
  37. package/dist/indexing/content-extractor.js +81 -0
  38. package/dist/indexing/content-extractor.js.map +1 -0
  39. package/dist/indexing/document-builder.d.ts +33 -0
  40. package/dist/indexing/document-builder.d.ts.map +1 -0
  41. package/dist/indexing/document-builder.js +117 -0
  42. package/dist/indexing/document-builder.js.map +1 -0
  43. package/dist/indexing/index.d.ts +4 -0
  44. package/dist/indexing/index.d.ts.map +1 -0
  45. package/dist/indexing/index.js +4 -0
  46. package/dist/indexing/index.js.map +1 -0
  47. package/dist/indexing/rebuild.d.ts +19 -0
  48. package/dist/indexing/rebuild.d.ts.map +1 -0
  49. package/dist/indexing/rebuild.js +133 -0
  50. package/dist/indexing/rebuild.js.map +1 -0
  51. package/dist/types.d.ts +112 -0
  52. package/dist/types.d.ts.map +1 -0
  53. package/dist/types.js +2 -0
  54. package/dist/types.js.map +1 -0
  55. package/dist/webhook/handler.d.ts +31 -0
  56. package/dist/webhook/handler.d.ts.map +1 -0
  57. package/dist/webhook/handler.js +133 -0
  58. package/dist/webhook/handler.js.map +1 -0
  59. package/dist/webhook/index.d.ts +2 -0
  60. package/dist/webhook/index.d.ts.map +1 -0
  61. package/dist/webhook/index.js +2 -0
  62. package/dist/webhook/index.js.map +1 -0
  63. package/package.json +70 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,11 @@
1
+ # @se-studio/search
2
+
3
+ ## 1.0.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Bulk version bump: patch for all packages
8
+ - Updated dependencies
9
+ - @se-studio/contentful-rest-api@1.0.90
10
+ - @se-studio/core-data-types@1.0.90
11
+ - @se-studio/markdown-renderer@1.0.54
package/README.md ADDED
@@ -0,0 +1,185 @@
1
+ # @se-studio/search
2
+
3
+ AI-powered site search for Next.js marketing sites using [Upstash Search](https://upstash.com/docs/search/overall/whatisupstashsearch). Combines semantic and full-text search with zero infrastructure to manage.
4
+
5
+ ## Overview
6
+
7
+ This package provides:
8
+
9
+ - **Search client** – typed wrapper around `@upstash/search` with automatic batch handling
10
+ - **Content indexing** – extracts searchable text from CMS content using `MarkdownConverter`
11
+ - **Webhook handler** – incremental index updates on Contentful publish/delete
12
+ - **Full rebuild** – enumerates all content and re-indexes in batches
13
+ - **API route factories** – drop-in Next.js route handlers for search and rebuild
14
+ - **Client hook** – `useSearch()` for building search UIs with debouncing
15
+
16
+ ## Setup
17
+
18
+ ### 1. Install
19
+
20
+ The package is a workspace dependency. Add it to your app's `package.json`:
21
+
22
+ ```json
23
+ {
24
+ "dependencies": {
25
+ "@se-studio/search": "workspace:*"
26
+ }
27
+ }
28
+ ```
29
+
30
+ ### 2. Create an Upstash Search database
31
+
32
+ Go to [console.upstash.com/search](https://console.upstash.com/search) and create a database. Copy the REST URL and token.
33
+
34
+ ### 3. Environment variables
35
+
36
+ Add to your `.env.local`:
37
+
38
+ ```bash
39
+ UPSTASH_SEARCH_REST_URL=https://your-search-url.upstash.io
40
+ UPSTASH_SEARCH_REST_TOKEN=your-token
41
+ ```
42
+
43
+ ### 4. Search config
44
+
45
+ Create `src/lib/search-config.ts`:
46
+
47
+ ```typescript
48
+ import 'server-only';
49
+ import type { SearchIndexingConfig } from '@se-studio/search';
50
+
51
+ export const searchIndexingConfig: SearchIndexingConfig = {
52
+ searchIndex: {
53
+ connection: {
54
+ url: process.env.UPSTASH_SEARCH_REST_URL ?? '',
55
+ token: process.env.UPSTASH_SEARCH_REST_TOKEN ?? '',
56
+ },
57
+ publishedIndexName: 'published',
58
+ previewIndexName: 'preview',
59
+ },
60
+ contentTypes: [
61
+ { type: 'page', enabled: true },
62
+ { type: 'article', enabled: true },
63
+ { type: 'person', enabled: false },
64
+ ],
65
+ indexComponents: true,
66
+ respectIndexedFlag: true,
67
+ respectHiddenFlag: true,
68
+ };
69
+ ```
70
+
71
+ ### 5. Search API route
72
+
73
+ Create `src/app/api/search/route.ts`:
74
+
75
+ ```typescript
76
+ import { createSearchApiHandler } from '@se-studio/search/api';
77
+ import { buildInformation } from '@/lib/converter-context';
78
+ import { searchIndexingConfig } from '@/lib/search-config';
79
+
80
+ export const GET = createSearchApiHandler({
81
+ searchConfig: searchIndexingConfig.searchIndex.connection,
82
+ publishedIndexName: searchIndexingConfig.searchIndex.publishedIndexName,
83
+ previewIndexName: searchIndexingConfig.searchIndex.previewIndexName,
84
+ isPreview: buildInformation.preview ?? false,
85
+ });
86
+ ```
87
+
88
+ ### 6. Rebuild API route
89
+
90
+ Create `src/app/api/search/rebuild/route.ts`:
91
+
92
+ ```typescript
93
+ import { createSearchClient } from '@se-studio/search/client';
94
+ import { rebuildSearchIndex } from '@se-studio/search/indexing';
95
+ import { createRebuildApiHandler } from '@se-studio/search/api';
96
+ import { buildOptions, getContentfulConfig } from '@/lib/cms-server';
97
+ import { customerName, license } from '@/lib/constants';
98
+ import { buildInformation, converterContext } from '@/lib/converter-context';
99
+ import { searchIndexingConfig } from '@/lib/search-config';
100
+ import { baseUrl, revalidationSecret } from '@/lib/server-config';
101
+
102
+ const isPreview = buildInformation.preview ?? false;
103
+
104
+ export const POST = createRebuildApiHandler({
105
+ rebuildSecret: revalidationSecret ?? '',
106
+ isPreview,
107
+ rebuildFn: () => {
108
+ const config = getContentfulConfig(isPreview);
109
+ const client = createSearchClient(searchIndexingConfig.searchIndex.connection);
110
+ const indexName = isPreview
111
+ ? searchIndexingConfig.searchIndex.previewIndexName
112
+ : searchIndexingConfig.searchIndex.publishedIndexName;
113
+
114
+ return rebuildSearchIndex({
115
+ client,
116
+ indexName,
117
+ indexingConfig: searchIndexingConfig,
118
+ converterContext,
119
+ contentfulConfig: config,
120
+ fetchOptions: buildOptions({ preview: isPreview }),
121
+ urlCalculators: converterContext.urlCalculators,
122
+ siteConfig: { canonicalBaseUrl: baseUrl, source: customerName, license },
123
+ });
124
+ },
125
+ });
126
+ ```
127
+
128
+ ### 7. Webhook integration
129
+
130
+ Update your `src/app/api/revalidate/route.ts` to call the search webhook handler after cache revalidation. See the example-brightline app for the full pattern.
131
+
132
+ ### 8. Client-side search
133
+
134
+ ```tsx
135
+ 'use client';
136
+ import { useSearch } from '@se-studio/search/hooks';
137
+
138
+ export function SearchPage() {
139
+ const { query, setQuery, results, isLoading, error, totalCount } = useSearch();
140
+
141
+ return (
142
+ <div>
143
+ <input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Search..." />
144
+ {isLoading && <p>Searching...</p>}
145
+ {error && <p>Error: {error}</p>}
146
+ {results.map((r) => (
147
+ <a key={r.id} href={r.metadata.href}>
148
+ <h3>{r.content.title}</h3>
149
+ <p>{r.content.description}</p>
150
+ </a>
151
+ ))}
152
+ </div>
153
+ );
154
+ }
155
+ ```
156
+
157
+ ## Triggering a full rebuild
158
+
159
+ ```bash
160
+ curl -X POST https://your-site.com/api/search/rebuild \
161
+ -H "Authorization: Bearer YOUR_REVALIDATION_SECRET"
162
+ ```
163
+
164
+ The index to rebuild (published vs preview) is determined by the app's own `isPreview` flag
165
+ set at initialization time in `createRebuildApiHandler`.
166
+
167
+ ## Architecture
168
+
169
+ - **Single Upstash database, two indexes**: `published` and `preview`
170
+ - **Text extraction**: Uses `MarkdownConverter` to deeply extract text from page components, then strips markdown formatting
171
+ - **Content truncation**: Body text is truncated to ~4,000 chars (Upstash limit)
172
+ - **Batch upsert**: Documents are upserted in batches of 100 (Upstash API limit)
173
+ - **Webhook-driven**: Incremental updates on Contentful publish/delete events
174
+ - **Flags**: Respects `indexed` and `hidden` fields on content entries
175
+
176
+ ## Subpath Exports
177
+
178
+ | Import | Purpose |
179
+ |--------|---------|
180
+ | `@se-studio/search` | Types only |
181
+ | `@se-studio/search/client` | `createSearchClient()` |
182
+ | `@se-studio/search/indexing` | `rebuildSearchIndex()`, `buildSearchDocument()` |
183
+ | `@se-studio/search/webhook` | `createSearchWebhookHandler()` |
184
+ | `@se-studio/search/api` | `createSearchApiHandler()`, `createRebuildApiHandler()` |
185
+ | `@se-studio/search/hooks` | `useSearch()` |
@@ -0,0 +1,2 @@
1
+ export { createRebuildApiHandler, createSearchApiHandler, deduplicateChunks, type RebuildApiConfig, type SearchApiConfig, } from './route-handler';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/api/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,uBAAuB,EACvB,sBAAsB,EACtB,iBAAiB,EACjB,KAAK,gBAAgB,EACrB,KAAK,eAAe,GACrB,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { createRebuildApiHandler, createSearchApiHandler, deduplicateChunks, } from './route-handler';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/api/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,uBAAuB,EACvB,sBAAsB,EACtB,iBAAiB,GAGlB,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,53 @@
1
+ import type { RebuildResult, SearchConfig, SearchResult } from '../types';
2
+ /**
3
+ * Groups results by base entry ID and keeps the highest-scoring chunk per entry.
4
+ * Results are returned in score-descending order.
5
+ */
6
+ export declare function deduplicateChunks(results: SearchResult[]): SearchResult[];
7
+ export interface SearchApiConfig {
8
+ searchConfig: SearchConfig;
9
+ publishedIndexName: string;
10
+ previewIndexName: string;
11
+ /** When true, queries the preview index instead of published. */
12
+ isPreview?: boolean;
13
+ }
14
+ /**
15
+ * Creates a Next.js GET route handler for search queries.
16
+ *
17
+ * Query params:
18
+ * - `q` -- search query (required)
19
+ * - `limit` -- max results (default: 20)
20
+ * - `filter` -- Upstash metadata filter expression
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * // app/api/search/route.ts
25
+ * import { createSearchApiHandler } from '@se-studio/search/api';
26
+ * export const GET = createSearchApiHandler({ ... });
27
+ * ```
28
+ */
29
+ export declare function createSearchApiHandler(config: SearchApiConfig): (request: Request) => Promise<Response>;
30
+ export interface RebuildApiConfig {
31
+ /** Secret used to authenticate rebuild requests (typically REVALIDATION_SECRET). */
32
+ rebuildSecret: string;
33
+ /** Whether this app instance is running in preview/draft mode. */
34
+ isPreview: boolean;
35
+ /** Function that performs the actual rebuild and returns stats. */
36
+ rebuildFn: () => Promise<RebuildResult>;
37
+ }
38
+ /**
39
+ * Creates a Next.js POST route handler for triggering a full search index rebuild.
40
+ * Protected by `Authorization: Bearer <secret>`.
41
+ *
42
+ * The index to rebuild (published vs preview) is determined by the app's
43
+ * own preview state via `isPreview`, not by a query parameter.
44
+ *
45
+ * @example
46
+ * ```ts
47
+ * // app/api/search/rebuild/route.ts
48
+ * import { createRebuildApiHandler } from '@se-studio/search/api';
49
+ * export const POST = createRebuildApiHandler({ ... });
50
+ * ```
51
+ */
52
+ export declare function createRebuildApiHandler(config: RebuildApiConfig): (request: Request) => Promise<Response>;
53
+ //# sourceMappingURL=route-handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route-handler.d.ts","sourceRoot":"","sources":["../../src/api/route-handler.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAkB,YAAY,EAAE,MAAM,UAAU,CAAC;AAE1F;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,YAAY,EAAE,CAazE;AAED,MAAM,WAAW,eAAe;IAC9B,YAAY,EAAE,YAAY,CAAC;IAC3B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,gBAAgB,EAAE,MAAM,CAAC;IACzB,iEAAiE;IACjE,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,eAAe,IAGxB,SAAS,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC,CAiFzE;AAED,MAAM,WAAW,gBAAgB;IAC/B,oFAAoF;IACpF,aAAa,EAAE,MAAM,CAAC;IACtB,kEAAkE;IAClE,SAAS,EAAE,OAAO,CAAC;IACnB,mEAAmE;IACnE,SAAS,EAAE,MAAM,OAAO,CAAC,aAAa,CAAC,CAAC;CACzC;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,gBAAgB,IACzB,SAAS,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC,CAyB1E"}
@@ -0,0 +1,135 @@
1
+ import { createSearchClient } from '../client/search-client';
2
+ import { debugLog } from '../debug';
3
+ import { baseEntryId } from '../indexing/document-builder';
4
+ /**
5
+ * Groups results by base entry ID and keeps the highest-scoring chunk per entry.
6
+ * Results are returned in score-descending order.
7
+ */
8
+ export function deduplicateChunks(results) {
9
+ const seen = new Map();
10
+ for (const result of results) {
11
+ const entryId = baseEntryId(result.id);
12
+ const existing = seen.get(entryId);
13
+ if (!existing || result.score > existing.score) {
14
+ seen.set(entryId, result);
15
+ }
16
+ }
17
+ return [...seen.values()].sort((a, b) => b.score - a.score);
18
+ }
19
+ /**
20
+ * Creates a Next.js GET route handler for search queries.
21
+ *
22
+ * Query params:
23
+ * - `q` -- search query (required)
24
+ * - `limit` -- max results (default: 20)
25
+ * - `filter` -- Upstash metadata filter expression
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * // app/api/search/route.ts
30
+ * import { createSearchApiHandler } from '@se-studio/search/api';
31
+ * export const GET = createSearchApiHandler({ ... });
32
+ * ```
33
+ */
34
+ export function createSearchApiHandler(config) {
35
+ const client = createSearchClient(config.searchConfig);
36
+ return async function searchHandler(request) {
37
+ const { searchParams } = new URL(request.url);
38
+ const query = (searchParams.get('q') ?? '').trim();
39
+ debugLog('api', `GET /api/search`, { query, params: Object.fromEntries(searchParams) });
40
+ if (!query) {
41
+ debugLog('api', 'Empty query, returning empty results');
42
+ return Response.json({ results: [], query: '', totalCount: 0 }, {
43
+ status: 200,
44
+ });
45
+ }
46
+ const limit = Math.min(Number(searchParams.get('limit')) || 20, 100);
47
+ const filter = searchParams.get('filter') ?? undefined;
48
+ const indexName = config.isPreview ? config.previewIndexName : config.publishedIndexName;
49
+ // Over-fetch to account for multiple chunks per entry, then deduplicate
50
+ const fetchLimit = Math.min(limit * 3, 100);
51
+ debugLog('api', `Querying index "${indexName}"`, {
52
+ query,
53
+ limit,
54
+ fetchLimit,
55
+ filter,
56
+ isPreview: config.isPreview,
57
+ });
58
+ try {
59
+ const response = await client.search(indexName, { query, limit: fetchLimit, filter });
60
+ debugLog('api', `Search returned ${response.totalCount} raw results for "${query}"`);
61
+ debugLog('api', 'Raw chunks (before dedup)', response.results.map((r) => ({
62
+ id: r.id,
63
+ title: r.content.title,
64
+ type: r.metadata.type,
65
+ slug: r.metadata.slug,
66
+ score: r.score,
67
+ chunkIndex: r.metadata.chunkIndex,
68
+ bodySnippet: r.content.body.slice(0, 200),
69
+ })));
70
+ const deduplicated = deduplicateChunks(response.results);
71
+ const trimmed = deduplicated.slice(0, limit);
72
+ debugLog('api', `After dedup: ${deduplicated.length} unique entries, returning ${trimmed.length}`);
73
+ debugLog('api', 'Final results', trimmed.map((r, i) => ({
74
+ rank: i + 1,
75
+ entryId: r.metadata.entryId,
76
+ title: r.content.title,
77
+ type: r.metadata.type,
78
+ slug: r.metadata.slug,
79
+ score: r.score,
80
+ description: r.content.description.slice(0, 150),
81
+ })));
82
+ const deduped = {
83
+ results: trimmed,
84
+ query: response.query,
85
+ totalCount: deduplicated.length,
86
+ };
87
+ return Response.json(deduped, { status: 200 });
88
+ }
89
+ catch (err) {
90
+ console.error('[search] Search query failed:', err);
91
+ debugLog('api', 'Search query threw an error', { error: String(err) });
92
+ return Response.json({ error: 'Search failed' }, { status: 500 });
93
+ }
94
+ };
95
+ }
96
+ /**
97
+ * Creates a Next.js POST route handler for triggering a full search index rebuild.
98
+ * Protected by `Authorization: Bearer <secret>`.
99
+ *
100
+ * The index to rebuild (published vs preview) is determined by the app's
101
+ * own preview state via `isPreview`, not by a query parameter.
102
+ *
103
+ * @example
104
+ * ```ts
105
+ * // app/api/search/rebuild/route.ts
106
+ * import { createRebuildApiHandler } from '@se-studio/search/api';
107
+ * export const POST = createRebuildApiHandler({ ... });
108
+ * ```
109
+ */
110
+ export function createRebuildApiHandler(config) {
111
+ return async function rebuildHandler(request) {
112
+ debugLog('api', 'POST /api/search/rebuild', { isPreview: config.isPreview });
113
+ const authHeader = request.headers.get('Authorization');
114
+ const token = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : null;
115
+ if (!token || token !== config.rebuildSecret) {
116
+ debugLog('api', 'Rebuild: unauthorized (bad or missing token)', {
117
+ hasAuthHeader: !!authHeader,
118
+ hasSecret: !!config.rebuildSecret,
119
+ });
120
+ return Response.json({ error: 'Unauthorized' }, { status: 401 });
121
+ }
122
+ debugLog('api', `Rebuild: authorized, isPreview=${config.isPreview}`);
123
+ try {
124
+ const result = await config.rebuildFn();
125
+ debugLog('api', 'Rebuild complete', result);
126
+ return Response.json(result, { status: 200 });
127
+ }
128
+ catch (err) {
129
+ console.error('[search] Rebuild failed:', err);
130
+ debugLog('api', 'Rebuild threw an error', { error: String(err) });
131
+ return Response.json({ error: 'Rebuild failed' }, { status: 500 });
132
+ }
133
+ };
134
+ }
135
+ //# sourceMappingURL=route-handler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route-handler.js","sourceRoot":"","sources":["../../src/api/route-handler.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export { createSearchClient, type SearchClient } from './search-client';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,KAAK,YAAY,EAAE,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { createSearchClient } from './search-client';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAqB,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,12 @@
1
+ import type { SearchConfig, SearchDocument, SearchOptions, SearchResponse } from '../types';
2
+ /**
3
+ * Creates a typed Upstash Search client bound to a single database.
4
+ * Use separate index names for published vs preview content.
5
+ */
6
+ export declare function createSearchClient(config: SearchConfig): {
7
+ search(indexName: string, options: SearchOptions): Promise<SearchResponse>;
8
+ upsert(indexName: string, documents: SearchDocument[]): Promise<void>;
9
+ delete(indexName: string, ids: string[]): Promise<void>;
10
+ };
11
+ export type SearchClient = ReturnType<typeof createSearchClient>;
12
+ //# sourceMappingURL=search-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search-client.d.ts","sourceRoot":"","sources":["../../src/client/search-client.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,YAAY,EACZ,cAAc,EAEd,aAAa,EACb,cAAc,EAEf,MAAM,UAAU,CAAC;AAOlB;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,YAAY;sBAY3B,MAAM,WAAW,aAAa,GAAG,OAAO,CAAC,cAAc,CAAC;sBA8BxD,MAAM,aAAa,cAAc,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;sBA6BnD,MAAM,OAAO,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;EAQhE;AAED,MAAM,MAAM,YAAY,GAAG,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC"}
@@ -0,0 +1,72 @@
1
+ import { Search } from '@upstash/search';
2
+ import { debugLog } from '../debug';
3
+ const UPSTASH_BATCH_LIMIT = 100;
4
+ /**
5
+ * Creates a typed Upstash Search client bound to a single database.
6
+ * Use separate index names for published vs preview content.
7
+ */
8
+ export function createSearchClient(config) {
9
+ debugLog('client', 'Creating search client', {
10
+ url: config.url ? `${config.url.slice(0, 30)}...` : '(empty)',
11
+ hasToken: !!config.token,
12
+ });
13
+ const client = new Search({ url: config.url, token: config.token });
14
+ function getIndex(indexName) {
15
+ return client.index(indexName);
16
+ }
17
+ return {
18
+ async search(indexName, options) {
19
+ debugLog('client', `search("${indexName}")`, {
20
+ query: options.query,
21
+ limit: options.limit,
22
+ filter: options.filter,
23
+ });
24
+ const index = getIndex(indexName);
25
+ const results = await index.search({
26
+ query: options.query,
27
+ limit: options.limit ?? 20,
28
+ filter: options.filter,
29
+ semanticWeight: options.semanticWeight,
30
+ });
31
+ debugLog('client', `search("${indexName}") returned ${results.length} raw results`);
32
+ const mapped = results.map((r) => ({
33
+ id: r.id,
34
+ content: r.content,
35
+ metadata: r.metadata,
36
+ score: r.score,
37
+ }));
38
+ return {
39
+ results: mapped,
40
+ query: options.query,
41
+ totalCount: mapped.length,
42
+ };
43
+ },
44
+ async upsert(indexName, documents) {
45
+ const totalBatches = Math.ceil(documents.length / UPSTASH_BATCH_LIMIT);
46
+ debugLog('client', `upsert("${indexName}"): ${documents.length} docs in ${totalBatches} batch(es)`);
47
+ const index = getIndex(indexName);
48
+ for (let i = 0; i < documents.length; i += UPSTASH_BATCH_LIMIT) {
49
+ const batch = documents.slice(i, i + UPSTASH_BATCH_LIMIT);
50
+ const batchNum = Math.floor(i / UPSTASH_BATCH_LIMIT) + 1;
51
+ debugLog('client', `upsert("${indexName}") batch ${batchNum}/${totalBatches}: ${batch.length} docs`, {
52
+ ids: batch.map((d) => d.id),
53
+ });
54
+ await index.upsert(batch.map((doc) => ({
55
+ id: doc.id,
56
+ content: doc.content,
57
+ metadata: doc.metadata,
58
+ })));
59
+ debugLog('client', `upsert("${indexName}") batch ${batchNum}/${totalBatches}: done`);
60
+ }
61
+ },
62
+ async delete(indexName, ids) {
63
+ if (ids.length === 0)
64
+ return;
65
+ debugLog('client', `delete("${indexName}"): ${ids.length} id(s)`, { ids });
66
+ const index = getIndex(indexName);
67
+ await index.delete(ids);
68
+ debugLog('client', `delete("${indexName}"): done`);
69
+ },
70
+ };
71
+ }
72
+ //# sourceMappingURL=search-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search-client.js","sourceRoot":"","sources":["../../src/client/search-client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export declare function debugLog(area: string, message: string, data?: unknown): void;
2
+ //# sourceMappingURL=debug.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"debug.d.ts","sourceRoot":"","sources":["../src/debug.ts"],"names":[],"mappings":"AAEA,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,QAOrE"}
package/dist/debug.js ADDED
@@ -0,0 +1,12 @@
1
+ const isDebug = () => process.env.DEBUG_SEARCH === 'true';
2
+ export function debugLog(area, message, data) {
3
+ if (!isDebug())
4
+ return;
5
+ if (data !== undefined) {
6
+ console.log(`[search:${area}] ${message}`, JSON.stringify(data, null, 2));
7
+ }
8
+ else {
9
+ console.log(`[search:${area}] ${message}`);
10
+ }
11
+ }
12
+ //# sourceMappingURL=debug.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"debug.js","sourceRoot":"","sources":["../src/debug.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,YAAY,KAAK,MAAM,CAAC;AAE1D,MAAM,UAAU,QAAQ,CAAC,IAAY,EAAE,OAAe,EAAE,IAAc;IACpE,IAAI,CAAC,OAAO,EAAE;QAAE,OAAO;IACvB,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,KAAK,OAAO,EAAE,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IAC5E,CAAC;SAAM,CAAC;QACN,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,KAAK,OAAO,EAAE,CAAC,CAAC;IAC7C,CAAC;AACH,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { type UseSearchOptions, type UseSearchReturn, useSearch } from './useSearch';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,gBAAgB,EAAE,KAAK,eAAe,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { useSearch } from './useSearch';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAA+C,SAAS,EAAE,MAAM,aAAa,CAAC"}
@@ -0,0 +1,26 @@
1
+ import type { SearchResult } from '../types';
2
+ export interface UseSearchOptions {
3
+ /** Debounce delay in milliseconds. Default: 300. */
4
+ debounceMs?: number;
5
+ /** Max results per query. Default: 20. */
6
+ limit?: number;
7
+ /** API endpoint path. Default: '/api/search'. */
8
+ apiPath?: string;
9
+ /** Seed the query on first render (e.g. from a URL param). Ignored after mount. */
10
+ initialQuery?: string;
11
+ }
12
+ export interface UseSearchReturn {
13
+ query: string;
14
+ setQuery: (q: string) => void;
15
+ results: SearchResult[];
16
+ isLoading: boolean;
17
+ error: string | null;
18
+ totalCount: number;
19
+ }
20
+ /**
21
+ * Client-side hook for searching via the app's `/api/search` endpoint.
22
+ * Manages query state, debouncing, loading, and error handling.
23
+ * Apps build their own search UI on top of this hook.
24
+ */
25
+ export declare function useSearch(options?: UseSearchOptions): UseSearchReturn;
26
+ //# sourceMappingURL=useSearch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useSearch.d.ts","sourceRoot":"","sources":["../../src/hooks/useSearch.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAkB,YAAY,EAAE,MAAM,UAAU,CAAC;AAE7D,MAAM,WAAW,gBAAgB;IAC/B,oDAAoD;IACpD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,0CAA0C;IAC1C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iDAAiD;IACjD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,mFAAmF;IACnF,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9B,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,eAAe,CA2ErE"}