@openspecui/search 1.1.0 → 2.3.5
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/index.mjs
CHANGED
|
@@ -1,5 +1,87 @@
|
|
|
1
|
-
import { a as SearchHitSchema, c as SearchWorkerResponseSchema,
|
|
1
|
+
import { a as SearchHitSchema, c as SearchWorkerResponseSchema, i as SearchDocumentSchema, n as buildWebWorkerSource, o as SearchQuerySchema, r as SearchDocumentKindSchema, s as SearchWorkerRequestSchema } from "./worker-source-CMPZlh9-.mjs";
|
|
2
2
|
|
|
3
|
+
//#region src/engine.ts
|
|
4
|
+
const DEFAULT_LIMIT = 50;
|
|
5
|
+
const MAX_LIMIT = 200;
|
|
6
|
+
const SNIPPET_SIZE = 180;
|
|
7
|
+
function normalizeText(input) {
|
|
8
|
+
return input.toLowerCase().replace(/\s+/g, " ").trim();
|
|
9
|
+
}
|
|
10
|
+
function splitTerms(query) {
|
|
11
|
+
return normalizeText(query).split(" ").map((term) => term.trim()).filter((term) => term.length > 0);
|
|
12
|
+
}
|
|
13
|
+
function toSearchIndexDocument(doc) {
|
|
14
|
+
return {
|
|
15
|
+
...doc,
|
|
16
|
+
normalizedTitle: normalizeText(doc.title),
|
|
17
|
+
normalizedPath: normalizeText(doc.path),
|
|
18
|
+
normalizedContent: normalizeText(doc.content)
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function buildSearchIndex(docs) {
|
|
22
|
+
return { documents: docs.map(toSearchIndexDocument) };
|
|
23
|
+
}
|
|
24
|
+
function resolveLimit(limit) {
|
|
25
|
+
if (typeof limit !== "number" || Number.isNaN(limit)) return DEFAULT_LIMIT;
|
|
26
|
+
return Math.min(MAX_LIMIT, Math.max(1, Math.trunc(limit)));
|
|
27
|
+
}
|
|
28
|
+
function isDocumentMatch(doc, terms) {
|
|
29
|
+
return terms.every((term) => doc.normalizedTitle.includes(term) || doc.normalizedPath.includes(term) || doc.normalizedContent.includes(term));
|
|
30
|
+
}
|
|
31
|
+
function scoreDocument(doc, terms) {
|
|
32
|
+
let score = 0;
|
|
33
|
+
for (const term of terms) {
|
|
34
|
+
if (doc.normalizedTitle.includes(term)) score += 30;
|
|
35
|
+
if (doc.normalizedPath.includes(term)) score += 20;
|
|
36
|
+
const contentIdx = doc.normalizedContent.indexOf(term);
|
|
37
|
+
if (contentIdx >= 0) {
|
|
38
|
+
score += 8;
|
|
39
|
+
if (contentIdx < 160) score += 4;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return score;
|
|
43
|
+
}
|
|
44
|
+
function createSnippet(content, terms) {
|
|
45
|
+
const source = content.trim();
|
|
46
|
+
if (!source) return "";
|
|
47
|
+
const normalizedSource = normalizeText(source);
|
|
48
|
+
let matchIndex = -1;
|
|
49
|
+
for (const term of terms) {
|
|
50
|
+
const idx = normalizedSource.indexOf(term);
|
|
51
|
+
if (idx >= 0 && (matchIndex < 0 || idx < matchIndex)) matchIndex = idx;
|
|
52
|
+
}
|
|
53
|
+
if (matchIndex < 0) return source.slice(0, SNIPPET_SIZE);
|
|
54
|
+
const start = Math.max(0, matchIndex - Math.floor(SNIPPET_SIZE / 3));
|
|
55
|
+
const end = Math.min(source.length, start + SNIPPET_SIZE);
|
|
56
|
+
const prefix = start > 0 ? "..." : "";
|
|
57
|
+
const suffix = end < source.length ? "..." : "";
|
|
58
|
+
return `${prefix}${source.slice(start, end)}${suffix}`;
|
|
59
|
+
}
|
|
60
|
+
function searchIndex(index, query) {
|
|
61
|
+
const terms = splitTerms(query.query);
|
|
62
|
+
if (terms.length === 0) return [];
|
|
63
|
+
const hits = [];
|
|
64
|
+
for (const doc of index.documents) {
|
|
65
|
+
if (!isDocumentMatch(doc, terms)) continue;
|
|
66
|
+
hits.push({
|
|
67
|
+
documentId: doc.id,
|
|
68
|
+
kind: doc.kind,
|
|
69
|
+
title: doc.title,
|
|
70
|
+
href: doc.href,
|
|
71
|
+
path: doc.path,
|
|
72
|
+
score: scoreDocument(doc, terms),
|
|
73
|
+
snippet: createSnippet(doc.content, terms),
|
|
74
|
+
updatedAt: doc.updatedAt
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
hits.sort((a, b) => {
|
|
78
|
+
if (b.score !== a.score) return b.score - a.score;
|
|
79
|
+
return b.updatedAt - a.updatedAt;
|
|
80
|
+
});
|
|
81
|
+
return hits.slice(0, resolveLimit(query.limit));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
//#endregion
|
|
3
85
|
//#region src/webworker-provider.ts
|
|
4
86
|
function requestId() {
|
|
5
87
|
return Math.random().toString(36).slice(2);
|
package/dist/node.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as SearchHitSchema, c as SearchWorkerResponseSchema, t as buildNodeWorkerSource } from "./worker-source-
|
|
1
|
+
import { a as SearchHitSchema, c as SearchWorkerResponseSchema, t as buildNodeWorkerSource } from "./worker-source-CMPZlh9-.mjs";
|
|
2
2
|
import { Worker } from "node:worker_threads";
|
|
3
3
|
|
|
4
4
|
//#region src/node-worker-provider.ts
|
|
@@ -1,87 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
|
-
//#region src/engine.ts
|
|
4
|
-
const DEFAULT_LIMIT = 50;
|
|
5
|
-
const MAX_LIMIT = 200;
|
|
6
|
-
const SNIPPET_SIZE = 180;
|
|
7
|
-
function normalizeText(input) {
|
|
8
|
-
return input.toLowerCase().replace(/\s+/g, " ").trim();
|
|
9
|
-
}
|
|
10
|
-
function splitTerms(query) {
|
|
11
|
-
return normalizeText(query).split(" ").map((term) => term.trim()).filter((term) => term.length > 0);
|
|
12
|
-
}
|
|
13
|
-
function toSearchIndexDocument(doc) {
|
|
14
|
-
return {
|
|
15
|
-
...doc,
|
|
16
|
-
normalizedTitle: normalizeText(doc.title),
|
|
17
|
-
normalizedPath: normalizeText(doc.path),
|
|
18
|
-
normalizedContent: normalizeText(doc.content)
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
function buildSearchIndex(docs) {
|
|
22
|
-
return { documents: docs.map(toSearchIndexDocument) };
|
|
23
|
-
}
|
|
24
|
-
function resolveLimit(limit) {
|
|
25
|
-
if (typeof limit !== "number" || Number.isNaN(limit)) return DEFAULT_LIMIT;
|
|
26
|
-
return Math.min(MAX_LIMIT, Math.max(1, Math.trunc(limit)));
|
|
27
|
-
}
|
|
28
|
-
function isDocumentMatch(doc, terms) {
|
|
29
|
-
return terms.every((term) => doc.normalizedTitle.includes(term) || doc.normalizedPath.includes(term) || doc.normalizedContent.includes(term));
|
|
30
|
-
}
|
|
31
|
-
function scoreDocument(doc, terms) {
|
|
32
|
-
let score = 0;
|
|
33
|
-
for (const term of terms) {
|
|
34
|
-
if (doc.normalizedTitle.includes(term)) score += 30;
|
|
35
|
-
if (doc.normalizedPath.includes(term)) score += 20;
|
|
36
|
-
const contentIdx = doc.normalizedContent.indexOf(term);
|
|
37
|
-
if (contentIdx >= 0) {
|
|
38
|
-
score += 8;
|
|
39
|
-
if (contentIdx < 160) score += 4;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
return score;
|
|
43
|
-
}
|
|
44
|
-
function createSnippet(content, terms) {
|
|
45
|
-
const source = content.trim();
|
|
46
|
-
if (!source) return "";
|
|
47
|
-
const normalizedSource = normalizeText(source);
|
|
48
|
-
let matchIndex = -1;
|
|
49
|
-
for (const term of terms) {
|
|
50
|
-
const idx = normalizedSource.indexOf(term);
|
|
51
|
-
if (idx >= 0 && (matchIndex < 0 || idx < matchIndex)) matchIndex = idx;
|
|
52
|
-
}
|
|
53
|
-
if (matchIndex < 0) return source.slice(0, SNIPPET_SIZE);
|
|
54
|
-
const start = Math.max(0, matchIndex - Math.floor(SNIPPET_SIZE / 3));
|
|
55
|
-
const end = Math.min(source.length, start + SNIPPET_SIZE);
|
|
56
|
-
const prefix = start > 0 ? "..." : "";
|
|
57
|
-
const suffix = end < source.length ? "..." : "";
|
|
58
|
-
return `${prefix}${source.slice(start, end)}${suffix}`;
|
|
59
|
-
}
|
|
60
|
-
function searchIndex(index, query) {
|
|
61
|
-
const terms = splitTerms(query.query);
|
|
62
|
-
if (terms.length === 0) return [];
|
|
63
|
-
const hits = [];
|
|
64
|
-
for (const doc of index.documents) {
|
|
65
|
-
if (!isDocumentMatch(doc, terms)) continue;
|
|
66
|
-
hits.push({
|
|
67
|
-
documentId: doc.id,
|
|
68
|
-
kind: doc.kind,
|
|
69
|
-
title: doc.title,
|
|
70
|
-
href: doc.href,
|
|
71
|
-
path: doc.path,
|
|
72
|
-
score: scoreDocument(doc, terms),
|
|
73
|
-
snippet: createSnippet(doc.content, terms),
|
|
74
|
-
updatedAt: doc.updatedAt
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
hits.sort((a, b) => {
|
|
78
|
-
if (b.score !== a.score) return b.score - a.score;
|
|
79
|
-
return b.updatedAt - a.updatedAt;
|
|
80
|
-
});
|
|
81
|
-
return hits.slice(0, resolveLimit(query.limit));
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
//#endregion
|
|
85
3
|
//#region src/protocol.ts
|
|
86
4
|
const SearchDocumentKindSchema = z.enum([
|
|
87
5
|
"spec",
|
|
@@ -155,15 +73,128 @@ const sharedRuntimeSource = String.raw`
|
|
|
155
73
|
const DEFAULT_LIMIT = 50;
|
|
156
74
|
const MAX_LIMIT = 200;
|
|
157
75
|
const SNIPPET_SIZE = 180;
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
76
|
+
|
|
77
|
+
function normalizeText(input) {
|
|
78
|
+
return String(input || '')
|
|
79
|
+
.toLowerCase()
|
|
80
|
+
.replace(/\s+/g, ' ')
|
|
81
|
+
.trim();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function splitTerms(query) {
|
|
85
|
+
return normalizeText(query)
|
|
86
|
+
.split(' ')
|
|
87
|
+
.map((term) => term.trim())
|
|
88
|
+
.filter((term) => term.length > 0);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function toSearchIndexDocument(doc) {
|
|
92
|
+
return {
|
|
93
|
+
id: doc.id,
|
|
94
|
+
kind: doc.kind,
|
|
95
|
+
title: doc.title,
|
|
96
|
+
href: doc.href,
|
|
97
|
+
path: doc.path,
|
|
98
|
+
content: doc.content,
|
|
99
|
+
updatedAt: doc.updatedAt,
|
|
100
|
+
normalizedTitle: normalizeText(doc.title),
|
|
101
|
+
normalizedPath: normalizeText(doc.path),
|
|
102
|
+
normalizedContent: normalizeText(doc.content),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function buildSearchIndex(docs) {
|
|
107
|
+
return {
|
|
108
|
+
documents: Array.isArray(docs) ? docs.map(toSearchIndexDocument) : [],
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function resolveLimit(limit) {
|
|
113
|
+
if (typeof limit !== 'number' || Number.isNaN(limit)) return DEFAULT_LIMIT;
|
|
114
|
+
return Math.min(MAX_LIMIT, Math.max(1, Math.trunc(limit)));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function isDocumentMatch(doc, terms) {
|
|
118
|
+
return terms.every(
|
|
119
|
+
(term) =>
|
|
120
|
+
doc.normalizedTitle.includes(term) ||
|
|
121
|
+
doc.normalizedPath.includes(term) ||
|
|
122
|
+
doc.normalizedContent.includes(term)
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function scoreDocument(doc, terms) {
|
|
127
|
+
let score = 0;
|
|
128
|
+
|
|
129
|
+
for (const term of terms) {
|
|
130
|
+
if (doc.normalizedTitle.includes(term)) score += 30;
|
|
131
|
+
if (doc.normalizedPath.includes(term)) score += 20;
|
|
132
|
+
|
|
133
|
+
const contentIdx = doc.normalizedContent.indexOf(term);
|
|
134
|
+
if (contentIdx >= 0) {
|
|
135
|
+
score += 8;
|
|
136
|
+
if (contentIdx < 160) score += 4;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return score;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function createSnippet(content, terms) {
|
|
144
|
+
const source = String(content || '').trim();
|
|
145
|
+
if (!source) return '';
|
|
146
|
+
|
|
147
|
+
const normalizedSource = normalizeText(source);
|
|
148
|
+
let matchIndex = -1;
|
|
149
|
+
|
|
150
|
+
for (const term of terms) {
|
|
151
|
+
const idx = normalizedSource.indexOf(term);
|
|
152
|
+
if (idx >= 0 && (matchIndex < 0 || idx < matchIndex)) {
|
|
153
|
+
matchIndex = idx;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (matchIndex < 0) {
|
|
158
|
+
return source.slice(0, SNIPPET_SIZE);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const start = Math.max(0, matchIndex - Math.floor(SNIPPET_SIZE / 3));
|
|
162
|
+
const end = Math.min(source.length, start + SNIPPET_SIZE);
|
|
163
|
+
|
|
164
|
+
const prefix = start > 0 ? '...' : '';
|
|
165
|
+
const suffix = end < source.length ? '...' : '';
|
|
166
|
+
return prefix + source.slice(start, end) + suffix;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function searchIndex(index, query) {
|
|
170
|
+
const terms = splitTerms(query && query.query ? query.query : '');
|
|
171
|
+
if (terms.length === 0) return [];
|
|
172
|
+
|
|
173
|
+
const hits = [];
|
|
174
|
+
|
|
175
|
+
for (const doc of index.documents) {
|
|
176
|
+
if (!isDocumentMatch(doc, terms)) continue;
|
|
177
|
+
|
|
178
|
+
hits.push({
|
|
179
|
+
documentId: doc.id,
|
|
180
|
+
kind: doc.kind,
|
|
181
|
+
title: doc.title,
|
|
182
|
+
href: doc.href,
|
|
183
|
+
path: doc.path,
|
|
184
|
+
score: scoreDocument(doc, terms),
|
|
185
|
+
snippet: createSnippet(doc.content, terms),
|
|
186
|
+
updatedAt: doc.updatedAt,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
hits.sort((a, b) => {
|
|
191
|
+
if (b.score !== a.score) return b.score - a.score;
|
|
192
|
+
return b.updatedAt - a.updatedAt;
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
return hits.slice(0, resolveLimit(query ? query.limit : undefined));
|
|
196
|
+
}
|
|
197
|
+
|
|
167
198
|
let index = buildSearchIndex([]);
|
|
168
199
|
|
|
169
200
|
function handleMessage(payload) {
|
|
@@ -202,4 +233,4 @@ function buildNodeWorkerSource() {
|
|
|
202
233
|
}
|
|
203
234
|
|
|
204
235
|
//#endregion
|
|
205
|
-
export { SearchHitSchema as a, SearchWorkerResponseSchema as c,
|
|
236
|
+
export { SearchHitSchema as a, SearchWorkerResponseSchema as c, SearchDocumentSchema as i, buildWebWorkerSource as n, SearchQuerySchema as o, SearchDocumentKindSchema as r, SearchWorkerRequestSchema as s, buildNodeWorkerSource as t };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openspecui/search",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.3.5",
|
|
4
4
|
"description": "Shared search engine and worker providers for OpenSpecUI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.mjs",
|
|
@@ -18,22 +18,26 @@
|
|
|
18
18
|
"files": [
|
|
19
19
|
"dist"
|
|
20
20
|
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsdown src/index.ts src/node.ts --format esm --dts",
|
|
23
|
+
"dev": "tsdown src/index.ts src/node.ts --format esm --dts --watch",
|
|
24
|
+
"typecheck": "tsc --noEmit",
|
|
25
|
+
"test": "vitest run",
|
|
26
|
+
"test:watch": "vitest"
|
|
27
|
+
},
|
|
21
28
|
"dependencies": {
|
|
22
29
|
"zod": "^3.24.1"
|
|
23
30
|
},
|
|
24
31
|
"devDependencies": {
|
|
25
32
|
"tsdown": "^0.16.6",
|
|
26
33
|
"typescript": "^5.7.2",
|
|
27
|
-
"vitest": "^
|
|
34
|
+
"vitest": "^4.1.0"
|
|
28
35
|
},
|
|
29
36
|
"peerDependencies": {
|
|
30
37
|
"typescript": "^5.0.0"
|
|
31
38
|
},
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"typecheck": "tsc --noEmit",
|
|
36
|
-
"test": "vitest run",
|
|
37
|
-
"test:watch": "vitest"
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "https://github.com/jixoai/openspecui"
|
|
38
42
|
}
|
|
39
|
-
}
|
|
43
|
+
}
|