Skip to content

perf(api): add Redis-cached QUERY and MCP reads - #1172

Open
phernandez wants to merge 5 commits into
mainfrom
codex/query-search-cache
Open

perf(api): add Redis-cached QUERY and MCP reads#1172
phernandez wants to merge 5 commits into
mainfrom
codex/query-search-cache

Conversation

@phernandez

@phernandez phernandez commented Jul 30, 2026

Copy link
Copy Markdown
Member

Why

Search is a read, but Basic Memory's v2 API exposed it only as POST. That made the transport
semantics misleading and kept the typed client from using HTTP QUERY. Search and whole-note reads
are also among the slowest hosted MCP operations, so exact repeated reads should avoid rebuilding
the same typed response from the authoritative database and file path.

This PR adds the non-mutating search transport, caches the complete semantic operation, and makes
the same optional Redis cache available to standalone bm mcp without breaking existing callers
or adding Redis to the default local-first runtime.

What Changed

  • Register QUERY and legacy POST on the same /v2/projects/{project_id}/search/ FastAPI
    function and send QUERY from SearchClient.
  • Cache the final typed search, note/entity, resource, directory, and related read responses behind
    project-generation invalidation.
  • Activate standalone MCP caching only when BASIC_MEMORY_REDIS_URL is set; bare hosts such as
    redis normalize to redis://redis.
  • Share one MCP-lifespan-owned cache between the in-process FastAPI request path and watcher/index
    invalidation, then close its Redis client during shutdown.
  • Keep the cache dependency as None when unconfigured instead of introducing no-op cache
    behavior.
  • Add a real-MCP, real-Redis integration test and reusable just recipes for the complete cache
    suite and paired read-load benchmark.
  • Document standalone activation, Cloud tenant requirements, benchmark methodology, and the
    distinction between semantic cache data and Cloud rate-limit/control-plane data.

Implementation Details

Search and read-cache semantics

QUERY and POST share one authoritative search function and one cache identity because transport
method is not semantic request identity. The key includes the trusted namespace, canonical project
UUID, project generation, operation, complete validated request, and pagination. Canonical JSON
stabilizes object-key ordering while preserving list order. Successful empty pages are cacheable;
exceptions and cancellation never populate Redis.

Mutations invalidate by replacing the project generation rather than scanning Redis. The request,
indexing, relation-resolution, and storage-event paths therefore share one correctness boundary.

Standalone MCP lifecycle

src/basic_memory/read_cache/lifecycle.py owns the optional import, URL normalization, client
construction, RedisReadCache facade, and cleanup. src/basic_memory/mcp/server.py creates that
resource once per MCP lifespan and installs the same cache in both McpContainer and the
off-lifespan ApiContainer. This matters because local MCP requests use FastAPI through an
in-process ASGI transport and do not enter FastAPI's lifespan.

BASIC_MEMORY_REDIS_URL is the sole standalone switch. When it is absent or blank, the dependency
is None and callers execute only the authoritative path. The optional redis package extra is
still required when the variable is enabled.

Cloud tenant boundary

Cloud runtime deliberately ignores the standalone Redis setting. Cloud constructs and injects its
own cache from trusted tenant context, using an opaque tenant namespace plus the canonical project
UUID. A physical Redis deployment and connection pool may be shared across tenants and with rate
limiting, but semantic-cache keys remain under bm:read:v1, tenant namespaces never come from
request input, and rate-limit/control-plane keyspaces retain separate ownership and failure policy.
The coordinated hosted integration remains in
basic-memory-cloud#1644.

Benchmark tooling

just bench-read-cache <redis-url> <run-id> runs adjacent authoritative and warmed-Redis MCP stdio
sweeps over 1, 16, and 64 KiB notes at concurrency 1, 8, 32, and 64. Corpus creation, indexing,
connection setup, and cache warmup stay outside the measured region. Each run ID preserves separate
scratch directories and JSONL artifacts; the harness records latency percentiles, throughput,
bandwidth, errors, validation failures, and workload metadata without recording Redis credentials.

Testing

