Class: Engram::UseCases::Recall

Inherits:
Object
  • Object
show all
Defined in:
lib/engram/use_cases/recall.rb

Overview

Embed a query and fetch the most relevant memories for a scope.

By default this is pure vector similarity (the store’s own ordering). When importance_weight or recency_weight are non-zero, it fetches a larger candidate pool and re-ranks by a composite score: similarity + importance + recency. With both weights at zero (the default) behaviour is identical to plain similarity search.

Constant Summary collapse

DEFAULT_HALFLIFE =

30 days, in seconds

30 * 24 * 60 * 60
DEFAULT_POOL_FACTOR =
4

Instance Method Summary collapse

Constructor Details

#initialize(store:, embedder:, importance_weight: 0.0, recency_weight: 0.0, recency_halflife: DEFAULT_HALFLIFE, pool_factor: DEFAULT_POOL_FACTOR, touch: false) ⇒ Recall

Returns a new instance of Recall.



15
16
17
18
19
20
21
22
23
24
# File 'lib/engram/use_cases/recall.rb', line 15

def initialize(store:, embedder:, importance_weight: 0.0, recency_weight: 0.0,
  recency_halflife: DEFAULT_HALFLIFE, pool_factor: DEFAULT_POOL_FACTOR, touch: false)
  @store = store
  @embedder = embedder
  @importance_weight = importance_weight.to_f
  @recency_weight = recency_weight.to_f
  @recency_halflife = recency_halflife.to_f
  @pool_factor = pool_factor
  @touch = touch
end

Instance Method Details

#call(query, scope:, limit: Engram.config.default_limit, kinds: nil) ⇒ Object

Returns Array<Record>, most relevant first.

Raises:

  • (ArgumentError)


27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# File 'lib/engram/use_cases/recall.rb', line 27

def call(query, scope:, limit: Engram.config.default_limit, kinds: nil)
  raise ArgumentError, "query must be a non-empty string" if query.to_s.strip.empty?

  payload = Engram::Instrumentation.payload(
    scope: scope,
    store: @store,
    limit: limit,
    kinds: Array(kinds).map(&:to_s),
    reranking: reranking?
  )
  Engram::Instrumentation.instrument("recall", payload) do
    embedding = @embedder.embed(query)
    pool_limit = reranking? ? limit * @pool_factor : limit
    pool = @store.search(embedding: embedding, scope: scope, limit: pool_limit, kinds: kinds)

    results = (reranking? ? rerank(pool, embedding) : pool).first(limit)
    touch(results) if @touch
    payload[:result_count] = results.size
    payload[:candidate_count] = pool.size
    results
  end
end