@voidwire/lore 1.3.0 → 1.4.0
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/lib/semantic.ts +62 -3
- package/package.json +1 -1
package/lib/semantic.ts
CHANGED
|
@@ -13,6 +13,44 @@ import { getDatabasePath, openDatabase } from "./db.js";
|
|
|
13
13
|
import { search as keywordSearch, type SearchResult } from "./search.js";
|
|
14
14
|
import { getConfig } from "./config.js";
|
|
15
15
|
|
|
16
|
+
// ─── Embedding Server (server-first, in-process fallback) ────────────────────
|
|
17
|
+
|
|
18
|
+
const EMBED_SERVER = process.env.EMBED_SERVER_URL || "http://localhost:8090";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Try the persistent embedding server first (warm: ~9ms vs 244ms in-process).
|
|
22
|
+
* Returns null on any failure — caller falls back to in-process.
|
|
23
|
+
*/
|
|
24
|
+
async function serverEmbed(
|
|
25
|
+
text: string,
|
|
26
|
+
prefix: string,
|
|
27
|
+
): Promise<number[] | null> {
|
|
28
|
+
try {
|
|
29
|
+
const resp = await fetch(`${EMBED_SERVER}/embed`, {
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers: { "Content-Type": "application/json" },
|
|
32
|
+
body: JSON.stringify({ text, prefix }),
|
|
33
|
+
signal: AbortSignal.timeout(500),
|
|
34
|
+
});
|
|
35
|
+
if (!resp.ok) return null;
|
|
36
|
+
const data = (await resp.json()) as {
|
|
37
|
+
embedding?: number[];
|
|
38
|
+
dims?: number;
|
|
39
|
+
};
|
|
40
|
+
if (
|
|
41
|
+
!Array.isArray(data.embedding) ||
|
|
42
|
+
data.embedding.length !== EMBEDDING_DIM
|
|
43
|
+
) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
return data.embedding;
|
|
47
|
+
} catch {
|
|
48
|
+
return null; // Server not running or timed out — fall back silently
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
53
|
+
|
|
16
54
|
export interface SemanticResult {
|
|
17
55
|
rowid: number;
|
|
18
56
|
source: string;
|
|
@@ -76,6 +114,11 @@ async function getEmbeddingPipeline(): Promise<EmbeddingPipeline> {
|
|
|
76
114
|
* @returns 768-dimensional embedding vector
|
|
77
115
|
*/
|
|
78
116
|
export async function embedQuery(query: string): Promise<number[]> {
|
|
117
|
+
// Try persistent server first (~9ms warm vs 244ms in-process)
|
|
118
|
+
const serverResult = await serverEmbed(query, "search_query");
|
|
119
|
+
if (serverResult) return serverResult;
|
|
120
|
+
|
|
121
|
+
// Fall back to in-process model loading
|
|
79
122
|
const embedder = await getEmbeddingPipeline();
|
|
80
123
|
|
|
81
124
|
// nomic model requires "search_query: " prefix for queries
|
|
@@ -104,6 +147,11 @@ export async function embedQuery(query: string): Promise<number[]> {
|
|
|
104
147
|
* @returns 768-dimensional embedding vector
|
|
105
148
|
*/
|
|
106
149
|
export async function embedDocument(text: string): Promise<number[]> {
|
|
150
|
+
// Try persistent server first (~9ms warm vs 244ms in-process)
|
|
151
|
+
const serverResult = await serverEmbed(text, "search_document");
|
|
152
|
+
if (serverResult) return serverResult;
|
|
153
|
+
|
|
154
|
+
// Fall back to in-process model loading
|
|
107
155
|
const embedder = await getEmbeddingPipeline();
|
|
108
156
|
|
|
109
157
|
const prefixedText = `search_document: ${text}`;
|
|
@@ -131,12 +179,23 @@ export async function embedDocument(text: string): Promise<number[]> {
|
|
|
131
179
|
export async function embedDocuments(texts: string[]): Promise<number[][]> {
|
|
132
180
|
if (texts.length === 0) return [];
|
|
133
181
|
|
|
134
|
-
const embedder = await getEmbeddingPipeline();
|
|
135
182
|
const results: number[][] = [];
|
|
136
183
|
|
|
137
|
-
//
|
|
138
|
-
|
|
184
|
+
// Try persistent server first for each document
|
|
185
|
+
let serverAvailable = true;
|
|
139
186
|
for (const text of texts) {
|
|
187
|
+
if (serverAvailable) {
|
|
188
|
+
const serverResult = await serverEmbed(text, "search_document");
|
|
189
|
+
if (serverResult) {
|
|
190
|
+
results.push(serverResult);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
// Server failed — stop trying and fall back for remaining
|
|
194
|
+
serverAvailable = false;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Fall back to in-process
|
|
198
|
+
const embedder = await getEmbeddingPipeline();
|
|
140
199
|
const prefixedText = `search_document: ${text}`;
|
|
141
200
|
const output = await embedder(prefixedText, {
|
|
142
201
|
pooling: "mean",
|