@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.
- package/CHANGELOG.md +11 -0
- package/README.md +185 -0
- package/dist/api/index.d.ts +2 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +2 -0
- package/dist/api/index.js.map +1 -0
- package/dist/api/route-handler.d.ts +53 -0
- package/dist/api/route-handler.d.ts.map +1 -0
- package/dist/api/route-handler.js +135 -0
- package/dist/api/route-handler.js.map +1 -0
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +2 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/search-client.d.ts +12 -0
- package/dist/client/search-client.d.ts.map +1 -0
- package/dist/client/search-client.js +72 -0
- package/dist/client/search-client.js.map +1 -0
- package/dist/debug.d.ts +2 -0
- package/dist/debug.d.ts.map +1 -0
- package/dist/debug.js +12 -0
- package/dist/debug.js.map +1 -0
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +2 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/useSearch.d.ts +26 -0
- package/dist/hooks/useSearch.d.ts.map +1 -0
- package/dist/hooks/useSearch.js +73 -0
- package/dist/hooks/useSearch.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/indexing/content-extractor.d.ts +20 -0
- package/dist/indexing/content-extractor.d.ts.map +1 -0
- package/dist/indexing/content-extractor.js +81 -0
- package/dist/indexing/content-extractor.js.map +1 -0
- package/dist/indexing/document-builder.d.ts +33 -0
- package/dist/indexing/document-builder.d.ts.map +1 -0
- package/dist/indexing/document-builder.js +117 -0
- package/dist/indexing/document-builder.js.map +1 -0
- package/dist/indexing/index.d.ts +4 -0
- package/dist/indexing/index.d.ts.map +1 -0
- package/dist/indexing/index.js +4 -0
- package/dist/indexing/index.js.map +1 -0
- package/dist/indexing/rebuild.d.ts +19 -0
- package/dist/indexing/rebuild.d.ts.map +1 -0
- package/dist/indexing/rebuild.js +133 -0
- package/dist/indexing/rebuild.js.map +1 -0
- package/dist/types.d.ts +112 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/webhook/handler.d.ts +31 -0
- package/dist/webhook/handler.d.ts.map +1 -0
- package/dist/webhook/handler.js +133 -0
- package/dist/webhook/handler.js.map +1 -0
- package/dist/webhook/index.d.ts +2 -0
- package/dist/webhook/index.d.ts.map +1 -0
- package/dist/webhook/index.js +2 -0
- package/dist/webhook/index.js.map +1 -0
- package/package.json +70 -0
package/CHANGELOG.md
ADDED
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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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"}
|
package/dist/debug.d.ts
ADDED
|
@@ -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 @@
|
|
|
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 @@
|
|
|
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"}
|