{"record":{"id":"a14812be9550df4d","repo":"thedotmack/claude-mem","slug":"fts5-observation-search-failed","errorCode":null,"errorMessage":"FTS5 observation search failed","messagePattern":"FTS5 observation search failed","errorType":"console","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/services/sqlite/SessionSearch.ts","lineNumber":292,"sourceCode":"\n      const sql = `\n        SELECT o.*, o.discovery_tokens\n        FROM observations o\n        JOIN observations_fts ON observations_fts.rowid = o.id\n        WHERE observations_fts MATCH ?\n        ${filterClause ? 'AND ' + filterClause : ''}\n        ${orderClause}\n        LIMIT ? OFFSET ?\n      `;\n\n      const escapedQuery = '\"' + query.replace(/\"/g, '\"\"') + '\"';\n      params.unshift(escapedQuery);\n      params.push(limit, offset);\n\n      try {\n        return this.db.prepare(sql).all(...params) as ObservationSearchResult[];\n      } catch (error) {\n        logger.warn('DB', 'FTS5 observation search failed', {}, error instanceof Error ? error : undefined);\n        throw error;\n      }\n    }\n\n    logger.warn('DB', 'Text search unavailable: ChromaDB disabled and FTS5 not available');\n    return [];\n  }\n\n  searchSessions(query: string | undefined, options: SearchOptions = {}): SessionSummarySearchResult[] {\n    const params: any[] = [];\n    const { limit = 50, offset = 0, orderBy = 'relevance', ...filters } = options;\n\n    if (!query) {\n      const filterOptions = { ...filters };\n      delete filterOptions.type;\n      const filterClause = this.buildFilterClause(filterOptions, params, 's');\n      if (!filterClause) {\n        throw new AppError(SessionSearch.MISSING_SEARCH_INPUT_MESSAGE, 400, 'INVALID_SEARCH_REQUEST');","sourceCodeStart":274,"sourceCodeEnd":310,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/e2d1df569a8f04075d40e92461128ece7cf04c82/src/services/sqlite/SessionSearch.ts#L274-L310","documentation":"An FTS5 MATCH query against the observations full-text index threw. SessionSearch logs this warning and rethrows, so callers of searchObservations() receive the raw SQLite error. Usual root causes: the FTS table was never created (the companion 'FTS5 table creation failed' warning fired at startup), a corrupt FTS index, or SQLITE_BUSY from a concurrent writer.","triggerScenarios":"Calling searchObservations(query, ...) with a non-empty query while the FTS table or its triggers are missing/corrupt despite _fts5Available being true; a writer holding an exclusive lock while the MATCH runs; the FTS table dropped manually after init.","commonSituations":"FTS creation failed silently in an earlier session; two workers sharing one DB file with one writing while the other searches; a DB restored from a partial backup missing shadow tables.","solutions":["Scan the same log for the earlier 'FTS5 table creation failed' / 'FTS5 not available' warnings — they are the usual root cause","Confirm the FTS table exists: SELECT name FROM sqlite_master WHERE type='table' AND name LIKE '%fts%';","If missing or corrupt, drop the FTS tables plus shadow tables and restart so initializeFTS() recreates them","If SQLITE_BUSY: reduce concurrent writers or raise busy_timeout before searching","Wrap callers with a fallback to ChromaDB or LIKE-based search on failure"],"exampleFix":"// before\nconst rows = search.searchObservations(q, { limit: 20 });\n\n// after — degrade to non-FTS search when the MATCH fails\nlet rows: ObservationSearchResult[];\ntry {\n  rows = search.searchObservations(q, { limit: 20 });\n} catch (err) {\n  logger.warn('DB', 'FTS observation search failed; falling back', {}, err instanceof Error ? err : undefined);\n  rows = search.searchObservations(undefined, { limit: 20 }); // undefined query skips the FTS path\n}","handlingStrategy":"try-catch","validationCode":"const hasFts = !!db\n  .prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name LIKE '%fts%'`)\n  .get();\nif (!hasFts) {\n  // skip the text-search path; use ChromaDB or LIKE directly\n}","typeGuard":null,"tryCatchPattern":"try {\n  rows = search.searchObservations(q, { limit });\n} catch (err) {\n  logger.warn('DB', 'FTS observation search failed; using fallback', {}, err instanceof Error ? err : undefined);\n  rows = search.searchObservations(undefined, { limit }); // undefined query skips FTS\n}","preventionTips":["Confirm the 'FTS5 tables created successfully' log line before advertising text-search features","Avoid concurrent exclusive writers or set busy_timeout so MATCH queries do not hit SQLITE_BUSY","Never drop FTS tables manually without also resetting the availability flag"],"tags":["sqlite","fts5","full-text-search","rethrow"],"backgroundTag":"fts5-query-failed","analyzedSha":"e2d1df569a8f04075d40e92461128ece7cf04c82","analyzedAt":"2026-08-20T23:58:13.836Z","contentChangedAt":"2026-08-20T23:58:13.836Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}