Automated

  • just fast-check: passed (ruff, formatting, and ty).
  • just test-read-cache: 63 passed against real Redis.
  • Focused container/config lifecycle suite: 22 passed.
  • just doctor: passed the end-to-end file, database, indexing, search, and status loop.
  • just --dry-run bench-read-cache redis://127.0.0.1:6379/0 check: generated the expected
    run-scoped authoritative, Redis, and comparison commands.
  • git diff --check: passed before the signed commit.

Benchmark observation

The first adjacent local SQLite pair completed 3,072 measured reads per mode with zero read or
response-validation errors. Across twelve size/concurrency scenarios it showed a directional
median p50 reduction of about 54% and median throughput gain of about 126% for warmed Redis.

A second pair run through the checked-in just recipe also completed without correctness errors,
but its Redis-side corpus preparation took roughly 11.2 seconds versus 5.1 seconds for the baseline
and several latency rows inverted under workstation contention. It is retained as proof that the
tooling completes, not included in the performance claim. The benchmark guide requires at least
six alternating paired repetitions and rejects materially contended runs before drawing a stable
conclusion.

Risks / Follow-ups

  • OpenAPI 3.1 cannot describe QUERY as a first-class operation, so POST remains the documented
    compatibility route and QUERY is intentionally hidden from the generated schema.
  • Redis is optional but enabling BASIC_MEMORY_REDIS_URL without installing the redis extra
    fails at MCP startup instead of silently disabling the requested cache.
  • The observed timings are a local SQLite control, not a Cloud/Postgres/Tigris forecast. Controlled
    repeated and hosted measurements are still needed for a production performance claim.
  • Exact repeated-query locality is modest; the main expected user benefit is repeated entity/note,
    directory, and resource access. Short TTLs and project invalidation bound stale-data risk.
  • Cloud must keep its Core dependency pin and trusted tenant namespace aligned with this PR before
    hosted activation. No per-tenant Redis instance is required.

Signed-off-by: phernandez <paul@basicmachines.co>
Signed-off-by: phernandez <paul@basicmachines.co>
Signed-off-by: phernandez <paul@basicmachines.co>
Signed-off-by: phernandez <paul@basicmachines.co>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 17ecb670bc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/basic_memory/mcp/clients/search.py
@phernandez

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Swish!

Reviewed commit: 17ecb670bc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Signed-off-by: phernandez <paul@basicmachines.co>
@phernandez phernandez changed the title feat(api): add cached QUERY search perf(api): add Redis-cached QUERY and MCP reads Aug 1, 2026
@phernandez

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 67e3d5bb46

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +241 to +244
redis_url: Optional[str] = Field(
default=None,
description="Optional Redis URL for standalone MCP read caching. Bare hostnames are accepted.",
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Redact credentials in the Redis URL

When BASIC_MEMORY_REDIS_URL contains standard Redis userinfo or query credentials, registering it as a scalar config field makes bm config get/list and basic_memory_diagnostics expose the complete URL verbatim because the shared redaction module's URL_FIELDS set was not updated. Diagnostics may be returned to MCP clients or logs, so add redis_url to the URL-redaction fields and cover credential-bearing values.

Useful? React with 👍 / 👎.

Comment on lines +448 to +449
await asyncio.sleep(args.quiesce_seconds)
await warm_targets(session, project, targets)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Rewarm entries before every measured cache scenario

When the Redis-enabled sweep takes more than the 60-second read-cache TTL, this single warmup leaves later size/concurrency rows measuring a mixture of expired authoritative misses and cache hits; cache lookups do not refresh the response-key TTL. This can invert or dilute the comparison on slower hosts or with larger --reads, despite every row being labeled as warmed, so warm each scenario immediately before starting its timer.

Useful? React with 👍 / 👎.

Comment on lines +476 to +480
record: dict[str, object] = {
"benchmark": (f"read-load size={size_label(size_bytes)} c={concurrency}"),
"timestamp_utc": datetime.now(UTC).isoformat(),
"label": args.label,
"metadata": {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Capture required provenance for benchmark artifacts

When these JSONL files are retained or used for a performance comparison, the metadata records only a caller-supplied label and runtime settings, not the invoked Basic Memory source/resolved SHA or version, and the harness emits no manifest.json. Consequently two artifacts cannot establish which builds produced them and are not reproducible; emit the required provenance manifest alongside the results.

AGENTS.md reference: benchmarks/AGENTS.md:L78-L83

Useful? React with 👍 / 👎.

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.

1 participant