Переглянути джерело

feat: redesign SDK search API with unified search() and ExpandedQuery type

Replace three separate search methods (query, search, structuredSearch)
with a single search(options) that accepts either a query string
(auto-expanded) or pre-expanded queries. Add searchLex/searchVector
convenience methods and expandQuery for manual control.

Unify StructuredSubSearch and ExpandedQuery into a single ExpandedQuery
type with { type, query } used throughout the pipeline. Add skipRerank
option to hybridQuery and structuredSearch for fast no-LLM searches.

New SDK surface:
- search({ query, intent, rerank, limit, ... })
- search({ queries: expanded })
- searchLex(query, opts)
- searchVector(query, opts)
- expandQuery(query, { intent })
Tobi Lutke 2 місяців тому
батько
коміт
839d774a06
9 змінених файлів з 1472 додано та 514 видалено
  1. 312 73
      src/index.ts
  2. 3 3
      src/mcp.ts
  3. 96 146
      src/qmd.ts
  4. 579 94
      src/store.ts
  5. 3 3
      test/intent.test.ts
  6. 35 8
      test/mcp.test.ts
  7. 412 175
      test/sdk.test.ts
  8. 23 3
      test/store.test.ts
  9. 9 9
      test/structured-search.test.ts

+ 312 - 73
src/index.ts

@@ -4,7 +4,7 @@
  * Usage:
  *   import { createStore } from '@tobilu/qmd'
  *
