Skip to content

feat(memory): add self-hosted PostgreSQL/pgvector memory service - #7274

Closed
Nanduu24 wants to merge 2 commits into
google:mainfrom
Nanduu24:feat/pgvector-memory-service
Closed

Nanduu24 wants to merge 2 commits into
google:mainfrom
Nanduu24:feat/pgvector-memory-service

Conversation

@Nanduu24

@Nanduu24 Nanduu24 commented Sep 25, 2026 •

Copy link
Copy Markdown

Link to Issue or Description of Change

Problem:

ADK's memory services do not cover persistent, semantic memory on
infrastructure the user controls. InMemoryMemoryService is keyword-only and
"for prototyping purpose only" (loses data on restart), while
VertexAiMemoryBankService and VertexAiRagMemoryService require Google Cloud.
Teams running on-premise, air-gapped, or under data-residency constraints have
no built-in option, and the pending SQLite memory service (#4116) provides
persistence but exact-match retrieval rather than semantic search.

Solution:

Add PgVectorMemoryService under google.adk.integrations.pgvector, a
BaseMemoryService backed by PostgreSQL + pgvector:

  • add_session_to_memory / add_events_to_memory embed each event's text and
    upsert it (idempotent per event) into a table scoped by (app_name, user_id).
  • add_memory supports direct writes of explicit MemoryEntry items.
  • search_memory embeds the query and returns the nearest memories by cosine
    distance using a pgvector HNSW index, with an optional distance_threshold.
  • Embeddings use the google-genai client by default; an injectable embedder
    makes the service provider-agnostic.
  • Ships as an optional extra: pip install "google-adk[pgvector]".

It follows the existing integrations/redis pattern (pydantic config plus an
injectable connection pool and embedder), so the store runs entirely on a
database the user owns and stays unit-testable without a live database. The
table, vector extension, and indexes are created on first use. Happy to align
the table layout with #4116 if a shared schema is preferred.

Testing Plan

Unit Tests:

  • I have added or updated unit tests for my change.
  • All unit tests pass locally.

New tests in tests/unittests/integrations/pgvector/ run against an in-process
fake connection pool and a deterministic bag-of-words embedder, so they exercise
ingestion, idempotent upserts, semantic ranking, per-(app_name, user_id)
scoping, the distance threshold, empty-query handling, skipping text-less
events, the direct-write path, serialization round-trips, and the
missing-driver / missing-dsn errors — all without psycopg, pgvector, or any
network access.

$ python -m pytest tests/unittests/integrations/pgvector -q
13 passed

The suite passes both with the optional driver installed and with it absent
(matching CI). Lint/format on the changed files: pyink and isort clean,
pylint 10.00/10.

Manual End-to-End (E2E) Tests:

Verified against a live PostgreSQL 17 + pgvector 0.8.6 database using the
real psycopg async driver (a deterministic local embedder stood in for the
embedding API). Confirmed: session ingestion, semantic ranking (a "billing"
query returns the billing turn; a "basketball" query returns the basketball
turn), cross-instance persistence (a fresh service reads the stored rows back),
(app_name, user_id) scoping, idempotent re-ingestion (row count stays at 3
after a double ingest), and the created schema (a vector(64) column and an
HNSW index). To reproduce:

docker run -d --name adk-pgvector -e POSTGRES_PASSWORD=pw -p 5432:5432 pgvector/pgvector:pg16
pip install "google-adk[pgvector]"
export GOOGLE_API_KEY=...   # for gemini-embedding-001
import asyncio
from google.adk.integrations.pgvector import (
    PgVectorMemoryService, PgVectorMemoryServiceConfig,
)
from google.adk.events.event import Event
from google.adk.sessions.session import Session
from google.genai import types

async def main():
    svc = PgVectorMemoryService(PgVectorMemoryServiceConfig(
        dsn="postgresql://postgres:pw@localhost:5432/postgres"))
    session = Session(app_name="demo", user_id="u1", id="s1", events=[
        Event(author="user", timestamp=1.0,
              content=types.Content(parts=[types.Part(
                  text="We decided to move billing to net-30 terms.")])),
    ])
    await svc.add_session_to_memory(session)
    res = await svc.search_memory(app_name="demo", user_id="u1",
                                  query="what did we decide about billing?")
    print([m.content.parts[0].text for m in res.memories])

asyncio.run(main())

Checklist

  • I have read the CONTRIBUTING.md document.
  • I have performed a self-review of my own code.
  • I have commented my code, particularly in hard-to-understand areas.
  • I have added tests that prove my fix is effective or that my feature works.
  • New and existing unit tests pass locally with my changes.
  • I have manually tested my changes end-to-end.
  • Any dependent changes have been merged and published in downstream modules.

Additional context

Design mirrors the integrations/redis session service and the google-genai
embedding usage in tools/spanner and integrations/mongodb. The new optional
extra pgvector pins psycopg[binary], psycopg-pool, and pgvector, and is
included in the all extra.

Adds PgVectorMemoryService under google.adk.integrations.pgvector, a
BaseMemoryService backed by PostgreSQL with the pgvector extension. Events are
embedded (via the google-genai client by default, or an injectable embedder)
and stored with their vectors in a table the user owns; search_memory returns
the nearest memories by cosine distance using a pgvector HNSW index.

Unlike VertexAiMemoryBankService and VertexAiRagMemoryService it needs no Google
Cloud, and unlike InMemoryMemoryService it persists across restarts and ranks by
semantic similarity rather than keyword overlap. This fills the self-hosted,
non-GCP semantic-memory gap raised in google#6254 and complements the exact-match
SQLite memory service in google#4116.

The service follows the existing integrations/redis pattern: a pydantic config
plus an injectable connection pool and embedder, which keeps it unit-testable
without a live database. Ships as an optional extra, google-adk[pgvector].

Includes unit tests that exercise ingestion, idempotent upserts, semantic
ranking, per-(app_name, user_id) scoping, the distance threshold, and
serialization round-trips against an in-process fake pool and a deterministic
embedder, so they run without psycopg, pgvector, or any network access.

Resolves google#7273
@Nanduu24
Nanduu24 force-pushed the feat/pgvector-memory-service branch from 96f7abb to a04727e Compare September 25, 2026 03:28
@Nanduu24

Copy link
Copy Markdown
Author

Hi @DeanChensj — friendly ping on this one whenever you have a chance. It's green across CI, unit-tested with the optional driver both present and absent, and I verified it end-to-end against a live PostgreSQL + pgvector instance (ranking, persistence, scoping, idempotent ingest, and the created schema). Happy to adjust anything — including aligning the table layout with the SQLite memory service in #4116 if you'd prefer a shared schema. Thanks!

@Nanduu24

Nanduu24 commented Oct 8, 2026

Copy link
Copy Markdown
Author

Gentle second follow-up — this has been green and ready for ~2 weeks. Is there anything I can do to help move the review along, or a better-suited reviewer I should loop in? Happy to rebase or adjust the schema to match the SQLite memory service if that helps. Thanks!

@Nanduu24

Copy link
Copy Markdown
Author

Friendly ping — this is still green and rebased-clean, and I'm happy to make any changes needed to get it reviewed. Is there anything I can do to help move it forward, or another maintainer better placed to take a look? Thanks!

@Nanduu24

Copy link
Copy Markdown
Author

Hi @GWeale — would you be able to take a look here, or point me to the right reviewer? This adds a PgVectorMemoryService under src/google/adk/integrations/pgvector/ (a pgvector-backed BaseMemoryService) with unit tests, following the existing memory-service pattern. It's been green since it opened on Sep 25. Happy to adjust anything to match adk conventions — thanks!

@DeanChensj

Copy link
Copy Markdown
Collaborator

Hi @Nanduu24, thank you for this contribution and for your patience. Sorry it took us so long to reply. The PR is clearly written, well tested with and without the optional driver, and verified end to end against a live database. We appreciate the care you put into it.

After a careful look, we've decided not to merge PgVectorMemoryService into the core google-adk package for now. Here is our reasoning.

Maintenance burden

  • It adds three new dependencies (psycopg[binary], psycopg-pool, pgvector) and puts them in the all extra. That also means a second Postgres driver stack next to the SQLAlchemy + asyncpg path DatabaseSessionService already uses, so a user on Postgres would run two drivers and two connection pools against the same database.
  • Our CI cannot exercise it against a real Postgres + pgvector instance, so regressions would mostly reach us through user reports.
  • The table is created on first use and has no migration story, and embedding_dimension is fixed for the life of the table. Every later schema or embedding-model change becomes a compatibility problem we would have to carry for every user.
  • Every integration in core is one the team commits to maintaining long term. Right now we can't take on that commitment for this backend.

Design points we'd want addressed for any version of this

  • add_session_to_memory re-embeds every event in the session on each call. The upsert keeps rows unique, but the embedding calls are repeated, so the common "add after every turn" pattern pays for the whole history again and again.
  • All texts go to embed_content in one request, so long sessions can hit the embedding API's per-request limits. Rows are also written one execute at a time.
  • CREATE EXTENSION IF NOT EXISTS vector runs at runtime. That usually requires elevated privileges an application role doesn't have on managed Postgres.
  • table_name is interpolated into the SQL with an f-string. Composing it with psycopg.sql.Identifier would be safer.
  • Each event is stored and retrieved verbatim. In practice, long-term memory usually works better when it extracts and consolidates facts rather than recalling raw turns.

Alternatives

  • If managed infrastructure is an option, we'd encourage you to try Vertex AI Memory Bank through VertexAiMemoryBankService. It handles memory extraction, consolidation and semantic retrieval for you, and it's the path we actively invest in.
  • For self-hosted or air-gapped deployments, BaseMemoryService is a public extension point, so this works well as a standalone package. You're also welcome to propose it to google/adk-python-community, which is meant for exactly this kind of backend integration.

This isn't a "never". If we see clear community demand for a first-party Postgres/pgvector memory service, we'll gladly re-evaluate. Reactions and use cases on #7273 are the best way to show that. Thanks again for the thoughtful work, and we hope to see more contributions from you!

@Nanduu24

Copy link
Copy Markdown
Author

Thanks @DeanChensj — I really appreciate the thorough and candid reply, and the reasoning makes complete sense. The long-term maintenance commitment for a first-party DB backend is real, and the design points (re-embedding on each add, batching writes and embed calls, runtime CREATE EXTENSION privileges, Identifier-safe SQL, and fact consolidation vs. verbatim recall) are all fair and genuinely useful. I'll take you up on the suggestion: I'll repackage this as a standalone BaseMemoryService backend with those points addressed and look at proposing it to google/adk-python-community. Thanks again for the time and the clear guidance.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FEAT: Add a self-hosted PostgreSQL/pgvector semantic MemoryService

2 participants