Skip to main content

lib/agent_sea/vector_store/memory.ex

defmodule AgentSea.VectorStore.Memory do
  @moduledoc """
  In-memory vector store backed by a `GenServer`. Brute-force cosine-similarity
  search — fine for tests, small corpora, and demos. Records are keyed by id.
  """

  @behaviour AgentSea.VectorStore
  use GenServer

  # --- Client ---

  def start_link(opts \\ []) do
    GenServer.start_link(__MODULE__, %{}, opts)
  end

  @impl AgentSea.VectorStore
  def upsert(store, records), do: GenServer.call(store, {:upsert, records})

  @impl AgentSea.VectorStore
  def query(store, vector, k, opts \\ []), do: GenServer.call(store, {:query, vector, k, opts})

  @impl AgentSea.VectorStore
  def delete(store, ids), do: GenServer.call(store, {:delete, ids})

  @impl AgentSea.VectorStore
  def count(store), do: GenServer.call(store, :count)

  # --- Server ---

  @impl GenServer
  def init(_), do: {:ok, %{records: %{}}}

  @impl GenServer
  def handle_call({:upsert, records}, _from, state) do
    records =
      Enum.reduce(records, state.records, fn record, acc ->
        Map.put(acc, record.id, normalize_record(record))
      end)

    {:reply, :ok, %{state | records: records}}
  end

  def handle_call({:query, vector, k, opts}, _from, state) do
    filter = Keyword.get(opts, :filter, fn _metadata -> true end)

    hits =
      state.records
      |> Map.values()
      |> Enum.filter(fn record -> filter.(record.metadata) end)
      |> Enum.map(fn record ->
        %{
          id: record.id,
          score: AgentSea.Vector.cosine(vector, record.vector),
          metadata: record.metadata,
          text: record.text
        }
      end)
      |> Enum.sort_by(& &1.score, :desc)
      |> Enum.take(k)

    {:reply, hits, state}
  end

  def handle_call({:delete, ids}, _from, state) do
    {:reply, :ok, %{state | records: Map.drop(state.records, ids)}}
  end

  def handle_call(:count, _from, state) do
    {:reply, map_size(state.records), state}
  end

  defp normalize_record(record) do
    %{
      id: record.id,
      vector: record.vector,
      metadata: Map.get(record, :metadata, %{}),
      text: Map.get(record, :text)
    }
  end
end