Class: SpreeMenuChat::Retriever

Inherits:
Object
  • Object
show all
Defined in:
app/services/spree_menu_chat/retriever.rb

Overview

Embeds a customer's question and returns the top-K most similar SpreeMenuChat::Embedding rows for the store, via pgvector cosine distance — the retrieval half of RAG. SpreeMenuChat::AnswerGenerator is the only caller; kept separate so retrieval and generation each have one job.

Uses input_type: 'query' (not 'document') on the embedding call — Voyage's asymmetric-embedding hint for the search-query side of a document/query pair, per the same rationale already documented on SpreeMenuChat::EmbeddingClient#embed.

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(question, store:) ⇒ Retriever

Returns a new instance of Retriever.



17
18
19
20
# File 'app/services/spree_menu_chat/retriever.rb', line 17

def initialize(question, store:)
  @question = question
  @store = store
end

Class Method Details

.call(question, store: Spree::Store.default) ⇒ Object



13
14
15
# File 'app/services/spree_menu_chat/retriever.rb', line 13

def self.call(question, store: Spree::Store.default)
  new(question, store: store).call
end

Instance Method Details

#callObject

Returns a plain Array of SpreeMenuChat::Embedding (each with a real #neighbor_distance, populated by the nearest_neighbors scope) — empty if the question is blank, embedding it fails, or nothing clears the similarity floor. Never raises SpreeMenuChat::RequestError itself; SpreeMenuChat::AnswerGenerator treats "nothing retrieved" as the trigger for its no-context fallback regardless of why nothing came back, so a Voyage outage degrades to the same honest "I don't have that" response as a genuinely unanswerable question, not a 500.



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'app/services/spree_menu_chat/retriever.rb', line 30

def call
  return [] if @question.blank?

  query_vector = embed_question
  return [] if query_vector.nil?

  # `nearest_neighbors` is chainable (a plain `scope`, see the neighbor
  # gem) but its `neighbor_distance` is a SELECT-list alias — Postgres
  # doesn't allow referencing that in a WHERE clause on the same query,
  # so the similarity floor is applied in Ruby after fetching, not as
  # SQL. `retrieval_top_k` keeps that fetch small regardless.
  SpreeMenuChat::Embedding
    .where(store: @store)
    .nearest_neighbors(:embedding, query_vector, distance: 'cosine')
    .limit(SpreeMenuChat::Config.retrieval_top_k)
    .select { |record| record.neighbor_distance <= max_distance }
end