- *   const store = createStore({
+ *   const store = await createStore({
  *     dbPath: './my-index.sqlite',
  *     config: {
  *       collections: {
@@ -13,15 +13,29 @@
  *     }
  *   })
  *
- *   const results = await store.query("how does auth work?")
- *   store.close()
+ *   const results = await store.search({ query: "how does auth work?" })
+ *   await store.close()
  */
 
 import {
   createStore as createStoreInternal,
   hybridQuery,
   structuredSearch,
+  DEFAULT_EMBED_MODEL,
+  reindexCollection,
+  generateEmbeddings,
   listCollections as storeListCollections,
+  syncConfigToDb,
+  getStoreCollections,
+  getStoreCollection,
+  getStoreGlobalContext,
+  getStoreContexts,
+  upsertStoreCollection,
+  deleteStoreCollection,
+  renameStoreCollection,
+  updateStoreContext,
+  removeStoreContext,
+  setStoreGlobalContext,
   type Store as InternalStore,
   type DocumentResult,
   type DocumentNotFound,
@@ -29,26 +43,29 @@ import {
   type HybridQueryResult,
   type HybridQueryOptions,
   type HybridQueryExplain,
-  type StructuredSubSearch,
+  type ExpandedQuery,
   type StructuredSearchOptions,
   type MultiGetResult,
   type IndexStatus,
   type IndexHealthInfo,
-  type ExpandedQuery,
   type SearchHooks,
+  type ReindexProgress,
+  type ReindexResult,
+  type EmbedProgress,
+  type EmbedResult,
 } from "./store.js";
+import {
+  LlamaCpp,
+} from "./llm.js";
 import {
   setConfigSource,
   loadConfig,
   addCollection as collectionsAddCollection,
   removeCollection as collectionsRemoveCollection,
   renameCollection as collectionsRenameCollection,
-  listCollections as collectionsListCollections,
   addContext as collectionsAddContext,
   removeContext as collectionsRemoveContext,
   setGlobalContext as collectionsSetGlobalContext,
-  getGlobalContext as collectionsGetGlobalContext,
-  listAllContexts as collectionsListAllContexts,
   type Collection,
   type CollectionConfig,
   type NamedCollection,
@@ -63,22 +80,97 @@ export type {
   HybridQueryResult,
   HybridQueryOptions,
   HybridQueryExplain,
-  StructuredSubSearch,
+  ExpandedQuery,
   StructuredSearchOptions,
   MultiGetResult,
   IndexStatus,
   IndexHealthInfo,
-  ExpandedQuery,
   SearchHooks,
+  ReindexProgress,
+  ReindexResult,
+  EmbedProgress,
+  EmbedResult,
   Collection,
   CollectionConfig,
   NamedCollection,
   ContextMap,
 };
 
+/**
+ * Progress info emitted during update() for each file processed.
+ */
+export type UpdateProgress = {
+  collection: string;
+  file: string;
+  current: number;
+  total: number;
+};
+
+/**
+ * Aggregated result from update() across all collections.
+ */
+export type UpdateResult = {
+  collections: number;
+  indexed: number;
+  updated: number;
+  unchanged: number;
+  removed: number;
+  needsEmbedding: number;
+};
+
+/**
+ * Options for the unified search() method.
+ */
+export interface SearchOptions {
+  /** Simple query string — will be auto-expanded via LLM */
+  query?: string;
+  /** Pre-expanded queries (from expandQuery) — skips auto-expansion */
+  queries?: ExpandedQuery[];
+  /** Domain intent hint — steers expansion and reranking */
+  intent?: string;
+  /** Rerank results using LLM (default: true) */
+  rerank?: boolean;
+  /** Filter to a specific collection */
+  collection?: string;
+  /** Filter to specific collections */
+  collections?: string[];
+  /** Max results (default: 10) */
+  limit?: number;
+  /** Minimum score threshold */
+  minScore?: number;
+  /** Include explain traces */
+  explain?: boolean;
+}
+
+/**
+ * Options for searchLex() — BM25 keyword search.
+ */
+export interface LexSearchOptions {
+  limit?: number;
+  collection?: string;
+}
+
+/**
+ * Options for searchVector() — vector similarity search.
+ */
+export interface VectorSearchOptions {
+  limit?: number;
+  collection?: string;
+}
+
+/**
+ * Options for expandQuery() — manual query expansion.
+ */
+export interface ExpandQueryOptions {
+  intent?: string;
+}
+
 /**
  * Options for creating a QMD store.
- * You must provide `dbPath` and either `configPath` (YAML file) or `config` (inline).
+ *
+ * Provide `dbPath` and optionally `configPath` (YAML file) or `config` (inline).
+ * If neither configPath nor config is provided, the store reads from existing
+ * DB state (useful for reopening a previously-configured store).
  */
 export interface StoreOptions {
   /** Path to the SQLite database file */
@@ -92,6 +184,9 @@ export interface StoreOptions {
 /**
  * The QMD SDK store — provides search, retrieval, collection management,
  * context management, and indexing operations.
+ *
+ * All methods are async. The store manages its own LlamaCpp instance
+ * (lazy-loaded, auto-unloaded after inactivity) — no global singletons.
  */
 export interface QMDStore {
   /** The underlying internal store (for advanced use) */
@@ -99,66 +194,86 @@ export interface QMDStore {
   /** Path to the SQLite database */
   readonly dbPath: string;
 
-  // ── Search & Retrieval ──────────────────────────────────────────────
+  // ── Search ──────────────────────────────────────────────────────────
+
+  /** Full search: query expansion + multi-signal retrieval + LLM reranking */
+  search(options: SearchOptions): Promise<HybridQueryResult[]>;
 
-  /** Hybrid search: BM25 + vector + query expansion + LLM reranking */
-  query(query: string, options?: HybridQueryOptions): Promise<HybridQueryResult[]>;
+  /** BM25 keyword search (fast, no LLM) */
+  searchLex(query: string, options?: LexSearchOptions): Promise<SearchResult[]>;
 
-  /** BM25 full-text keyword search (fast, no LLM) */
-  search(query: string, options?: { limit?: number; collection?: string }): SearchResult[];
+  /** Vector similarity search (embedding model, no reranking) */
+  searchVector(query: string, options?: VectorSearchOptions): Promise<SearchResult[]>;
 
-  /** Structured search with pre-expanded queries (for LLM callers) */
-  structuredSearch(searches: StructuredSubSearch[], options?: StructuredSearchOptions): Promise<HybridQueryResult[]>;
+  /** Expand a query into typed sub-searches (lex/vec/hyde) for manual control */
+  expandQuery(query: string, options?: ExpandQueryOptions): Promise<ExpandedQuery[]>;
+
+  // ── Document Retrieval ──────────────────────────────────────────────
 
   /** Get a single document by path or docid */
-  get(pathOrDocid: string, options?: { includeBody?: boolean }): DocumentResult | DocumentNotFound;
+  get(pathOrDocid: string, options?: { includeBody?: boolean }): Promise<DocumentResult | DocumentNotFound>;
 
   /** Get multiple documents by glob pattern or comma-separated list */
-  multiGet(pattern: string, options?: { includeBody?: boolean; maxBytes?: number }): { docs: MultiGetResult[]; errors: string[] };
+  multiGet(pattern: string, options?: { includeBody?: boolean; maxBytes?: number }): Promise<{ docs: MultiGetResult[]; errors: string[] }>;
 
   // ── Collection Management ───────────────────────────────────────────
 
   /** Add or update a collection */
-  addCollection(name: string, opts: { path: string; pattern?: string; ignore?: string[] }): void;
+  addCollection(name: string, opts: { path: string; pattern?: string; ignore?: string[] }): Promise<void>;
 
   /** Remove a collection */
-  removeCollection(name: string): boolean;
+  removeCollection(name: string): Promise<boolean>;
 
   /** Rename a collection */
-  renameCollection(oldName: string, newName: string): boolean;
+  renameCollection(oldName: string, newName: string): Promise<boolean>;
 
   /** List all collections with document stats */
-  listCollections(): { name: string; pwd: string; glob_pattern: string; doc_count: number; active_count: number; last_modified: string | null }[];
+  listCollections(): Promise<{ name: string; pwd: string; glob_pattern: string; doc_count: number; active_count: number; last_modified: string | null }[]>;
 
   // ── Context Management ──────────────────────────────────────────────
 
   /** Add context for a path within a collection */
-  addContext(collectionName: string, pathPrefix: string, contextText: string): boolean;
+  addContext(collectionName: string, pathPrefix: string, contextText: string): Promise<boolean>;
 
   /** Remove context from a collection path */
-  removeContext(collectionName: string, pathPrefix: string): boolean;
+  removeContext(collectionName: string, pathPrefix: string): Promise<boolean>;
 
   /** Set global context (applies to all collections) */
-  setGlobalContext(context: string | undefined): void;
+  setGlobalContext(context: string | undefined): Promise<void>;
 
   /** Get global context */
-  getGlobalContext(): string | undefined;
+  getGlobalContext(): Promise<string | undefined>;
 
   /** List all contexts across all collections */
-  listContexts(): Array<{ collection: string; path: string; context: string }>;
+  listContexts(): Promise<Array<{ collection: string; path: string; context: string }>>;
+
+  // ── Indexing ────────────────────────────────────────────────────────
+
+  /** Re-index collections by scanning the filesystem */
+  update(options?: {
+    collections?: string[];
+    onProgress?: (info: UpdateProgress) => void;
+  }): Promise<UpdateResult>;
+
+  /** Generate vector embeddings for documents that need them */
+  embed(options?: {
+    force?: boolean;
+    model?: string;
+    onProgress?: (info: EmbedProgress) => void;
+  }): Promise<EmbedResult>;
 
   // ── Index Health ────────────────────────────────────────────────────
 
   /** Get index status (document counts, collections, embedding state) */
-  getStatus(): IndexStatus;
+  getStatus(): Promise<IndexStatus>;
 
   /** Get index health info (stale embeddings, etc.) */
-  getIndexHealth(): IndexHealthInfo;
+  getIndexHealth(): Promise<IndexHealthInfo>;
 
   // ── Lifecycle ───────────────────────────────────────────────────────
 
-  /** Close the database connection */
-  close(): void;
+  /** Close the store and release all resources (LLM models, DB connection) */
+  close(): Promise<void>;
 }
 
 /**
@@ -167,13 +282,13 @@ export interface QMDStore {
  * @example
  * ```typescript
  * // With a YAML config file
- * const store = createStore({
+ * const store = await createStore({
  *   dbPath: './index.sqlite',
  *   configPath: './qmd.yml',
  * })
  *
  * // With inline config (no files needed besides the DB)
- * const store = createStore({
+ * const store = await createStore({
  *   dbPath: './index.sqlite',
  *   config: {
  *     collections: {
@@ -182,66 +297,190 @@ export interface QMDStore {
  *   }
  * })
  *
- * const results = await store.query("authentication flow")
- * store.close()
+ * const results = await store.search({ query: "authentication flow" })
+ * await store.close()
  * ```
  */
-export function createStore(options: StoreOptions): QMDStore {
+export async function createStore(options: StoreOptions): Promise<QMDStore> {
   if (!options.dbPath) {
     throw new Error("dbPath is required");
   }
-  if (!options.configPath && !options.config) {
-    throw new Error("Either configPath or config is required");
-  }
   if (options.configPath && options.config) {
     throw new Error("Provide either configPath or config, not both");
   }
 
-  // Inject config source into collections module
-  setConfigSource({
-    configPath: options.configPath,
-    config: options.config,
-  });
-
-  // Create the internal store
+  // Create the internal store (opens DB, creates tables)
   const internal = createStoreInternal(options.dbPath);
+  const db = internal.db;
+
+  // Track whether we have a YAML config path for write-through
+  const hasYamlConfig = !!options.configPath;
+
+  // Sync config into SQLite store_collections
+  if (options.configPath) {
+    // YAML mode: inject config source for write-through, sync to DB
+    setConfigSource({ configPath: options.configPath });
+    const config = loadConfig();
+    syncConfigToDb(db, config);
+  } else if (options.config) {
+    // Inline config mode: inject config source for mutations, sync to DB
+    setConfigSource({ config: options.config });
+    syncConfigToDb(db, options.config);
+  }
+  // else: DB-only mode — no external config, use existing store_collections
+
+  // Create a per-store LlamaCpp instance — lazy-loads models on first use,
+  // auto-unloads after 5 min inactivity to free VRAM.
+  const llm = new LlamaCpp({
+    inactivityTimeoutMs: 5 * 60 * 1000,
+    disposeModelsOnInactivity: true,
+  });
+  internal.llm = llm;
 
   const store: QMDStore = {
     internal,
     dbPath: internal.dbPath,
 
-    // Search & Retrieval
-    query: (q, opts) => hybridQuery(internal, q, opts),
-    search: (q, opts) => internal.searchFTS(q, opts?.limit, opts?.collection),
-    structuredSearch: (searches, opts) => structuredSearch(internal, searches, opts),
-    get: (pathOrDocid, opts) => internal.findDocument(pathOrDocid, opts),
-    multiGet: (pattern, opts) => internal.findDocuments(pattern, opts),
+    // Search
+    search: async (opts) => {
+      if (!opts.query && !opts.queries) {
+        throw new Error("search() requires either 'query' or 'queries'");
+      }
+      // Normalize collection/collections
+      const collections = [
+        ...(opts.collection ? [opts.collection] : []),
+        ...(opts.collections ?? []),
+      ];
+      const skipRerank = opts.rerank === false;
+
+      if (opts.queries) {
+        // Pre-expanded queries — use structuredSearch
+        return structuredSearch(internal, opts.queries, {
+          collections: collections.length > 0 ? collections : undefined,
+          limit: opts.limit,
+          minScore: opts.minScore,
+          explain: opts.explain,
+          intent: opts.intent,
+          skipRerank,
+        });
+      }
+
+      // Simple query string — use hybridQuery (expand + search + rerank)
+      return hybridQuery(internal, opts.query!, {
+        collection: collections[0],
+        limit: opts.limit,
+        minScore: opts.minScore,
+        explain: opts.explain,
+        intent: opts.intent,
+        skipRerank,
+      });
+    },
+    searchLex: async (q, opts) => internal.searchFTS(q, opts?.limit, opts?.collection),
+    searchVector: async (q, opts) => internal.searchVec(q, DEFAULT_EMBED_MODEL, opts?.limit, opts?.collection),
+    expandQuery: async (q, opts) => internal.expandQuery(q, undefined, opts?.intent),
+    get: async (pathOrDocid, opts) => internal.findDocument(pathOrDocid, opts),
+    multiGet: async (pattern, opts) => internal.findDocuments(pattern, opts),
+
+    // Collection Management — write to SQLite + write-through to YAML/inline if configured
+    addCollection: async (name, opts) => {
+      upsertStoreCollection(db, name, { path: opts.path, pattern: opts.pattern, ignore: opts.ignore });
+      if (hasYamlConfig || options.config) {
+        collectionsAddCollection(name, opts.path, opts.pattern);
+      }
+    },
+    removeCollection: async (name) => {
+      const result = deleteStoreCollection(db, name);
+      if (hasYamlConfig || options.config) {
+        collectionsRemoveCollection(name);
+      }
+      return result;
+    },
+    renameCollection: async (oldName, newName) => {
+      const result = renameStoreCollection(db, oldName, newName);
+      if (hasYamlConfig || options.config) {
+        collectionsRenameCollection(oldName, newName);
+      }
+      return result;
+    },
+    listCollections: async () => storeListCollections(db),
+
+    // Context Management — write to SQLite + write-through to YAML/inline if configured
+    addContext: async (collectionName, pathPrefix, contextText) => {
+      const result = updateStoreContext(db, collectionName, pathPrefix, contextText);
+      if (hasYamlConfig || options.config) {
+        collectionsAddContext(collectionName, pathPrefix, contextText);
+      }
+      return result;
+    },
+    removeContext: async (collectionName, pathPrefix) => {
+      const result = removeStoreContext(db, collectionName, pathPrefix);
+      if (hasYamlConfig || options.config) {
+        collectionsRemoveContext(collectionName, pathPrefix);
+      }
+      return result;
+    },
+    setGlobalContext: async (context) => {
+      setStoreGlobalContext(db, context);
+      if (hasYamlConfig || options.config) {
+        collectionsSetGlobalContext(context);
+      }
+    },
+    getGlobalContext: async () => getStoreGlobalContext(db),
+    listContexts: async () => getStoreContexts(db),
+
+    // Indexing — reads collections from SQLite
+    update: async (updateOpts) => {
+      const collections = getStoreCollections(db);
+      const filtered = updateOpts?.collections
+        ? collections.filter(c => updateOpts.collections!.includes(c.name))
+        : collections;
+
+      internal.clearCache();
+
+      let totalIndexed = 0, totalUpdated = 0, totalUnchanged = 0, totalRemoved = 0;
+
+      for (const col of filtered) {
+        const result = await reindexCollection(internal, col.path, col.pattern || "**/*.md", col.name, {
+          ignorePatterns: col.ignore,
+          onProgress: updateOpts?.onProgress
+            ? (info) => updateOpts.onProgress!({ collection: col.name, ...info })
+            : undefined,
+        });
+        totalIndexed += result.indexed;
+        totalUpdated += result.updated;
+        totalUnchanged += result.unchanged;
+        totalRemoved += result.removed;
+      }
+
+      return {
+        collections: filtered.length,
+        indexed: totalIndexed,
+        updated: totalUpdated,
+        unchanged: totalUnchanged,
+        removed: totalRemoved,
+        needsEmbedding: internal.getHashesNeedingEmbedding(),
+      };
+    },
 
-    // Collection Management
-    addCollection: (name, opts) => {
-      collectionsAddCollection(name, opts.path, opts.pattern);
+    embed: async (embedOpts) => {
+      return generateEmbeddings(internal, {
+        force: embedOpts?.force,
+        model: embedOpts?.model,
+        onProgress: embedOpts?.onProgress,
+      });
     },
-    removeCollection: (name) => collectionsRemoveCollection(name),
-    renameCollection: (oldName, newName) => collectionsRenameCollection(oldName, newName),
-    listCollections: () => storeListCollections(internal.db),
-
-    // Context Management
-    addContext: (collectionName, pathPrefix, contextText) =>
-      collectionsAddContext(collectionName, pathPrefix, contextText),
-    removeContext: (collectionName, pathPrefix) =>
-      collectionsRemoveContext(collectionName, pathPrefix),
-    setGlobalContext: (context) => collectionsSetGlobalContext(context),
-    getGlobalContext: () => collectionsGetGlobalContext(),
-    listContexts: () => collectionsListAllContexts(),
 
     // Index Health
-    getStatus: () => internal.getStatus(),
-    getIndexHealth: () => internal.getIndexHealth(),
+    getStatus: async () => internal.getStatus(),
+    getIndexHealth: async () => internal.getIndexHealth(),
 
     // Lifecycle
-    close: () => {
+    close: async () => {
+      await llm.dispose();
       internal.close();
-      setConfigSource(undefined); // Reset config source
+      if (hasYamlConfig || options.config) {
+        setConfigSource(undefined); // Reset config source
+      }
     },
   };
 

+ 3 - 3
src/mcp.ts

@@ -23,7 +23,7 @@ import {
   structuredSearch,
   DEFAULT_MULTI_GET_MAX_BYTES,
 } from "./store.js";
-import type { Store, StructuredSubSearch } from "./store.js";
+import type { Store, ExpandedQuery } from "./store.js";
 import { getCollection, getGlobalContext, getDefaultCollectionNames } from "./collections.js";
 import { disposeDefaultLlamaCpp } from "./llm.js";
 
@@ -322,7 +322,7 @@ Intent-aware lex (C++ performance, not sports):
     },
     async ({ searches, limit, minScore, candidateLimit, collections, intent }) => {
       // Map to internal format
-      const subSearches: StructuredSubSearch[] = searches.map(s => ({
+      const subSearches: ExpandedQuery[] = searches.map(s => ({
         type: s.type,
         query: s.query,
       }));
@@ -654,7 +654,7 @@ export async function startMcpHttpServer(port: number, options?: { quiet?: boole
         }
 
         // Map to internal format
-        const subSearches: StructuredSubSearch[] = params.searches.map((s: any) => ({
+        const subSearches: ExpandedQuery[] = params.searches.map((s: any) => ({
           type: s.type as 'lex' | 'vec' | 'hyde',
           query: String(s.query || ""),
         }));

+ 96 - 146
src/qmd.ts

@@ -63,13 +63,16 @@ import {
   addLineNumbers,
   type ExpandedQuery,
   type HybridQueryExplain,
-  type StructuredSubSearch,
   DEFAULT_EMBED_MODEL,
   DEFAULT_RERANK_MODEL,
   DEFAULT_GLOB,
   DEFAULT_MULTI_GET_MAX_BYTES,
   createStore,
   getDefaultDbPath,
+  reindexCollection,
+  generateEmbeddings,
+  syncConfigToDb,
+  type ReindexResult,
 } from "./store.js";
 import { disposeDefaultLlamaCpp, getDefaultLlamaCpp, withLLMSession, pullModels, DEFAULT_EMBED_MODEL_URI, DEFAULT_GENERATE_MODEL_URI, DEFAULT_RERANK_MODEL_URI, DEFAULT_MODEL_CACHE_DIR } from "./llm.js";
 import {
@@ -85,9 +88,12 @@ import {
   getDefaultCollectionNames,
   addContext as yamlAddContext,
   removeContext as yamlRemoveContext,
+  removeCollection as yamlRemoveCollectionFn,
+  renameCollection as yamlRenameCollectionFn,
   setGlobalContext,
   listAllContexts,
   setConfigIndexName,
+  loadConfig,
 } from "./collections.js";
 
 // Enable production mode - allows using default database path
@@ -104,6 +110,13 @@ let storeDbPathOverride: string | undefined;
 function getStore(): ReturnType<typeof createStore> {
   if (!store) {
     store = createStore(storeDbPathOverride);
+    // Sync YAML config into SQLite store_collections so store.ts reads from DB
+    try {
+      const config = loadConfig();
+      syncConfigToDb(store.db, config);
+    } catch {
+      // Config may not exist yet — that's fine, DB works without it
+    }
   }
   return store;
 }
@@ -112,6 +125,19 @@ function getDb(): Database {
   return getStore().db;
 }
 
+/** Re-sync YAML config into SQLite after CLI mutations (add/remove/rename collection, context changes) */
+function resyncConfig(): void {
+  const s = getStore();
+  try {
+    const config = loadConfig();
+    // Clear config hash to force re-sync
+    s.db.prepare(`DELETE FROM store_config WHERE key = 'config_hash'`).run();
+    syncConfigToDb(s.db, config);
+  } catch {
+    // Config may not exist — that's fine
+  }
+}
+
 function closeDb(): void {
   if (store) {
     store.close();
@@ -467,6 +493,7 @@ async function showStatus(): Promise<void> {
 
 async function updateCollections(): Promise<void> {
   const db = getDb();
+  const storeInstance = getStore();
   // Collections are defined in YAML; no duplicate cleanup needed.
 
   // Clear Ollama cache on update
@@ -480,7 +507,6 @@ async function updateCollections(): Promise<void> {
     return;
   }
 
-  // Don't close db here - indexFiles will reuse it and close at the end
   console.log(`${c.bold}Updating ${collections.length} collection(s)...${c.reset}\n`);
 
   for (let i = 0; i < collections.length; i++) {
@@ -524,13 +550,32 @@ async function updateCollections(): Promise<void> {
       }
     }
 
-    await indexFiles(col.pwd, col.glob_pattern, col.name, true, yamlCol?.ignore);
+    const startTime = Date.now();
+    console.log(`Collection: ${col.pwd} (${col.glob_pattern})`);
+    progress.indeterminate();
+
+    const result = await reindexCollection(storeInstance, col.pwd, col.glob_pattern, col.name, {
+      ignorePatterns: yamlCol?.ignore,
+      onProgress: (info) => {
+        progress.set((info.current / info.total) * 100);
+        const elapsed = (Date.now() - startTime) / 1000;
+        const rate = info.current / elapsed;
+        const remaining = (info.total - info.current) / rate;
+        const eta = info.current > 2 ? ` ETA: ${formatETA(remaining)}` : "";
+        if (isTTY) process.stderr.write(`\rIndexing: ${info.current}/${info.total}${eta}        `);
+      },
+    });
+
+    progress.clear();
+    console.log(`\nIndexed: ${result.indexed} new, ${result.updated} updated, ${result.unchanged} unchanged, ${result.removed} removed`);
+    if (result.orphanedCleaned > 0) {
+      console.log(`Cleaned up ${result.orphanedCleaned} orphaned content hash(es)`);
+    }
     console.log("");
   }
 
   // Check if any documents need embedding (show once at end)
-  const finalDb = getDb();
-  const needsEmbedding = getHashesNeedingEmbedding(finalDb);
+  const needsEmbedding = getHashesNeedingEmbedding(db);
   closeDb();
 
   console.log(`${c.green}✓ All collections updated.${c.reset}`);
@@ -581,6 +626,7 @@ async function contextAdd(pathArg: string | undefined, contextText: string): Pro
   // Handle "/" as global context (applies to all collections)
   if (pathArg === '/') {
     setGlobalContext(contextText);
+    resyncConfig();
     console.log(`${c.green}✓${c.reset} Set global context`);
     console.log(`${c.dim}Context: ${contextText}${c.reset}`);
     closeDb();
@@ -612,6 +658,7 @@ async function contextAdd(pathArg: string | undefined, contextText: string): Pro
     }
 
     yamlAddContext(parsed.collectionName, parsed.path, contextText);
+    resyncConfig();
 
     const displayPath = parsed.path
       ? `qmd://${parsed.collectionName}/${parsed.path}`
@@ -631,6 +678,7 @@ async function contextAdd(pathArg: string | undefined, contextText: string): Pro
   }
 
   yamlAddContext(detected.collectionName, detected.relativePath, contextText);
+  resyncConfig();
 
   const displayPath = detected.relativePath ? `qmd://${detected.collectionName}/${detected.relativePath}` : `qmd://${detected.collectionName}/`;
   console.log(`${c.green}✓${c.reset} Added context for: ${displayPath}`);
@@ -670,6 +718,10 @@ function contextRemove(pathArg: string): void {
   if (pathArg === '/') {
     // Remove global context
     setGlobalContext(undefined);
+    // Resync so SQLite store_config is updated
+    const s = getStore();
+    resyncConfig();
+    closeDb();
     console.log(`${c.green}✓${c.reset} Removed global context`);
     return;
   }
@@ -1347,9 +1399,10 @@ async function collectionAdd(pwd: string, globPattern: string, name?: string): P
     process.exit(1);
   }
 
-  // Add to YAML config
+  // Add to YAML config + sync to SQLite
   const { addCollection } = await import("./collections.js");
   addCollection(collName, pwd, globPattern);
+  resyncConfig();
 
   // Create the collection and index files
   console.log(`Creating collection '${collName}'...`);
@@ -1369,6 +1422,8 @@ function collectionRemove(name: string): void {
 
   const db = getDb();
   const result = removeCollection(db, name);
+  // Also remove from YAML config
+  yamlRemoveCollectionFn(name);
   closeDb();
 
   console.log(`${c.green}✓${c.reset} Removed collection '${name}'`);
@@ -1397,6 +1452,8 @@ function collectionRename(oldName: string, newName: string): void {
 
   const db = getDb();
   renameCollection(db, oldName, newName);
+  // Also rename in YAML config
+  yamlRenameCollectionFn(oldName, newName);
   closeDb();
 
   console.log(`${c.green}✓${c.reset} Renamed collection '${oldName}' to '${newName}'`);
@@ -1549,171 +1606,64 @@ function renderProgressBar(percent: number, width: number = 30): string {
 }
 
 async function vectorIndex(model: string = DEFAULT_EMBED_MODEL, force: boolean = false): Promise<void> {
-  const db = getDb();
-  const now = new Date().toISOString();
+  const storeInstance = getStore();
+  const db = storeInstance.db;
 
-  // If force, clear all vectors
   if (force) {
     console.log(`${c.yellow}Force re-indexing: clearing all vectors...${c.reset}`);
-    clearAllEmbeddings(db);
   }
 
-  // Find unique hashes that need embedding (from active documents)
+  // Check if there's work to do before starting
   const hashesToEmbed = getHashesForEmbedding(db);
-
-  if (hashesToEmbed.length === 0) {
+  if (hashesToEmbed.length === 0 && !force) {
     console.log(`${c.green}✓ All content hashes already have embeddings.${c.reset}`);
     closeDb();
     return;
   }
 
-  // Prepare documents with chunks
-  type ChunkItem = { hash: string; title: string; text: string; seq: number; pos: number; tokens: number; bytes: number; displayName: string };
-  const allChunks: ChunkItem[] = [];
-  let multiChunkDocs = 0;
-
-  // Chunk all documents using actual token counts
-  process.stderr.write(`Chunking ${hashesToEmbed.length} documents by token count...\n`);
-  for (const item of hashesToEmbed) {
-    const encoder = new TextEncoder();
-    const bodyBytes = encoder.encode(item.body).length;
-    if (bodyBytes === 0) continue; // Skip empty
-
-    const title = extractTitle(item.body, item.path);
-    const displayName = item.path;
-    const chunks = await chunkDocumentByTokens(item.body);  // Uses actual tokenizer
-
-    if (chunks.length > 1) multiChunkDocs++;
-
-    for (let seq = 0; seq < chunks.length; seq++) {
-      allChunks.push({
-        hash: item.hash,
-        title,
-        text: chunks[seq]!.text, // Chunk is guaranteed to exist by seq loop
-        seq,
-        pos: chunks[seq]!.pos,
-        tokens: chunks[seq]!.tokens,
-        bytes: encoder.encode(chunks[seq]!.text).length,
-        displayName,
-      });
-    }
-  }
-
-  if (allChunks.length === 0) {
-    console.log(`${c.green}✓ No non-empty documents to embed.${c.reset}`);
-    closeDb();
-    return;
-  }
-
-  const totalBytes = allChunks.reduce((sum, chk) => sum + chk.bytes, 0);
-  const totalChunks = allChunks.length;
-  const totalDocs = hashesToEmbed.length;
-
-  console.log(`${c.bold}Embedding ${totalDocs} documents${c.reset} ${c.dim}(${totalChunks} chunks, ${formatBytes(totalBytes)})${c.reset}`);
-  if (multiChunkDocs > 0) {
-    console.log(`${c.dim}${multiChunkDocs} documents split into multiple chunks${c.reset}`);
-  }
   console.log(`${c.dim}Model: ${model}${c.reset}\n`);
-
-  // Hide cursor during embedding
   cursor.hide();
+  progress.indeterminate();
 
-  // Wrap all LLM embedding operations in a session for lifecycle management
-  // Use 30 minute timeout for large collections
-  await withLLMSession(async (session) => {
-    // Get embedding dimensions from first chunk
-    progress.indeterminate();
-    const firstChunk = allChunks[0];
-    if (!firstChunk) {
-      throw new Error("No chunks available to embed");
-    }
-    const firstText = formatDocForEmbedding(firstChunk.text, firstChunk.title);
-    const firstResult = await session.embed(firstText);
-    if (!firstResult) {
-      throw new Error("Failed to get embedding dimensions from first chunk");
-    }
-    ensureVecTable(db, firstResult.embedding.length);
-
-    let chunksEmbedded = 0, errors = 0, bytesProcessed = 0;
-    const startTime = Date.now();
-
-    // Batch embedding for better throughput
-    // Process in batches of 32 to balance memory usage and efficiency
-    const BATCH_SIZE = 32;
-
-    for (let batchStart = 0; batchStart < allChunks.length; batchStart += BATCH_SIZE) {
-      const batchEnd = Math.min(batchStart + BATCH_SIZE, allChunks.length);
-      const batch = allChunks.slice(batchStart, batchEnd);
-
-      // Format texts for embedding
-      const texts = batch.map(chunk => formatDocForEmbedding(chunk.text, chunk.title));
-
-      try {
-        // Batch embed all texts at once
-        const embeddings = await session.embedBatch(texts);
-
-        // Insert each embedding
-        for (let i = 0; i < batch.length; i++) {
-          const chunk = batch[i]!;
-          const embedding = embeddings[i];
-
-          if (embedding) {
-            insertEmbedding(db, chunk.hash, chunk.seq, chunk.pos, new Float32Array(embedding.embedding), model, now);
-            chunksEmbedded++;
-          } else {
-            errors++;
-            console.error(`\n${c.yellow}⚠ Error embedding "${chunk.displayName}" chunk ${chunk.seq}${c.reset}`);
-          }
-          bytesProcessed += chunk.bytes;
-        }
-      } catch (err) {
-        // If batch fails, try individual embeddings as fallback
-        for (const chunk of batch) {
-          try {
-            const text = formatDocForEmbedding(chunk.text, chunk.title);
-            const result = await session.embed(text);
-            if (result) {
-              insertEmbedding(db, chunk.hash, chunk.seq, chunk.pos, new Float32Array(result.embedding), model, now);
-              chunksEmbedded++;
-            } else {
-              errors++;
-            }
-          } catch (innerErr) {
-            errors++;
-            console.error(`\n${c.yellow}⚠ Error embedding "${chunk.displayName}" chunk ${chunk.seq}: ${innerErr}${c.reset}`);
-          }
-          bytesProcessed += chunk.bytes;
-        }
-      }
+  const startTime = Date.now();
 
-      const percent = (bytesProcessed / totalBytes) * 100;
+  const result = await generateEmbeddings(storeInstance, {
+    force,
+    model,
+    onProgress: (info) => {
+      if (info.totalBytes === 0) return;
+      const percent = (info.bytesProcessed / info.totalBytes) * 100;
       progress.set(percent);
 
       const elapsed = (Date.now() - startTime) / 1000;
-      const bytesPerSec = bytesProcessed / elapsed;
-      const remainingBytes = totalBytes - bytesProcessed;
+      const bytesPerSec = info.bytesProcessed / elapsed;
+      const remainingBytes = info.totalBytes - info.bytesProcessed;
       const etaSec = remainingBytes / bytesPerSec;
 
       const bar = renderProgressBar(percent);
       const percentStr = percent.toFixed(0).padStart(3);
       const throughput = `${formatBytes(bytesPerSec)}/s`;
       const eta = elapsed > 2 ? formatETA(etaSec) : "...";
-      const errStr = errors > 0 ? ` ${c.yellow}${errors} err${c.reset}` : "";
+      const errStr = info.errors > 0 ? ` ${c.yellow}${info.errors} err${c.reset}` : "";
 
-      if (isTTY) process.stderr.write(`\r${c.cyan}${bar}${c.reset} ${c.bold}${percentStr}%${c.reset} ${c.dim}${chunksEmbedded}/${totalChunks}${c.reset}${errStr} ${c.dim}${throughput} ETA ${eta}${c.reset}   `);
-    }
+      if (isTTY) process.stderr.write(`\r${c.cyan}${bar}${c.reset} ${c.bold}${percentStr}%${c.reset} ${c.dim}${info.chunksEmbedded}/${info.totalChunks}${c.reset}${errStr} ${c.dim}${throughput} ETA ${eta}${c.reset}   `);
+    },
+  });
 
-    progress.clear();
-    cursor.show();
-    const totalTimeSec = (Date.now() - startTime) / 1000;
-    const avgThroughput = formatBytes(totalBytes / totalTimeSec);
+  progress.clear();
+  cursor.show();
 
+  const totalTimeSec = result.durationMs / 1000;
+
+  if (result.chunksEmbedded === 0 && result.docsProcessed === 0) {
+    console.log(`${c.green}✓ No non-empty documents to embed.${c.reset}`);
+  } else {
     console.log(`\r${c.green}${renderProgressBar(100)}${c.reset} ${c.bold}100%${c.reset}                                    `);
-    console.log(`\n${c.green}✓ Done!${c.reset} Embedded ${c.bold}${chunksEmbedded}${c.reset} chunks from ${c.bold}${totalDocs}${c.reset} documents in ${c.bold}${formatETA(totalTimeSec)}${c.reset} ${c.dim}(${avgThroughput}/s)${c.reset}`);
-    if (errors > 0) {
-      console.log(`${c.yellow}⚠ ${errors} chunks failed${c.reset}`);
+    console.log(`\n${c.green}✓ Done!${c.reset} Embedded ${c.bold}${result.chunksEmbedded}${c.reset} chunks from ${c.bold}${result.docsProcessed}${c.reset} documents in ${c.bold}${formatETA(totalTimeSec)}${c.reset}`);
+    if (result.errors > 0) {
+      console.log(`${c.yellow}⚠ ${result.errors} chunks failed${c.reset}`);
     }
-  }, { maxDuration: 30 * 60 * 1000, name: 'embed-command' });
+  }
 
   closeDb();
 }
@@ -2028,7 +1978,7 @@ function filterByCollections<T extends { filepath?: string; file?: string }>(res
  * Plain lines without prefix go through query expansion.
  * 
  * Returns null if this is a plain query (single line, no prefix).
- * Returns StructuredSubSearch[] if structured syntax detected.
+ * Returns ExpandedQuery[] if structured syntax detected.
  * Throws if multiple plain lines (ambiguous).
  * 
  * Examples:
@@ -2038,7 +1988,7 @@ function filterByCollections<T extends { filepath?: string; file?: string }>(res
  *   "CAP\nconsistency"               -> throws (multiple plain lines)
  */
 interface ParsedStructuredQuery {
-  searches: StructuredSubSearch[];
+  searches: ExpandedQuery[];
   intent?: string;
 }
 
@@ -2054,7 +2004,7 @@ function parseStructuredQuery(query: string): ParsedStructuredQuery | null {
   const prefixRe = /^(lex|vec|hyde):\s*/i;
   const expandRe = /^expand:\s*/i;
   const intentRe = /^intent:\s*/i;
-  const typed: StructuredSubSearch[] = [];
+  const typed: ExpandedQuery[] = [];
   let intent: string | undefined;
 
   for (const line of rawLines) {
@@ -2153,7 +2103,7 @@ function logExpansionTree(originalQuery: string, expanded: ExpandedQuery[]): voi
   const lines: string[] = [];
   lines.push(`${c.dim}├─ ${originalQuery}${c.reset}`);
   for (const q of expanded) {
-    let preview = q.text.replace(/\n/g, ' ');
+    let preview = q.query.replace(/\n/g, ' ');
     if (preview.length > 72) preview = preview.substring(0, 69) + '...';
     lines.push(`${c.dim}├─ ${q.type}: ${preview}${c.reset}`);
   }

Різницю між файлами не показано, бо вона завелика
+ 579 - 94
src/store.ts


+ 3 - 3
test/intent.test.ts

@@ -18,7 +18,7 @@ import {
   extractIntentTerms,
   INTENT_WEIGHT_SNIPPET,
   INTENT_WEIGHT_CHUNK,
-  type StructuredSubSearch,
+  type ExpandedQuery,
 } from "../src/store.js";
 
 // =============================================================================
@@ -27,7 +27,7 @@ import {
 // =============================================================================
 
 interface ParsedStructuredQuery {
-  searches: StructuredSubSearch[];
+  searches: ExpandedQuery[];
   intent?: string;
 }
 
@@ -43,7 +43,7 @@ function parseStructuredQuery(query: string): ParsedStructuredQuery | null {
   const prefixRe = /^(lex|vec|hyde):\s*/i;
   const expandRe = /^expand:\s*/i;
   const intentRe = /^intent:\s*/i;
-  const typed: StructuredSubSearch[] = [];
+  const typed: ExpandedQuery[] = [];
   let intent: string | undefined;
 
   for (const line of rawLines) {

+ 35 - 8
test/mcp.test.ts

@@ -18,6 +18,7 @@ import { tmpdir } from "node:os";
 import YAML from "yaml";
 import type { CollectionConfig } from "../src/collections";
 import { setConfigIndexName } from "../src/collections";
+import { syncConfigToDb } from "../src/store";
 
 // =============================================================================
 // Test Database Setup
@@ -104,6 +105,26 @@ function initTestDatabase(db: Database): void {
 
   // Create vector table
   db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS vectors_vec USING vec0(hash_seq TEXT PRIMARY KEY, embedding float[768] distance_metric=cosine)`);
+
+  // Store collections — makes the DB self-contained
+  db.exec(`
+    CREATE TABLE IF NOT EXISTS store_collections (
+      name TEXT PRIMARY KEY,
+      path TEXT NOT NULL,
+      pattern TEXT NOT NULL DEFAULT '**/*.md',
+      ignore_patterns TEXT,
+      include_by_default INTEGER DEFAULT 1,
+      update_command TEXT,
+      context TEXT
+    )
+  `);
+
+  db.exec(`
+    CREATE TABLE IF NOT EXISTS store_config (
+      key TEXT PRIMARY KEY,
+      value TEXT
+    )
+  `);
 }
 
 function seedTestData(db: Database): void {
@@ -234,6 +255,9 @@ describe("MCP Server", () => {
     testDb = openDatabase(testDbPath);
     initTestDatabase(testDb);
     seedTestData(testDb);
+
+    // Sync YAML config into SQLite store_collections
+    syncConfigToDb(testDb, testConfig);
   });
 
   afterAll(async () => {
@@ -332,7 +356,7 @@ describe("MCP Server", () => {
       expect(expanded.length).toBeGreaterThanOrEqual(1);
       for (const q of expanded) {
         expect(['lex', 'vec', 'hyde']).toContain(q.type);
-        expect(q.text.length).toBeGreaterThan(0);
+        expect(q.query.length).toBeGreaterThan(0);
       }
     }, 30000); // 30s timeout for model loading
 
@@ -382,7 +406,7 @@ describe("MCP Server", () => {
       // Expanded queries → route by type: lex→FTS, vec/hyde skipped (no vectors in test)
       for (const q of expanded) {
         if (q.type === 'lex') {
-          const ftsResults = searchFTS(testDb, q.text, 20);
+          const ftsResults = searchFTS(testDb, q.query, 20);
           if (ftsResults.length > 0) {
             rankedLists.push(ftsResults.map(r => ({
               file: r.filepath, displayPath: r.displayPath,
@@ -888,12 +912,9 @@ describe("MCP HTTP Transport", () => {
     const db = openDatabase(httpTestDbPath);
     initTestDatabase(db);
     seedTestData(db);
-    db.close();
 
-    // Create isolated YAML config
-    const configPrefix = join(tmpdir(), `qmd-mcp-http-config-${Date.now()}-${Math.random().toString(36).slice(2)}`);
-    httpTestConfigDir = await mkdtemp(configPrefix);
-    const testConfig: CollectionConfig = {
+    // Sync config into SQLite
+    const httpTestConfig: CollectionConfig = {
       collections: {
         docs: {
           path: "/test/docs",
@@ -901,7 +922,13 @@ describe("MCP HTTP Transport", () => {
         }
       }
     };
-    await writeFile(join(httpTestConfigDir, "index.yml"), YAML.stringify(testConfig));
+    syncConfigToDb(db, httpTestConfig);
+    db.close();
+
+    // Create isolated YAML config
+    const configPrefix = join(tmpdir(), `qmd-mcp-http-config-${Date.now()}-${Math.random().toString(36).slice(2)}`);
+    httpTestConfigDir = await mkdtemp(configPrefix);
+    await writeFile(join(httpTestConfigDir, "index.yml"), YAML.stringify(httpTestConfig));
 
     // Point createStore() at our test DB
     process.env.INDEX_PATH = httpTestDbPath;

Різницю між файлами не показано, бо вона завелика
+ 412 - 175
test/sdk.test.ts


+ 23 - 3
test/store.test.ts

@@ -44,6 +44,7 @@ import {
   parseVirtualPath,
   normalizeDocid,
   isDocid,
+  syncConfigToDb,
   STRONG_SIGNAL_MIN_SCORE,
   STRONG_SIGNAL_MIN_GAP,
   type Store,
@@ -67,6 +68,7 @@ import type { CollectionConfig } from "../src/collections.js";
 let testDir: string;
 let testDbPath: string;
 let testConfigDir: string;
+let currentTestStore: Store | null = null;
 
 async function createTestStore(): Promise<Store> {
   testDbPath = join(testDir, `test-${Date.now()}-${Math.random().toString(36).slice(2)}.sqlite`);
@@ -85,10 +87,13 @@ async function createTestStore(): Promise<Store> {
     YAML.stringify(emptyConfig)
   );
 
-  return createStore(testDbPath);
+  const store = createStore(testDbPath);
+  currentTestStore = store;
+  return store;
 }
 
 async function cleanupTestDb(store: Store): Promise<void> {
+  currentTestStore = null;
   store.close();
   try {
     await unlink(store.dbPath);
@@ -163,6 +168,18 @@ async function insertTestDocument(
   return Number(result.lastInsertRowid);
 }
 
+/** Sync YAML config file to SQLite store_collections in the current test store */
+async function syncTestConfig(): Promise<void> {
+  if (!currentTestStore) return;
+  const configPath = join(testConfigDir, "index.yml");
+  const { readFile } = await import("node:fs/promises");
+  const content = await readFile(configPath, "utf-8");
+  const config = YAML.parse(content) as CollectionConfig;
+  // Clear config hash to force re-sync
+  currentTestStore.db.prepare(`DELETE FROM store_config WHERE key = 'config_hash'`).run();
+  syncConfigToDb(currentTestStore.db, config);
+}
+
 // Helper to create a test collection in YAML config
 async function createTestCollection(
   options: { pwd?: string; glob?: string; name?: string } = {}
@@ -185,6 +202,7 @@ async function createTestCollection(
 
   // Write back
   await writeFile(configPath, YAML.stringify(config));
+  await syncTestConfig();
   return name;
 }
 
@@ -209,6 +227,7 @@ async function addPathContext(collectionName: string, pathPrefix: string, contex
 
   // Write back
   await writeFile(configPath, YAML.stringify(config));
+  await syncTestConfig();
 }
 
 // Helper to add global context in YAML config
@@ -221,6 +240,7 @@ async function addGlobalContext(contextText: string): Promise<void> {
   config.global_context = contextText;
 
   await writeFile(configPath, YAML.stringify(config));
+  await syncTestConfig();
 }
 
 // =============================================================================
@@ -2376,8 +2396,8 @@ describe.skipIf(!!process.env.CI)("LlamaCpp Integration", () => {
     expect(expanded.length).toBeGreaterThanOrEqual(1);
     for (const q of expanded) {
       expect(['lex', 'vec', 'hyde']).toContain(q.type);
-      expect(q.text.length).toBeGreaterThan(0);
-      expect(q.text).not.toBe("test query"); // original excluded
+      expect(q.query.length).toBeGreaterThan(0);
+      expect(q.query).not.toBe("test query"); // original excluded
     }
 
     await cleanupTestDb(store);

+ 9 - 9
test/structured-search.test.ts

@@ -3,7 +3,7 @@
  *
  * Tests cover:
  * - CLI query parser (parseStructuredQuery)
- * - StructuredSubSearch type validation
+ * - ExpandedQuery type validation
  * - Basic structuredSearch function behavior
  *
  * Run with: bun test structured-search.test.ts
@@ -18,7 +18,7 @@ import {
   structuredSearch,
   validateSemanticQuery,
   validateLexQuery,
-  type StructuredSubSearch,
+  type ExpandedQuery,
   type Store,
 } from "../src/store.js";
 import { disposeDefaultLlamaCpp } from "../src/llm.js";
@@ -27,7 +27,7 @@ import { disposeDefaultLlamaCpp } from "../src/llm.js";
 // parseStructuredQuery Tests (CLI Parser)
 // =============================================================================
 
-function parseStructuredQuery(query: string): StructuredSubSearch[] | null {
+function parseStructuredQuery(query: string): ExpandedQuery[] | null {
   const rawLines = query.split('\n').map((line, idx) => ({
     raw: line,
     trimmed: line.trim(),
@@ -38,7 +38,7 @@ function parseStructuredQuery(query: string): StructuredSubSearch[] | null {
 
   const prefixRe = /^(lex|vec|hyde):\s*/i;
   const expandRe = /^expand:\s*/i;
-  const typed: StructuredSubSearch[] = [];
+  const typed: ExpandedQuery[] = [];
 
   for (const line of rawLines) {
     if (expandRe.test(line.trimmed)) {
@@ -253,24 +253,24 @@ describe("parseStructuredQuery", () => {
 });
 
 // =============================================================================
-// StructuredSubSearch Type Tests
+// ExpandedQuery Type Tests
 // =============================================================================
 
-describe("StructuredSubSearch type", () => {
+describe("ExpandedQuery type", () => {
   test("accepts lex type", () => {
-    const search: StructuredSubSearch = { type: "lex", query: "test" };
+    const search: ExpandedQuery = { type: "lex", query: "test" };
     expect(search.type).toBe("lex");
     expect(search.query).toBe("test");
   });
 
   test("accepts vec type", () => {
-    const search: StructuredSubSearch = { type: "vec", query: "test" };
+    const search: ExpandedQuery = { type: "vec", query: "test" };
     expect(search.type).toBe("vec");
     expect(search.query).toBe("test");
   });
 
   test("accepts hyde type", () => {
-    const search: StructuredSubSearch = { type: "hyde", query: "test" };
+    const search: ExpandedQuery = { type: "hyde", query: "test" };
     expect(search.type).toBe("hyde");
     expect(search.query).toBe("test");
   });

Деякі файли не було показано, через те що забагато файлів було змінено