Class: ClaudeMemory::Index::LexicalFTS
- Inherits:
-
Object
- Object
- ClaudeMemory::Index::LexicalFTS
- Defined in:
- lib/claude_memory/index/lexical_fts.rb
Defined Under Namespace
Classes: CorruptRankIndexError
Constant Summary collapse
- RANK_CORRUPTION_HINT =
"FTS5 rank index is corrupt — recall is broken even though " \ "the database otherwise looks healthy. Run `claude-memory compact` to rebuild it."
Instance Method Summary collapse
- #escape_fts_query(query) ⇒ Object
- #index_content_item(content_item_id, text) ⇒ Object
-
#initialize(store) ⇒ LexicalFTS
constructor
A new instance of LexicalFTS.
-
#rebuild! ⇒ Object
Rebuild the entire FTS index from content_items.
-
#remove_content_item(content_item_id, text) ⇒ Object
Remove a content item from the FTS index.
- #search(query, limit: 20) ⇒ Object
-
#search_with_ranks(query, limit: 20) ⇒ Array<Hash>
Search returning content IDs with FTS5 BM25 rank values.
Constructor Details
#initialize(store) ⇒ LexicalFTS
Returns a new instance of LexicalFTS.
15 16 17 18 19 20 |
# File 'lib/claude_memory/index/lexical_fts.rb', line 15 def initialize(store) @store = store @db = store.db @fts_table_ensured = false @contentless = nil end |
Instance Method Details
#escape_fts_query(query) ⇒ Object
120 121 122 123 124 125 126 127 128 129 |
# File 'lib/claude_memory/index/lexical_fts.rb', line 120 def escape_fts_query(query) words = query.split(/\s+/).map do |word| next word if word == "*" escaped = word.gsub('"', '""') %("#{escaped}") end.compact return words.first if words.size == 1 words.join(" OR ") end |
#index_content_item(content_item_id, text) ⇒ Object
22 23 24 25 26 27 28 29 30 31 32 33 |
# File 'lib/claude_memory/index/lexical_fts.rb', line 22 def index_content_item(content_item_id, text) ensure_fts_table! if contentless? existing = @db.fetch("SELECT rowid FROM content_fts WHERE rowid = ?", content_item_id).first return if existing @db.fetch("INSERT INTO content_fts(rowid, text) VALUES (?, ?)", content_item_id, text).insert else existing = @db[:content_fts].where(content_item_id: content_item_id).get(:content_item_id) return if existing @db[:content_fts].insert(content_item_id: content_item_id, text: text) end end |
#rebuild! ⇒ Object
Rebuild the entire FTS index from content_items. Always rebuilds as contentless to save space.
105 106 107 108 109 110 111 112 113 114 115 116 117 118 |
# File 'lib/claude_memory/index/lexical_fts.rb', line 105 def rebuild! @db.run("DROP TABLE IF EXISTS content_fts") @fts_table_ensured = false @contentless = nil create_contentless_table! @db[:content_items].select(:id, :raw_text).order(:id).paged_each(rows_per_fetch: 500) do |row| @db.fetch( "INSERT INTO content_fts(rowid, text) VALUES (?, ?)", row[:id], row[:raw_text] ).insert end end |
#remove_content_item(content_item_id, text) ⇒ Object
Remove a content item from the FTS index
91 92 93 94 95 96 97 98 99 100 101 |
# File 'lib/claude_memory/index/lexical_fts.rb', line 91 def remove_content_item(content_item_id, text) ensure_fts_table! if contentless? @db.fetch( "INSERT INTO content_fts(content_fts, rowid, text) VALUES('delete', ?, ?)", content_item_id, text ).insert else @db[:content_fts].where(content_item_id: content_item_id).delete end end |
#search(query, limit: 20) ⇒ Object
35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
# File 'lib/claude_memory/index/lexical_fts.rb', line 35 def search(query, limit: 20) ensure_fts_table! return [] if query.nil? || query.strip.empty? if query.strip == "*" return @db[:content_items] .order(Sequel.desc(:id)) .limit(limit) .select_map(:id) end escaped_query = escape_fts_query(query) with_rank_index do if contentless? @db.fetch( "SELECT rowid AS content_item_id FROM content_fts WHERE text MATCH ? ORDER BY rank LIMIT ?", escaped_query, limit ).map { |row| row[:content_item_id] } else @db[:content_fts] .where(Sequel.lit("text MATCH ?", escaped_query)) .order(:rank) .limit(limit) .select_map(:content_item_id) end end end |
#search_with_ranks(query, limit: 20) ⇒ Array<Hash>
Search returning content IDs with FTS5 BM25 rank values
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 |
# File 'lib/claude_memory/index/lexical_fts.rb', line 67 def search_with_ranks(query, limit: 20) ensure_fts_table! return [] if query.nil? || query.strip.empty? return [] if query.strip == "*" escaped_query = escape_fts_query(query) with_rank_index do if contentless? @db.fetch( "SELECT rowid AS content_item_id, rank FROM content_fts WHERE text MATCH ? ORDER BY rank LIMIT ?", escaped_query, limit ).all else @db[:content_fts] .where(Sequel.lit("text MATCH ?", escaped_query)) .order(:rank) .limit(limit) .select(Sequel.lit("content_item_id, rank")) .all end end end |