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