@wei840222/qmd 2026.8.24 → 2026.9.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.
- package/CHANGELOG.md +8 -0
- package/dist/cli/build-info.json +2 -2
- package/dist/cli/qmd.js +16 -9
- package/dist/db.d.ts +16 -29
- package/dist/db.js +63 -40
- package/dist/hybrid-llm.d.ts +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +5 -1
- package/dist/llm.d.ts +3 -0
- package/dist/llm.js +20 -6
- package/dist/mcp/server.js +3 -1
- package/dist/remote-llm.d.ts +1 -0
- package/dist/remote-llm.js +20 -8
- package/dist/store.d.ts +12 -2
- package/dist/store.js +43 -12
- package/package.json +2 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- Replace `better-sqlite3` with Node.js built-in `node:sqlite`, preventing SQLite runtime symbol collisions when QMD is embedded alongside other `node:sqlite` consumers. The minimum supported Node.js version is now 22.16.0.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Disable HyDE Expansion Control**: Added `--no-hyde` CLI option for `qmd query` and `qmd vsearch`, `includeHyde` parameter to SDK (`store.search`, `store.expandQuery`) and MCP `query` tool, allowing users to disable generating hypothetical document embeddings during query expansion.
|
|
12
|
+
|
|
5
13
|
## [2026.8.23-1] - 2026-08-23
|
|
6
14
|
|
|
7
15
|
### Added
|
package/dist/cli/build-info.json
CHANGED
package/dist/cli/qmd.js
CHANGED
|
@@ -137,12 +137,16 @@ function getStore() {
|
|
|
137
137
|
}
|
|
138
138
|
return store;
|
|
139
139
|
}
|
|
140
|
-
function getDoctorStore() {
|
|
140
|
+
function getDoctorStore(options = {}) {
|
|
141
141
|
if (!store) {
|
|
142
142
|
const dbPath = getDbPath();
|
|
143
|
+
const shouldReconcile = existsSync(dbPath) && options.reconcileConfig !== undefined;
|
|
143
144
|
store = existsSync(dbPath)
|
|
144
|
-
? createStore(dbPath, { readOnly:
|
|
145
|
+
? createStore(dbPath, { readOnly: !shouldReconcile })
|
|
145
146
|
: createStore(":memory:");
|
|
147
|
+
if (shouldReconcile) {
|
|
148
|
+
syncConfigToDb(store.db, options.reconcileConfig);
|
|
149
|
+
}
|
|
146
150
|
}
|
|
147
151
|
return store;
|
|
148
152
|
}
|
|
@@ -2674,6 +2678,7 @@ async function vectorSearch(query, opts, _model = DEFAULT_EMBED_MODEL) {
|
|
|
2674
2678
|
limit: opts.all ? 500 : (opts.limit || 10),
|
|
2675
2679
|
minScore: opts.minScore || 0.3,
|
|
2676
2680
|
expansionContext: opts.intent,
|
|
2681
|
+
includeHyde: opts.includeHyde,
|
|
2677
2682
|
hooks: {
|
|
2678
2683
|
onExpand: (original, expanded) => {
|
|
2679
2684
|
logExpansionTree(original, expanded);
|
|
@@ -2766,6 +2771,7 @@ async function querySearch(query, opts, _embedModel = DEFAULT_EMBED_MODEL, _rera
|
|
|
2766
2771
|
explain: !!opts.explain,
|
|
2767
2772
|
rerankContext: intent,
|
|
2768
2773
|
expansion: opts.expansion,
|
|
2774
|
+
includeHyde: opts.includeHyde,
|
|
2769
2775
|
chunkStrategy: opts.chunkStrategy,
|
|
2770
2776
|
hooks: {
|
|
2771
2777
|
onExpansionDecision: (decision) => {
|
|
@@ -2884,6 +2890,7 @@ function parseCLI() {
|
|
|
2884
2890
|
// Query options
|
|
2885
2891
|
"candidate-limit": { type: "string", short: "C" },
|
|
2886
2892
|
"no-rerank": { type: "boolean", default: false },
|
|
2893
|
+
"no-hyde": { type: "boolean", default: false },
|
|
2887
2894
|
expand: { type: "boolean", default: false },
|
|
2888
2895
|
"no-gpu": { type: "boolean", default: false },
|
|
2889
2896
|
intent: { type: "string" },
|
|
@@ -2960,6 +2967,7 @@ function parseCLI() {
|
|
|
2960
2967
|
lineNumbers: !!values["line-numbers"],
|
|
2961
2968
|
candidateLimit: values["candidate-limit"] ? parseInt(String(values["candidate-limit"]), 10) : undefined,
|
|
2962
2969
|
skipRerank: !!values["no-rerank"],
|
|
2970
|
+
includeHyde: !values["no-hyde"],
|
|
2963
2971
|
explain: !!values.explain,
|
|
2964
2972
|
intent: values.intent,
|
|
2965
2973
|
expansion: values.expand ? "force" : "auto",
|
|
@@ -3459,6 +3467,7 @@ function showHelp() {
|
|
|
3459
3467
|
console.log(" --chunk-strategy <auto|regex> - Chunking mode (default: regex; auto uses AST for code files)");
|
|
3460
3468
|
console.log(" --timeout <minutes> - Embed session cap in minutes (0 = no limit; default 30)");
|
|
3461
3469
|
console.log(" --expand - Force query expansion (auto is the default; lex: skips)");
|
|
3470
|
+
console.log(" --no-hyde - Disable HyDE (hypothetical document) in query expansion");
|
|
3462
3471
|
console.log("");
|
|
3463
3472
|
console.log("Embedding providers & disclosure:");
|
|
3464
3473
|
console.log(" - Local embedding is the default. OpenAI requires explicit provider configuration and OPENAI_API_KEY.");
|
|
@@ -3911,10 +3920,6 @@ async function runDoctorDeviceChecks(nextSteps) {
|
|
|
3911
3920
|
}
|
|
3912
3921
|
}
|
|
3913
3922
|
async function showDoctor() {
|
|
3914
|
-
const storeInstance = getDoctorStore();
|
|
3915
|
-
const db = storeInstance.db;
|
|
3916
|
-
const pkg = readPackageJson();
|
|
3917
|
-
const activeModels = resolveModelsForCli();
|
|
3918
3923
|
let doctorConfig;
|
|
3919
3924
|
try {
|
|
3920
3925
|
doctorConfig = loadConfig();
|
|
@@ -3923,6 +3928,9 @@ async function showDoctor() {
|
|
|
3923
3928
|
// The dedicated index-config check below reports parse errors. Keep the
|
|
3924
3929
|
// remaining diagnostics available by falling back to DB/default config.
|
|
3925
3930
|
}
|
|
3931
|
+
const storeInstance = getDoctorStore({ reconcileConfig: doctorConfig });
|
|
3932
|
+
const db = storeInstance.db;
|
|
3933
|
+
const activeModels = resolveModelsForCli();
|
|
3926
3934
|
const doctorEmbedding = resolveEmbeddingConfig({
|
|
3927
3935
|
config: doctorConfig,
|
|
3928
3936
|
dbConfig: readCanonicalEmbeddingConfig(db),
|
|
@@ -3933,7 +3941,7 @@ async function showDoctor() {
|
|
|
3933
3941
|
const nextSteps = [];
|
|
3934
3942
|
console.log(`${c.bold}QMD Doctor${c.reset}\n`);
|
|
3935
3943
|
console.log(`Index: ${getDbPath()}`);
|
|
3936
|
-
console.log(`Runtime:
|
|
3944
|
+
console.log(`Runtime: node:sqlite`);
|
|
3937
3945
|
try {
|
|
3938
3946
|
const row = db.prepare(`SELECT sqlite_version() AS version`).get();
|
|
3939
3947
|
doctorCheck("SQLite runtime", true, row.version);
|
|
@@ -3941,8 +3949,7 @@ async function showDoctor() {
|
|
|
3941
3949
|
catch (error) {
|
|
3942
3950
|
doctorCheck("SQLite runtime", false, error instanceof Error ? error.message : String(error));
|
|
3943
3951
|
}
|
|
3944
|
-
|
|
3945
|
-
doctorCheck("better-sqlite3 package", true, String(betterSqliteVersion));
|
|
3952
|
+
doctorCheck("node:sqlite", true, process.versions.node);
|
|
3946
3953
|
try {
|
|
3947
3954
|
loadSqliteVec(db);
|
|
3948
3955
|
const row = db.prepare(`SELECT vec_version() AS version`).get();
|
package/dist/db.d.ts
CHANGED
|
@@ -1,39 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* db.ts - SQLite database connection and extension management
|
|
3
3
|
*
|
|
4
|
-
* Provides
|
|
5
|
-
* and sqlite-vec.
|
|
4
|
+
* Provides a synchronous node:sqlite connection with QMD's transaction helper
|
|
5
|
+
* and sqlite-vec extension loading.
|
|
6
6
|
*/
|
|
7
|
-
import
|
|
7
|
+
import { DatabaseSync, type StatementSync } from "node:sqlite";
|
|
8
8
|
export type SQLiteValue = string | number | bigint | Buffer | Uint8Array | Float32Array | null;
|
|
9
9
|
export type SQLiteParams = readonly SQLiteValue[];
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
* Default 120_000 ms outlasts the worst-case batch commit on a multi-GB
|
|
23
|
-
* index. Override with `QMD_SQLITE_BUSY_TIMEOUT` (value in milliseconds; `0`
|
|
24
|
-
* restores the upstream fail-fast behaviour).
|
|
25
|
-
*/
|
|
10
|
+
export type Transaction<TArgs extends unknown[], TResult> = ((...args: TArgs) => TResult) & {
|
|
11
|
+
deferred: (...args: TArgs) => TResult;
|
|
12
|
+
immediate: (...args: TArgs) => TResult;
|
|
13
|
+
exclusive: (...args: TArgs) => TResult;
|
|
14
|
+
};
|
|
15
|
+
/** Synchronous SQLite connection with QMD-compatible transactions. */
|
|
16
|
+
export declare class Database extends DatabaseSync {
|
|
17
|
+
transaction<TArgs extends unknown[], TResult>(operation: (...args: TArgs) => TResult): Transaction<TArgs, TResult>;
|
|
18
|
+
}
|
|
19
|
+
/** Statement type used throughout QMD. */
|
|
20
|
+
export type Statement<T extends SQLiteParams = SQLiteParams> = StatementSync;
|
|
21
|
+
/** Open a writable QMD database using Node's built-in SQLite runtime. */
|
|
26
22
|
export declare function openDatabase(path: string): Database;
|
|
27
23
|
/** Open an existing database without changing journal mode, schema, or user data. */
|
|
28
24
|
export declare function openReadOnlyDatabase(path: string): Database;
|
|
29
|
-
/**
|
|
30
|
-
* Database and Statement types used throughout QMD.
|
|
31
|
-
*/
|
|
32
|
-
export type Database = BetterSqlite3.Database;
|
|
33
|
-
export type Statement<T extends SQLiteParams = SQLiteParams> = BetterSqlite3.Statement<T>;
|
|
34
|
-
/**
|
|
35
|
-
* Load the sqlite-vec extension into a database.
|
|
36
|
-
*
|
|
37
|
-
* Throws with fix instructions when the extension is unavailable.
|
|
38
|
-
*/
|
|
25
|
+
/** Load the sqlite-vec extension into a database. */
|
|
39
26
|
export declare function loadSqliteVec(db: Database): void;
|
package/dist/db.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* db.ts - SQLite database connection and extension management
|
|
3
3
|
*
|
|
4
|
-
* Provides
|
|
5
|
-
* and sqlite-vec.
|
|
4
|
+
* Provides a synchronous node:sqlite connection with QMD's transaction helper
|
|
5
|
+
* and sqlite-vec extension loading.
|
|
6
6
|
*/
|
|
7
|
-
import
|
|
7
|
+
import { DatabaseSync } from "node:sqlite";
|
|
8
8
|
import * as sqliteVec from "sqlite-vec";
|
|
9
|
+
let savepointSequence = 0;
|
|
9
10
|
function isBusyError(err) {
|
|
10
11
|
if (typeof err !== "object" || err === null)
|
|
11
12
|
return false;
|
|
@@ -18,12 +19,57 @@ function isBusyError(err) {
|
|
|
18
19
|
function sleepSync(ms) {
|
|
19
20
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
20
21
|
}
|
|
22
|
+
/** Synchronous SQLite connection with QMD-compatible transactions. */
|
|
23
|
+
export class Database extends DatabaseSync {
|
|
24
|
+
transaction(operation) {
|
|
25
|
+
const execute = (mode, args) => {
|
|
26
|
+
if (!this.isTransaction) {
|
|
27
|
+
this.exec(`BEGIN ${mode}`);
|
|
28
|
+
try {
|
|
29
|
+
const result = operation(...args);
|
|
30
|
+
this.exec("COMMIT");
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
try {
|
|
35
|
+
this.exec("ROLLBACK");
|
|
36
|
+
}
|
|
37
|
+
catch { }
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const savepoint = `qmd_${++savepointSequence}`;
|
|
42
|
+
this.exec(`SAVEPOINT ${savepoint}`);
|
|
43
|
+
try {
|
|
44
|
+
const result = operation(...args);
|
|
45
|
+
this.exec(`RELEASE ${savepoint}`);
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
try {
|
|
50
|
+
this.exec(`ROLLBACK TO ${savepoint}`);
|
|
51
|
+
this.exec(`RELEASE ${savepoint}`);
|
|
52
|
+
}
|
|
53
|
+
catch { }
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const transaction = ((...args) => execute("DEFERRED", args));
|
|
58
|
+
transaction.deferred = (...args) => execute("DEFERRED", args);
|
|
59
|
+
transaction.immediate = (...args) => execute("IMMEDIATE", args);
|
|
60
|
+
transaction.exclusive = (...args) => execute("EXCLUSIVE", args);
|
|
61
|
+
return transaction;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function resolveBusyTimeout() {
|
|
65
|
+
const raw = process.env.QMD_SQLITE_BUSY_TIMEOUT;
|
|
66
|
+
const parsed = raw !== undefined && raw !== "" ? Number(raw) : Number.NaN;
|
|
67
|
+
return Number.isFinite(parsed) && parsed >= 0 ? Math.floor(parsed) : 120_000;
|
|
68
|
+
}
|
|
21
69
|
/**
|
|
22
70
|
* Switch a connection to WAL, retrying on `SQLITE_BUSY` within the busy-timeout
|
|
23
|
-
* budget.
|
|
24
|
-
*
|
|
25
|
-
* cold database throw "database is locked" even with `busy_timeout` set. Once the
|
|
26
|
-
* database is already WAL the pragma is a cheap no-op that does not contend.
|
|
71
|
+
* budget. Migrating the journal needs a brief exclusive lock and does not invoke
|
|
72
|
+
* SQLite's busy handler on every supported runtime.
|
|
27
73
|
*/
|
|
28
74
|
function enableWal(db, budgetMs) {
|
|
29
75
|
const deadline = Date.now() + Math.max(budgetMs, 0);
|
|
@@ -39,46 +85,23 @@ function enableWal(db, budgetMs) {
|
|
|
39
85
|
}
|
|
40
86
|
}
|
|
41
87
|
}
|
|
42
|
-
/**
|
|
43
|
-
* Open a SQLite database using better-sqlite3.
|
|
44
|
-
*
|
|
45
|
-
* `better-sqlite3` defaults `busy_timeout` to 0, so concurrent writers throw
|
|
46
|
-
* `SQLITE_BUSY` instead of waiting. WAL improves read-while-write concurrency
|
|
47
|
-
* but does not serialise writers. Setting the timeout at connection open makes
|
|
48
|
-
* parallel processes queue at batch boundaries instead of failing on contact.
|
|
49
|
-
*
|
|
50
|
-
* WAL is enabled here too (with a bounded retry) so connection-level pragmas
|
|
51
|
-
* live in one place and the cold-database journal migration survives concurrent
|
|
52
|
-
* opens.
|
|
53
|
-
*
|
|
54
|
-
* Default 120_000 ms outlasts the worst-case batch commit on a multi-GB
|
|
55
|
-
* index. Override with `QMD_SQLITE_BUSY_TIMEOUT` (value in milliseconds; `0`
|
|
56
|
-
* restores the upstream fail-fast behaviour).
|
|
57
|
-
*/
|
|
88
|
+
/** Open a writable QMD database using Node's built-in SQLite runtime. */
|
|
58
89
|
export function openDatabase(path) {
|
|
59
|
-
const
|
|
60
|
-
const
|
|
61
|
-
const parsed = raw !== undefined && raw !== "" ? Number(raw) : Number.NaN;
|
|
62
|
-
const busyTimeoutMs = Number.isFinite(parsed) && parsed >= 0 ? Math.floor(parsed) : 120_000;
|
|
63
|
-
db.exec(`PRAGMA busy_timeout = ${busyTimeoutMs}`);
|
|
90
|
+
const busyTimeoutMs = resolveBusyTimeout();
|
|
91
|
+
const db = new Database(path, { allowExtension: true, timeout: busyTimeoutMs });
|
|
64
92
|
enableWal(db, busyTimeoutMs);
|
|
65
93
|
return db;
|
|
66
94
|
}
|
|
67
95
|
/** Open an existing database without changing journal mode, schema, or user data. */
|
|
68
96
|
export function openReadOnlyDatabase(path) {
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return db;
|
|
97
|
+
const busyTimeoutMs = resolveBusyTimeout();
|
|
98
|
+
return new Database(path, {
|
|
99
|
+
readOnly: true,
|
|
100
|
+
allowExtension: true,
|
|
101
|
+
timeout: busyTimeoutMs,
|
|
102
|
+
});
|
|
76
103
|
}
|
|
77
|
-
/**
|
|
78
|
-
* Load the sqlite-vec extension into a database.
|
|
79
|
-
*
|
|
80
|
-
* Throws with fix instructions when the extension is unavailable.
|
|
81
|
-
*/
|
|
104
|
+
/** Load the sqlite-vec extension into a database. */
|
|
82
105
|
export function loadSqliteVec(db) {
|
|
83
106
|
try {
|
|
84
107
|
sqliteVec.load(db);
|
package/dist/hybrid-llm.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export declare class HybridLLM implements LLM {
|
|
|
12
12
|
expandQuery(query: string, options?: {
|
|
13
13
|
context?: string;
|
|
14
14
|
includeLexical?: boolean;
|
|
15
|
+
includeHyde?: boolean;
|
|
15
16
|
}): Promise<Queryable[]>;
|
|
16
17
|
rerank(query: string, documents: RerankDocument[], options?: RerankOptions): Promise<RerankResult>;
|
|
17
18
|
dispose(): Promise<void>;
|
package/dist/index.d.ts
CHANGED
|
@@ -75,6 +75,8 @@ export interface SearchOptions {
|
|
|
75
75
|
explain?: boolean;
|
|
76
76
|
/** Query expansion policy (default: auto) */
|
|
77
77
|
expansion?: ExpansionMode;
|
|
78
|
+
/** Whether to include HyDE (hypothetical document) in query expansion (default: true) */
|
|
79
|
+
includeHyde?: boolean;
|
|
78
80
|
/** Optional progress/decision hooks for search orchestration */
|
|
79
81
|
hooks?: SearchHooks;
|
|
80
82
|
/** Chunk strategy: "auto" (default, uses AST for code files) or "regex" (legacy) */
|
|
@@ -100,6 +102,10 @@ export interface VectorSearchOptions {
|
|
|
100
102
|
export interface ExpandQueryOptions {
|
|
101
103
|
/** Additional context used only while generating query expansions. */
|
|
102
104
|
expansionContext?: string;
|
|
105
|
+
/** Whether to include lexical (BM25) sub-queries (default: true) */
|
|
106
|
+
includeLexical?: boolean;
|
|
107
|
+
/** Whether to include HyDE (hypothetical document) sub-queries (default: true) */
|
|
108
|
+
includeHyde?: boolean;
|
|
103
109
|
}
|
|
104
110
|
/**
|
|
105
111
|
* Options for creating a QMD store.
|
package/dist/index.js
CHANGED
|
@@ -231,6 +231,7 @@ export async function createStore(options) {
|
|
|
231
231
|
expansionContext: opts.expansionContext,
|
|
232
232
|
rerankContext: opts.rerankContext,
|
|
233
233
|
expansion: opts.expansion,
|
|
234
|
+
includeHyde: opts.includeHyde,
|
|
234
235
|
hooks: opts.hooks,
|
|
235
236
|
candidateLimit: opts.candidateLimit,
|
|
236
237
|
skipRerank,
|
|
@@ -242,7 +243,10 @@ export async function createStore(options) {
|
|
|
242
243
|
const provider = internal.embeddingProvider;
|
|
243
244
|
return internal.searchVec(q, provider?.model ?? internal.llm?.embedModelName ?? DEFAULT_EMBED_MODEL_URI, opts?.limit, opts?.collection);
|
|
244
245
|
},
|
|
245
|
-
expandQuery: async (q, opts) => internal.expandQuery(q, undefined, opts?.expansionContext
|
|
246
|
+
expandQuery: async (q, opts) => internal.expandQuery(q, undefined, opts?.expansionContext, {
|
|
247
|
+
includeLexical: opts?.includeLexical,
|
|
248
|
+
includeHyde: opts?.includeHyde,
|
|
249
|
+
}),
|
|
246
250
|
get: async (pathOrDocid, opts) => internal.findDocument(pathOrDocid, opts),
|
|
247
251
|
getDocumentBody: async (pathOrDocid, opts) => {
|
|
248
252
|
const result = internal.findDocument(pathOrDocid, { includeBody: false });
|
package/dist/llm.d.ts
CHANGED
|
@@ -137,6 +137,7 @@ export interface ILLMSession {
|
|
|
137
137
|
expandQuery(query: string, options?: {
|
|
138
138
|
context?: string;
|
|
139
139
|
includeLexical?: boolean;
|
|
140
|
+
includeHyde?: boolean;
|
|
140
141
|
}): Promise<Queryable[]>;
|
|
141
142
|
rerank(query: string, documents: RerankDocument[], options?: RerankOptions): Promise<RerankResult>;
|
|
142
143
|
/** Whether this session is still valid (not released or aborted) */
|
|
@@ -236,6 +237,7 @@ export interface LLM {
|
|
|
236
237
|
expandQuery(query: string, options?: {
|
|
237
238
|
context?: string;
|
|
238
239
|
includeLexical?: boolean;
|
|
240
|
+
includeHyde?: boolean;
|
|
239
241
|
}): Promise<Queryable[]>;
|
|
240
242
|
/**
|
|
241
243
|
* Rerank documents by relevance to a query
|
|
@@ -465,6 +467,7 @@ export declare class LlamaCpp implements LLM {
|
|
|
465
467
|
expandQuery(query: string, options?: {
|
|
466
468
|
context?: string;
|
|
467
469
|
includeLexical?: boolean;
|
|
470
|
+
includeHyde?: boolean;
|
|
468
471
|
}): Promise<Queryable[]>;
|
|
469
472
|
private static readonly RERANK_TEMPLATE_OVERHEAD;
|
|
470
473
|
private static readonly RERANK_TARGET_DOCS_PER_CONTEXT;
|
package/dist/llm.js
CHANGED
|
@@ -1245,6 +1245,7 @@ export class LlamaCpp {
|
|
|
1245
1245
|
const llama = await this.ensureLlama();
|
|
1246
1246
|
await this.ensureGenerateModel();
|
|
1247
1247
|
const includeLexical = options.includeLexical ?? true;
|
|
1248
|
+
const includeHyde = options.includeHyde ?? true;
|
|
1248
1249
|
const context = options.context;
|
|
1249
1250
|
// Keep the caller-provided expansion context separate from the query. It
|
|
1250
1251
|
// may clarify ambiguous terms, but it is untrusted data rather than an
|
|
@@ -1259,11 +1260,18 @@ export class LlamaCpp {
|
|
|
1259
1260
|
let genContext;
|
|
1260
1261
|
let sequence;
|
|
1261
1262
|
try {
|
|
1263
|
+
const allowedTypes = [];
|
|
1264
|
+
if (includeLexical)
|
|
1265
|
+
allowedTypes.push('"lex"');
|
|
1266
|
+
allowedTypes.push('"vec"');
|
|
1267
|
+
if (includeHyde)
|
|
1268
|
+
allowedTypes.push('"hyde"');
|
|
1269
|
+
const typeRule = allowedTypes.join(' | ');
|
|
1262
1270
|
const grammar = await llama.createGrammar({
|
|
1263
1271
|
grammar: `
|
|
1264
1272
|
root ::= line+
|
|
1265
1273
|
line ::= type ": " content "\\n"
|
|
1266
|
-
type ::=
|
|
1274
|
+
type ::= ${typeRule}
|
|
1267
1275
|
content ::= [^\\n]+
|
|
1268
1276
|
`
|
|
1269
1277
|
});
|
|
@@ -1304,21 +1312,27 @@ export class LlamaCpp {
|
|
|
1304
1312
|
const type = line.slice(0, colonIdx).trim();
|
|
1305
1313
|
if (type !== 'lex' && type !== 'vec' && type !== 'hyde')
|
|
1306
1314
|
return null;
|
|
1315
|
+
if (type === 'lex' && !includeLexical)
|
|
1316
|
+
return null;
|
|
1317
|
+
if (type === 'hyde' && !includeHyde)
|
|
1318
|
+
return null;
|
|
1307
1319
|
const text = line.slice(colonIdx + 1).trim();
|
|
1308
1320
|
if (!hasQueryTerm(text))
|
|
1309
1321
|
return null;
|
|
1310
1322
|
return { type: type, text };
|
|
1311
1323
|
}).filter((q) => q !== null);
|
|
1312
|
-
// Filter out
|
|
1313
|
-
const filtered =
|
|
1324
|
+
// Filter out unwanted types if any slipped through
|
|
1325
|
+
const filtered = queryables
|
|
1326
|
+
.filter(q => (includeLexical || q.type !== 'lex'))
|
|
1327
|
+
.filter(q => (includeHyde || q.type !== 'hyde'));
|
|
1314
1328
|
if (filtered.length > 0)
|
|
1315
1329
|
return filtered;
|
|
1316
1330
|
const fallback = [
|
|
1317
|
-
{ type: 'hyde', text: `Information about ${query}` },
|
|
1318
|
-
{ type: 'lex', text: query },
|
|
1331
|
+
...(includeHyde ? [{ type: 'hyde', text: `Information about ${query}` }] : []),
|
|
1332
|
+
...(includeLexical ? [{ type: 'lex', text: query }] : []),
|
|
1319
1333
|
{ type: 'vec', text: query },
|
|
1320
1334
|
];
|
|
1321
|
-
return
|
|
1335
|
+
return fallback;
|
|
1322
1336
|
}
|
|
1323
1337
|
catch (error) {
|
|
1324
1338
|
console.error("Structured query expansion failed:", error);
|
package/dist/mcp/server.js
CHANGED
|
@@ -255,8 +255,9 @@ Context-aware lex (C++ performance, not sports):
|
|
|
255
255
|
rerankContext: z.string().optional().describe("Additional context used only to rerank results and select snippets/chunks."),
|
|
256
256
|
rerank: z.boolean().optional().default(true).describe("Rerank results using LLM (default: true). Set to false for faster results on CPU-only machines."),
|
|
257
257
|
explain: z.boolean().optional().default(false).describe("Include retrieval traces and the shared query-expansion decision or typed expansion error"),
|
|
258
|
+
includeHyde: z.boolean().optional().default(true).describe("Whether to include HyDE (hypothetical document) in query expansion (default: true)"),
|
|
258
259
|
}),
|
|
259
|
-
}, track(async ({ query, searches, expansion, limit, minScore, candidateLimit, collections, expansionContext, rerankContext, rerank, explain }) => {
|
|
260
|
+
}, track(async ({ query, searches, expansion, includeHyde, limit, minScore, candidateLimit, collections, expansionContext, rerankContext, rerank, explain }) => {
|
|
260
261
|
// Require exactly one of `query` (plain text with an expansion policy) or `searches` (typed sub-queries).
|
|
261
262
|
if (!query && (!searches || searches.length === 0)) {
|
|
262
263
|
return {
|
|
@@ -292,6 +293,7 @@ Context-aware lex (C++ performance, not sports):
|
|
|
292
293
|
rerankContext,
|
|
293
294
|
explain,
|
|
294
295
|
expansion: query ? expansion : undefined,
|
|
296
|
+
includeHyde,
|
|
295
297
|
hooks: explain && query ? {
|
|
296
298
|
onExpansionDecision: decision => { expansionDecision = decision; },
|
|
297
299
|
onExpansionError: event => { expansionError = event; },
|
package/dist/remote-llm.d.ts
CHANGED
|
@@ -38,6 +38,7 @@ export declare class RemoteLLM implements LLM {
|
|
|
38
38
|
expandQuery(query: string, options?: {
|
|
39
39
|
context?: string;
|
|
40
40
|
includeLexical?: boolean;
|
|
41
|
+
includeHyde?: boolean;
|
|
41
42
|
timeZone?: string;
|
|
42
43
|
}): Promise<Queryable[]>;
|
|
43
44
|
rerank(query: string, documents: RerankDocument[], options?: RerankOptions | string | (RerankOptions & {
|
package/dist/remote-llm.js
CHANGED
|
@@ -127,18 +127,29 @@ export class RemoteLLM {
|
|
|
127
127
|
throw new Error("Remote expansion is not configured or circuit is broken.");
|
|
128
128
|
}
|
|
129
129
|
const includeLexical = options?.includeLexical !== false;
|
|
130
|
+
const includeHyde = options?.includeHyde !== false;
|
|
130
131
|
const lexicalOutput = includeLexical ? "lex: keyword-focused search phrase\n" : "";
|
|
131
132
|
const lexicalRule = includeLexical
|
|
132
133
|
? "- lex: preserve precise terms and add only useful synonyms or related keywords; do not write a complete question.\n"
|
|
133
134
|
: "";
|
|
134
135
|
const lexicalExample = includeLexical ? "lex: database connection pool timeout exhaustion\n" : "";
|
|
136
|
+
const hydeOutput = includeHyde ? "hyde: concise hypothetical answer-style passage\n" : "";
|
|
137
|
+
const hydeRule = includeHyde
|
|
138
|
+
? "- hyde: write a concise hypothetical passage describing plausible answer content, describing general concepts without inventing specific fake facts.\n"
|
|
139
|
+
: "";
|
|
140
|
+
const hydeExample = includeHyde ? "hyde: Database connection pool timeout troubleshooting may examine pool limits, active connections, query latency, and connection handling.\n" : "";
|
|
141
|
+
const requestedBackends = [
|
|
142
|
+
includeLexical ? "lex" : null,
|
|
143
|
+
"vec",
|
|
144
|
+
includeHyde ? "hyde" : null,
|
|
145
|
+
].filter(Boolean).join(", ");
|
|
135
146
|
const systemPrompt = `<role>
|
|
136
147
|
You are a specialized assistant for hybrid document-search query expansion.
|
|
137
148
|
You expand search queries to enhance retrieval recall with analytical precision while preserving user intent and constraints.
|
|
138
149
|
</role>
|
|
139
150
|
|
|
140
151
|
<instructions>
|
|
141
|
-
1. Proactively generate one high-quality variation for each requested backend (
|
|
152
|
+
1. Proactively generate one high-quality variation for each requested backend (${requestedBackends}) whenever the query has clear intent.
|
|
142
153
|
2. Preserve query constraints and avoid inventing unmentioned facts.
|
|
143
154
|
3. Return only the requested prefix lines.
|
|
144
155
|
</instructions>
|
|
@@ -150,15 +161,13 @@ You expand search queries to enhance retrieval recall with analytical precision
|
|
|
150
161
|
- Keep the query's primary language and script, while preserving exact identifiers, product names, API names, abbreviations, and established domain terms from the query or context.
|
|
151
162
|
${lexicalRule}- vec: state the search intent as a clear natural-language phrase or question.
|
|
152
163
|
- For space-separated or keyword-list queries, synthesize the scattered terms into a coherent, natural-language phrase or question for vec.
|
|
153
|
-
-
|
|
154
|
-
- For very short or identifier-only queries, retain exact terms without inventing unprovided constraints.
|
|
164
|
+
${hydeRule}- For very short or identifier-only queries, retain exact terms without inventing unprovided constraints.
|
|
155
165
|
</constraints>
|
|
156
166
|
|
|
157
167
|
<output_format>
|
|
158
168
|
Output only prefix lines. Do not include preambles, explanations, markdown, or code fences.
|
|
159
169
|
${lexicalOutput}vec: natural-language semantic search phrase or question
|
|
160
|
-
|
|
161
|
-
Generate at most one line of each listed type.
|
|
170
|
+
${hydeOutput}Generate at most one line of each listed type.
|
|
162
171
|
</output_format>
|
|
163
172
|
|
|
164
173
|
<example>
|
|
@@ -173,8 +182,7 @@ database pool timeout
|
|
|
173
182
|
</task>
|
|
174
183
|
|
|
175
184
|
${lexicalExample}vec: Why is the database connection pool timing out under load?
|
|
176
|
-
|
|
177
|
-
</example>`;
|
|
185
|
+
${hydeExample}</example>`;
|
|
178
186
|
const currentTime = getFormattedLocalTime(new Date(), options?.timeZone ?? this.timeZone);
|
|
179
187
|
const additionalContext = options?.context
|
|
180
188
|
? `Additional context:\n${escapePromptXml(options.context)}`
|
|
@@ -224,7 +232,11 @@ Return only the prefix lines specified in the output format.
|
|
|
224
232
|
const match = /^(lex|vec|hyde)\s*:\s*(.+)$/i.exec(line.trim());
|
|
225
233
|
if (match && match[1] && match[2]) {
|
|
226
234
|
const type = match[1].toLowerCase();
|
|
227
|
-
if (
|
|
235
|
+
if (type === "lex" && !includeLexical)
|
|
236
|
+
continue;
|
|
237
|
+
if (type === "hyde" && !includeHyde)
|
|
238
|
+
continue;
|
|
239
|
+
if (!seenTypes.has(type)) {
|
|
228
240
|
seenTypes.add(type);
|
|
229
241
|
results.push({ type, text: match[2].trim() });
|
|
230
242
|
}
|
package/dist/store.d.ts
CHANGED
|
@@ -132,6 +132,8 @@ export type ExpandedQuery = {
|
|
|
132
132
|
};
|
|
133
133
|
export type QueryExpansionOptions = {
|
|
134
134
|
requireResult?: boolean;
|
|
135
|
+
includeLexical?: boolean;
|
|
136
|
+
includeHyde?: boolean;
|
|
135
137
|
};
|
|
136
138
|
export declare function homedir(): string;
|
|
137
139
|
/**
|
|
@@ -313,7 +315,7 @@ export type Store = {
|
|
|
313
315
|
searchVec: (query: string, model: string, limit?: number, collectionFilter?: CollectionFilter, session?: ILLMSession, precomputedEmbedding?: number[]) => Promise<SearchResult[]>;
|
|
314
316
|
expandQuery: (query: string, model?: string, expansionContext?: string, options?: QueryExpansionOptions) => Promise<ExpandedQuery[]>;
|
|
315
317
|
/** Drop the cached expansion for a query so the next call regenerates. */
|
|
316
|
-
invalidateExpansionCache: (query: string, expansionContext?: string) => void;
|
|
318
|
+
invalidateExpansionCache: (query: string, expansionContext?: string, options?: QueryExpansionOptions) => void;
|
|
317
319
|
rerank: (query: string, documents: {
|
|
318
320
|
file: string;
|
|
319
321
|
text: string;
|
|
@@ -634,6 +636,8 @@ export type CacheKeyBody = {
|
|
|
634
636
|
chunk?: string;
|
|
635
637
|
file?: string;
|
|
636
638
|
expansionContext?: string;
|
|
639
|
+
noHyde?: boolean;
|
|
640
|
+
noLex?: boolean;
|
|
637
641
|
};
|
|
638
642
|
export declare function getCacheKey(url: string, body: CacheKeyBody): string;
|
|
639
643
|
export declare function getCachedResult(db: Database, cacheKey: string): string | null;
|
|
@@ -968,7 +972,10 @@ export declare function expandQuery(query: string, model: string | undefined, db
|
|
|
968
972
|
* expansion's sub-queries all came back empty — left in place, the dud entry
|
|
969
973
|
* would replay the same misses on every warm repeat of the query.
|
|
970
974
|
*/
|
|
971
|
-
export declare function deleteExpansionCacheEntry(db: Database, query: string, model?: string, expansionContext?: string
|
|
975
|
+
export declare function deleteExpansionCacheEntry(db: Database, query: string, model?: string, expansionContext?: string, options?: {
|
|
976
|
+
includeLexical?: boolean;
|
|
977
|
+
includeHyde?: boolean;
|
|
978
|
+
}): void;
|
|
972
979
|
export declare function rerank(query: string, documents: {
|
|
973
980
|
file: string;
|
|
974
981
|
text: string;
|
|
@@ -1106,6 +1113,7 @@ export interface HybridQueryOptions {
|
|
|
1106
1113
|
/** Additional context used for reranking and snippet/chunk selection. */
|
|
1107
1114
|
rerankContext?: string;
|
|
1108
1115
|
expansion?: ExpansionMode;
|
|
1116
|
+
includeHyde?: boolean;
|
|
1109
1117
|
skipRerank?: boolean;
|
|
1110
1118
|
chunkStrategy?: ChunkStrategy;
|
|
1111
1119
|
hooks?: SearchHooks;
|
|
@@ -1159,6 +1167,8 @@ export interface VectorSearchOptions {
|
|
|
1159
1167
|
minScore?: number;
|
|
1160
1168
|
/** Additional context used only while generating query expansions. */
|
|
1161
1169
|
expansionContext?: string;
|
|
1170
|
+
/** Whether to include HyDE (hypothetical document) in query expansion (default: true) */
|
|
1171
|
+
includeHyde?: boolean;
|
|
1162
1172
|
hooks?: Pick<SearchHooks, 'onExpand'>;
|
|
1163
1173
|
}
|
|
1164
1174
|
export interface VectorSearchResult {
|
package/dist/store.js
CHANGED
|
@@ -1208,7 +1208,7 @@ function rebuildFTSForCjkNormalization(db) {
|
|
|
1208
1208
|
}
|
|
1209
1209
|
});
|
|
1210
1210
|
// Pull bounded batches and finalize each SELECT before starting the insert
|
|
1211
|
-
// transaction. Holding
|
|
1211
|
+
// transaction. Holding an SQLite iterator open while beginning a
|
|
1212
1212
|
// transaction on the same connection raises "database is busy".
|
|
1213
1213
|
const selectBatch = db.prepare(`
|
|
1214
1214
|
SELECT d.id, d.collection, d.path, d.title, content.doc as body
|
|
@@ -1532,6 +1532,12 @@ export function setStoreGlobalContext(db, value) {
|
|
|
1532
1532
|
db.prepare(`INSERT INTO store_config (key, value) VALUES ('global_context', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value`).run(value);
|
|
1533
1533
|
}
|
|
1534
1534
|
}
|
|
1535
|
+
function writeConfigSyncDiagnostic(db, diagnostic) {
|
|
1536
|
+
db.prepare(`
|
|
1537
|
+
INSERT INTO store_config (key, value) VALUES ('config_sync_diagnostic', ?)
|
|
1538
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value
|
|
1539
|
+
`).run(JSON.stringify(diagnostic));
|
|
1540
|
+
}
|
|
1535
1541
|
function storeCollectionMatchesConfig(row, collection) {
|
|
1536
1542
|
return row.path === collection.path
|
|
1537
1543
|
&& row.pattern === (collection.pattern || '**/*.md')
|
|
@@ -1556,12 +1562,14 @@ export function syncConfigToDb(db, config) {
|
|
|
1556
1562
|
});
|
|
1557
1563
|
const currentGlobalContext = getStoreGlobalContext(db);
|
|
1558
1564
|
if (allMatch && currentGlobalContext === config.global_context) {
|
|
1559
|
-
|
|
1565
|
+
const diagnostic = {
|
|
1560
1566
|
configHashChanged: false,
|
|
1561
1567
|
reconciled: false,
|
|
1562
1568
|
collections: { added: [], updated: [], removed: [] },
|
|
1563
1569
|
globalContextUpdated: false,
|
|
1564
1570
|
};
|
|
1571
|
+
writeConfigSyncDiagnostic(db, diagnostic);
|
|
1572
|
+
return diagnostic;
|
|
1565
1573
|
}
|
|
1566
1574
|
}
|
|
1567
1575
|
}
|
|
@@ -2690,7 +2698,7 @@ export function createStore(dbPath, options = {}) {
|
|
|
2690
2698
|
searchVec: (query, model, limit, collectionFilter, session, precomputedEmbedding) => searchVec(db, query, model, limit, collectionFilter, session, precomputedEmbedding, store.embeddingProvider, store.authorizeRemoteRequest, store.llm),
|
|
2691
2699
|
// Query expansion & reranking
|
|
2692
2700
|
expandQuery: (query, model, expansionContext, options) => expandQuery(query, model ?? store.localLlm?.generateModelName ?? store.llm?.generateModelName ?? DEFAULT_QUERY_MODEL, db, expansionContext, store.llm, options),
|
|
2693
|
-
invalidateExpansionCache: (query, expansionContext) => deleteExpansionCacheEntry(db, query, store.localLlm?.generateModelName ?? store.llm?.generateModelName ?? DEFAULT_QUERY_MODEL, expansionContext),
|
|
2701
|
+
invalidateExpansionCache: (query, expansionContext, options) => deleteExpansionCacheEntry(db, query, store.localLlm?.generateModelName ?? store.llm?.generateModelName ?? DEFAULT_QUERY_MODEL, expansionContext, options),
|
|
2694
2702
|
rerank: (query, documents, model, rerankContext) => {
|
|
2695
2703
|
const llm = getLlm(store);
|
|
2696
2704
|
return rerank(query, documents, model ?? store.localLlm?.rerankModelName ?? llm?.rerankModelName ?? DEFAULT_RERANK_MODEL, db, rerankContext, store.llm ?? llm);
|
|
@@ -2905,7 +2913,7 @@ export async function maybeAdoptLegacyEmbeddingFingerprint(store, model = DEFAUL
|
|
|
2905
2913
|
return { checked: true, adopted: 0, reason: `legacy sample differs from current fingerprint (nearest ${nearest.hash_seq}, distance ${nearest.distance.toFixed(6)})` };
|
|
2906
2914
|
}
|
|
2907
2915
|
const update = withLazyContentVectorMigration(db, () => db.prepare(`UPDATE content_vectors SET embed_fingerprint = ? WHERE model = ? AND embed_fingerprint = ''`).run(fingerprint, model));
|
|
2908
|
-
return { checked: true, adopted: update.changes, reason: `sample ${expectedHashSeq} matched current fingerprint at distance ${nearest.distance.toFixed(6)}` };
|
|
2916
|
+
return { checked: true, adopted: Number(update.changes), reason: `sample ${expectedHashSeq} matched current fingerprint at distance ${nearest.distance.toFixed(6)}` };
|
|
2909
2917
|
});
|
|
2910
2918
|
}
|
|
2911
2919
|
export function getIndexHealth(db, model = DEFAULT_EMBED_MODEL) {
|
|
@@ -2952,7 +2960,7 @@ export function clearCache(db) {
|
|
|
2952
2960
|
*/
|
|
2953
2961
|
export function deleteLLMCache(db) {
|
|
2954
2962
|
const result = db.prepare(`DELETE FROM llm_cache`).run();
|
|
2955
|
-
return result.changes;
|
|
2963
|
+
return Number(result.changes);
|
|
2956
2964
|
}
|
|
2957
2965
|
/**
|
|
2958
2966
|
* Remove inactive document records (active = 0).
|
|
@@ -2976,7 +2984,7 @@ export function cleanupOrphanedContent(db) {
|
|
|
2976
2984
|
DELETE FROM content
|
|
2977
2985
|
WHERE hash NOT IN (SELECT DISTINCT hash FROM documents)
|
|
2978
2986
|
`).run();
|
|
2979
|
-
return result.changes;
|
|
2987
|
+
return Number(result.changes);
|
|
2980
2988
|
}
|
|
2981
2989
|
/**
|
|
2982
2990
|
* Count content hashes that would be unreferenced after inactive documents
|
|
@@ -4670,8 +4678,16 @@ export function insertEmbedding(db, hash, seq, pos, embedding, model, embeddedAt
|
|
|
4670
4678
|
// Query expansion
|
|
4671
4679
|
// =============================================================================
|
|
4672
4680
|
export async function expandQuery(query, model = DEFAULT_QUERY_MODEL, db, expansionContext, llmOverride, options) {
|
|
4681
|
+
const includeLexical = options?.includeLexical ?? true;
|
|
4682
|
+
const includeHyde = options?.includeHyde ?? true;
|
|
4673
4683
|
// Check cache first — stored as JSON preserving types
|
|
4674
|
-
const cacheKey = getCacheKey("expandQuery", {
|
|
4684
|
+
const cacheKey = getCacheKey("expandQuery", {
|
|
4685
|
+
query,
|
|
4686
|
+
model,
|
|
4687
|
+
...(expansionContext && { expansionContext }),
|
|
4688
|
+
...(!includeHyde && { noHyde: true }),
|
|
4689
|
+
...(!includeLexical && { noLex: true }),
|
|
4690
|
+
});
|
|
4675
4691
|
const cached = getCachedResult(db, cacheKey);
|
|
4676
4692
|
if (cached) {
|
|
4677
4693
|
try {
|
|
@@ -4693,7 +4709,11 @@ export async function expandQuery(query, model = DEFAULT_QUERY_MODEL, db, expans
|
|
|
4693
4709
|
}
|
|
4694
4710
|
const llm = llmOverride ?? getDefaultLlamaCpp();
|
|
4695
4711
|
// Note: LlamaCpp uses hardcoded model, model parameter is ignored
|
|
4696
|
-
const results = await llm.expandQuery(query, {
|
|
4712
|
+
const results = await llm.expandQuery(query, {
|
|
4713
|
+
context: expansionContext,
|
|
4714
|
+
includeLexical,
|
|
4715
|
+
includeHyde,
|
|
4716
|
+
});
|
|
4697
4717
|
// Map Queryable[] → ExpandedQuery[] (same shape, decoupled from llm.ts internals).
|
|
4698
4718
|
// Filter out entries that duplicate the original query text.
|
|
4699
4719
|
const expanded = results
|
|
@@ -4712,8 +4732,14 @@ export async function expandQuery(query, model = DEFAULT_QUERY_MODEL, db, expans
|
|
|
4712
4732
|
* expansion's sub-queries all came back empty — left in place, the dud entry
|
|
4713
4733
|
* would replay the same misses on every warm repeat of the query.
|
|
4714
4734
|
*/
|
|
4715
|
-
export function deleteExpansionCacheEntry(db, query, model = DEFAULT_QUERY_MODEL, expansionContext) {
|
|
4716
|
-
const cacheKey = getCacheKey("expandQuery", {
|
|
4735
|
+
export function deleteExpansionCacheEntry(db, query, model = DEFAULT_QUERY_MODEL, expansionContext, options) {
|
|
4736
|
+
const cacheKey = getCacheKey("expandQuery", {
|
|
4737
|
+
query,
|
|
4738
|
+
model,
|
|
4739
|
+
...(expansionContext && { expansionContext }),
|
|
4740
|
+
...(options?.includeHyde === false && { noHyde: true }),
|
|
4741
|
+
...(options?.includeLexical === false && { noLex: true }),
|
|
4742
|
+
});
|
|
4717
4743
|
db.prepare(`DELETE FROM llm_cache WHERE hash = ?`).run(cacheKey);
|
|
4718
4744
|
}
|
|
4719
4745
|
// =============================================================================
|
|
@@ -5491,6 +5517,7 @@ export async function hybridQuery(store, query, options) {
|
|
|
5491
5517
|
if (hasStrongSignal)
|
|
5492
5518
|
hooks?.onStrongSignal?.(topScore);
|
|
5493
5519
|
// Step 2: Expand query (or skip if strong signal)
|
|
5520
|
+
const includeHyde = options?.includeHyde ?? true;
|
|
5494
5521
|
if (expansionDecision.action === "expand")
|
|
5495
5522
|
hooks?.onExpandStart?.();
|
|
5496
5523
|
const expandStart = Date.now();
|
|
@@ -5500,6 +5527,7 @@ export async function hybridQuery(store, query, options) {
|
|
|
5500
5527
|
? []
|
|
5501
5528
|
: await store.expandQuery(query, undefined, expansionContext, {
|
|
5502
5529
|
requireResult: expansionDecision.reason === "explicit-force",
|
|
5530
|
+
includeHyde,
|
|
5503
5531
|
});
|
|
5504
5532
|
}
|
|
5505
5533
|
catch (error) {
|
|
@@ -5590,7 +5618,7 @@ export async function hybridQuery(store, query, options) {
|
|
|
5590
5618
|
const runnable = expanded.filter(q => q.type === "lex" || hasVectors);
|
|
5591
5619
|
const expansionContributed = rankedListMeta.some(m => m.queryType !== "original");
|
|
5592
5620
|
if (runnable.length > 0 && !expansionContributed) {
|
|
5593
|
-
store.invalidateExpansionCache(query, expansionContext);
|
|
5621
|
+
store.invalidateExpansionCache(query, expansionContext, { includeHyde });
|
|
5594
5622
|
}
|
|
5595
5623
|
}
|
|
5596
5624
|
// Step 4: RRF fusion — original-query FTS and vector lists get 2x weight;
|
|
@@ -5767,11 +5795,14 @@ export async function vectorSearchQuery(store, query, options) {
|
|
|
5767
5795
|
const minScore = options?.minScore ?? 0.3;
|
|
5768
5796
|
const collection = options?.collection;
|
|
5769
5797
|
const expansionContext = options?.expansionContext;
|
|
5798
|
+
const includeHyde = options?.includeHyde ?? true;
|
|
5770
5799
|
if (!hasSearchableVectorIndex(store))
|
|
5771
5800
|
return [];
|
|
5772
5801
|
// Expand query — filter to vec/hyde only (lex queries target FTS, not vector)
|
|
5773
5802
|
const expandStart = Date.now();
|
|
5774
|
-
const allExpanded = await store.expandQuery(query, undefined, expansionContext
|
|
5803
|
+
const allExpanded = await store.expandQuery(query, undefined, expansionContext, {
|
|
5804
|
+
includeHyde,
|
|
5805
|
+
});
|
|
5775
5806
|
const vecExpanded = allExpanded.filter(q => q.type !== 'lex');
|
|
5776
5807
|
options?.hooks?.onExpand?.(query, vecExpanded, Date.now() - expandStart);
|
|
5777
5808
|
const embedModel = store.embeddingProvider?.model ?? getLlm(store).embedModelName;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wei840222/qmd",
|
|
3
|
-
"version": "2026.
|
|
3
|
+
"version": "2026.9.6",
|
|
4
4
|
"packageManager": "pnpm@11.15.1",
|
|
5
5
|
"description": "Query Markup Documents - On-device hybrid search for markdown files with BM25, vector search, and LLM reranking",
|
|
6
6
|
"type": "module",
|
|
@@ -66,7 +66,6 @@
|
|
|
66
66
|
"dependencies": {
|
|
67
67
|
"@modelcontextprotocol/server": "2.0.0",
|
|
68
68
|
"@node-rs/jieba": "2.0.2",
|
|
69
|
-
"better-sqlite3": "^13.0.3",
|
|
70
69
|
"fast-glob": "3.3.3",
|
|
71
70
|
"node-llama-cpp": "3.20.0",
|
|
72
71
|
"picomatch": "4.0.5",
|
|
@@ -88,7 +87,6 @@
|
|
|
88
87
|
},
|
|
89
88
|
"devDependencies": {
|
|
90
89
|
"@oxlint/plugins": "1.78.0",
|
|
91
|
-
"@types/better-sqlite3": "7.6.13",
|
|
92
90
|
"oxlint": "1.78.0",
|
|
93
91
|
"tsx": "4.23.12",
|
|
94
92
|
"vitest": "3.2.7"
|
|
@@ -113,7 +111,7 @@
|
|
|
113
111
|
"typescript": "^5.9.3 || ^6.0.0-0"
|
|
114
112
|
},
|
|
115
113
|
"engines": {
|
|
116
|
-
"node": ">=22.
|
|
114
|
+
"node": ">=22.16.0"
|
|
117
115
|
},
|
|
118
116
|
"keywords": [
|
|
119
117
|
"markdown",
|