Class: Llmemory::VectorStore::ActiveRecordStore

Inherits:
Base
  • Object
show all
Defined in:
lib/llmemory/vector_store/active_record_store.rb

Overview

Persists embeddings in llmemory_embeddings (pgvector). Use when long_term_store is :active_record so hybrid search finds persisted embeddings.

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(embedding_provider: nil) ⇒ ActiveRecordStore

Returns a new instance of ActiveRecordStore.



10
11
12
13
# File 'lib/llmemory/vector_store/active_record_store.rb', line 10

def initialize(embedding_provider: nil)
  self.class.load_model!
  @embedding_provider = embedding_provider
end

Class Method Details

.load_model!Object



15
16
17
18
19
20
# File 'lib/llmemory/vector_store/active_record_store.rb', line 15

def self.load_model!
  return if @model_loaded
  require "active_record"
  require_relative "active_record_embedding"
  @model_loaded = true
end

Instance Method Details

#embed(text) ⇒ Object



22
23
24
25
# File 'lib/llmemory/vector_store/active_record_store.rb', line 22

def embed(text)
  return Array.new(1536, 0.0) unless @embedding_provider&.respond_to?(:embed)
  @embedding_provider.embed(text)
end

#search(query_embedding, top_k: 10, user_id: nil) ⇒ Object



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/llmemory/vector_store/active_record_store.rb', line 41

def search(query_embedding, top_k: 10, user_id: nil)
  return [] if user_id.nil? || user_id.to_s.empty?
  vec = query_embedding.to_a.map(&:to_f)
  return [] if vec.empty?
  # Sanitize vector for pgvector (only floats allowed)
  sanitized_vec = vec.map { |v| v.finite? ? v : 0.0 }
  vector_literal = "[#{sanitized_vec.join(',')}]"
  # pgvector cosine distance <=> (0 = same, 2 = opposite); score = 1 - distance for similarity
  scope = Llmemory::VectorStore::ActiveRecordEmbedding.where(user_id: user_id.to_s)
  rows = scope.select(
    Llmemory::VectorStore::ActiveRecordEmbedding.arel_table[Arel.star],
    Arel.sql("(embedding <=> '#{vector_literal}'::vector) AS distance")
  ).order(Arel.sql("embedding <=> '#{vector_literal}'::vector")).limit(top_k)
  rows.map do |r|
    distance = r["distance"] || r.attributes["distance"] || 0.0
    score = (1.0 - distance.to_f).clamp(-1.0, 1.0)
    {
      id: r.source_id,
      score: score,
      metadata: { "text" => r.text_content, "created_at" => r.created_at, "user_id" => r.user_id }
    }
  end
end

#search_by_text(query_text, top_k: 10, user_id: nil) ⇒ Object



65
66
67
68
69
70
# File 'lib/llmemory/vector_store/active_record_store.rb', line 65

def search_by_text(query_text, top_k: 10, user_id: nil)
  return [] if user_id.nil? || user_id.to_s.empty?
  return [] unless @embedding_provider&.respond_to?(:embed)
  query_embedding = @embedding_provider.embed(query_text)
  search(query_embedding, top_k: top_k, user_id: user_id)
end

#store(id:, embedding:, metadata: {}, user_id: nil) ⇒ Object



27
28
29
30
31
32
33
34
35
36
37
38
39
# File 'lib/llmemory/vector_store/active_record_store.rb', line 27

def store(id:, embedding:, metadata: {}, user_id: nil)
  return id if user_id.nil? || user_id.to_s.empty?
  text_content = ( || {}).dig("text") || ( || {}).dig(:text)
  rec = Llmemory::VectorStore::ActiveRecordEmbedding.find_or_initialize_by(
    user_id: user_id.to_s,
    source_type: "edge",
    source_id: id.to_s
  )
  rec.embedding = embedding.to_a.map(&:to_f)
  rec.text_content = text_content
  rec.save!
  id
end