Files
LangBot/docs/VALKEY_SEARCH_INTEGRATION.md
T
Daria Korenieva 0c405901d2 feat(vector): add Valkey Search vector database backend (#2276)
* feat(vector): add Valkey Search vector database backend

Add a new opt-in VectorDatabase backend backed by the Valkey Search module
(valkey/valkey-bundle), accessed via the official valkey-glide client's native
ft command namespace.

- Implements the full VectorDatabase ABC: VECTOR, FULL_TEXT and HYBRID search,
  all 8 metadata filter operators, and pagination with exact totals.
- HYBRID uses filter-then-KNN (no app-side weighted fusion); vector_weight is
  accepted for interface parity but NOT honored (docstring + one-time warning +
  docs caveat).
- Lazy connect so a down Valkey never blocks boot; mandatory
  client_name=langbot_vector_client; optional auth + TLS (never logged).
- Registered via a single elif branch in vector/mgr.py; disabled by default
  (vdb.use stays chroma) for toC compatibility.
- Adds valkey-glide>=2.4.1,<3.0.0; no protobuf/pydantic downgrade; no ORM
  change so no Alembic migration.
- Unit tests (fast lane, no server) + slow-gated integration tests
  (TEST_VALKEY_URL, valkey/valkey-bundle:9.1.0) + integration doc.

* fix(vector): paginate Valkey Search deletes and guard delete_by_filter

Address self-review follow-ups for the Valkey Search VDB backend:

- _search_keys now paginates through the full result set in batches of
  _DELETE_SCAN_BATCH instead of capping at a single hard-coded 10000-key
  page, so delete_by_file_id / delete_by_filter fully remove files and
  filters that match more than one page of chunks (no orphaned vectors).
- Add unit regression tests for the delete_by_filter mass-deletion guard:
  a filter referencing only non-indexed fields must skip and return 0
  (never fall back to match-all), and a supported filter still deletes
  matching keys.

* refactor(vector): harden Valkey Search backend and add adversarial tests

Address the self-review NICE-TO-HAVE items for the Valkey Search VDB backend:
- Guard the username-without-password credential edge (skip auth + warn
  instead of building ServerCredentials(password=None, ...), which glide
  rejects).
- Add an async close() teardown that closes the glide client and resets
  cached state (re-init is safe via the existing None guard).
- Hoist 'import json' to module top (was imported inside three methods).
- Document the FT TAG literal-brace limitation in _escape_tag (fails closed,
  never widens).

Tests:
- Add an adversarial-input integration test proving crafted file_id /
  query_text cannot break out of or widen a query (fail-closed on braces).
- Add unit tests for close() and the credential-build guard.

Signed-off-by: Daria Korenieva <daric2612@gmail.com>

* fix(vector): make Valkey Search file_id TAG support arbitrary characters

Valkey Search's FT TAG query parser cannot handle '{', '}' or '*' even when
backslash-escaped, so a file_id containing those characters previously
produced an unparseable query (it failed closed / raised). Percent-encode
exactly those FT-unsafe characters (plus '%' for reversibility) in the
file_id TAG value, applied identically at write time and query time, so an
arbitrary file_id round-trips. For normal UUID/hash ids this is a no-op and
the stored value is unchanged; the original file_id is always preserved
verbatim in metadata_json.

Strengthen the adversarial integration test to assert a brace/star-bearing
file_id matches and deletes exactly its own row (no widening, no raise), and
add unit tests for _encode_file_id and the filter encoding.

Signed-off-by: Daria Korenieva <daric2612@gmail.com>

* refactor(vector): address Valkey Search review feedback

- Add configurable request_timeout (default 5000ms; glide default 250ms is
  too low for KNN); expose in config.yaml + docs table
- Validate embedding dimension consistency in add_embeddings (fail fast on
  mixed lengths to avoid silent KNN corruption)
- Use ft.info (O(1)) instead of ft.list (O(n)) for index existence checks in
  the query hot path; also closes the check-then-create TOCTOU window
- Pipeline HSETs via a non-atomic Batch instead of N sequential awaits
- Extract shared _iter_reply_docs to deduplicate reply parsing between
  _reply_to_chroma and list_by_filter
- Parenthesize multi-condition pre-filters before the => KNN clause
- Fail closed when a username is configured without a password
- Catch only RequestError on ft.dropindex (let connection/auth errors surface)
- Bound the delete_collection SCAN loop with a safety cap
- Add VectorDatabase.close() (no-op default) + VectorDBManager.shutdown()
- Simplify _MATCH_ALL literal; normalize typing to builtin generics

* fix(vector/valkey_search): address round-2 review feedback

- Serialize lazy client creation with an asyncio.Lock (double-checked) so
  concurrent first-use callers don't construct and leak duplicate clients.
- Make the filter operator chain exhaustive: raise on an unhandled op rather
  than silently dropping the condition (which could widen delete_by_filter).
- Cast numeric range (///) values to float, failing closed on
  non-numeric input and pre-empting a future NUMERIC-field injection surface.

* refactor(vector): remove shutdown/close from base ABC per maintainer feedback Per maintainer request, interface changes to VectorDatabase ABC and VectorDBManager should be in a separate PR with implementation across all backends. The ValkeySearchVectorDatabase.close() method remains but does not override an ABC method.

Signed-off-by: Daria Korenieva <daric2612@gmail.com>

* docs(test): list valkey_search in vdb coverage exclusions Add valkey_search to the documented vector/vdbs/ coverage-exclusion list, matching the existing chroma/milvus/pgvector/qdrant/seekdb entries. These adapters require a live database instance and are covered by env-gated integration tests instead of unit tests.

Signed-off-by: Daria Korenieva <daric2612@gmail.com>

---------

Signed-off-by: Daria Korenieva <daric2612@gmail.com>
2026-07-08 06:59:16 +08:00

7.5 KiB

Valkey Search Vector Database Integration

This document describes how to use Valkey Search (the search/vector module bundled in valkey/valkey-bundle) as the vector database backend for LangBot's knowledge base (RAG) feature.

Valkey Search is a module that adds vector similarity search and full-text search to Valkey, the open-source, BSD-licensed in-memory data store forked from Redis OSS. It is distributed in the valkey/valkey-bundle image alongside other modules (JSON, Bloom, LDAP).

LangBot talks to Valkey through the official valkey-glide client (Rust core + async Python wrapper), using its native ft (search) command namespace.

Key Features

  • Vector search: ANN via HNSW or exact via FLAT, with COSINE / L2 / IP distance metrics
  • Full-text search: term, prefix and phrase matching over indexed text fields
  • Hybrid search: a metadata/text filter pre-selects candidates, then KNN ranks them
  • In-memory speed: vectors and documents are stored as Valkey HASH keys
  • Auth + TLS: optional username/password and TLS for production (toB / SaaS) deployments

Licensing

  • Valkey core and the Search module are BSD-3-Clause.
  • The valkey-glide client is Apache-2.0.

Both are compatible with LangBot.

Installation

Valkey Search support is included when you install LangBot — the valkey-glide dependency is declared in pyproject.toml. To install manually:

pip install 'valkey-glide>=2.4.1,<3.0.0'

You also need a running Valkey server with the Search module loaded. The simplest way is the bundled image:

# Run valkey-bundle (includes the Search module) on host port 6380
podman run -d --name valkey-test-langbot -p 6380:6379 valkey/valkey-bundle:9.1.0
# (docker run ... works identically)

valkey-bundle ships multi-arch images (linux/amd64 + linux/arm64), so it runs on both CI (x86_64) and Apple-silicon dev machines.

Configuration

Valkey Search is opt-in and disabled by default — the default vdb.use stays chroma, so existing single-process deployments are unaffected. To enable it, edit your config.yaml:

vdb:
  use: valkey_search
  valkey_search:
    host: 'localhost'
    port: 6379            # use 6380 if you started the container as shown above
    db: 0
    password: ''          # optional (ACL / requirepass) — never logged
    username: ''          # optional (ACL user)
    tls: false            # optional (toB / SaaS)
    index_algorithm: 'HNSW'   # HNSW | FLAT
    distance_metric: 'COSINE' # COSINE | L2 | IP
    request_timeout: 5000     # per-request timeout in ms
Option Default Description
host localhost Valkey host
port 6379 Valkey port
db 0 Logical database id
password '' Optional auth password (empty = no auth). Never logged.
username '' Optional ACL username. Configuring a username without a password fails closed (raises) rather than connecting unauthenticated.
tls false Enable TLS for the connection
index_algorithm HNSW HNSW (approximate) or FLAT (exact)
distance_metric COSINE COSINE, L2, or IP
request_timeout 5000 Per-request timeout in milliseconds. The valkey-glide default (250ms) is too low for vector KNN under load; raise it further for remote/cross-AZ Valkey.

Connection behavior

The backend uses a lazy connection (lazy_connect=True): the client is created on first use and the connection is deferred to the first command. A misconfigured or unreachable Valkey server therefore does not block LangBot from booting — knowledge-base operations will error at call time instead, and you can recover by switching vdb.use back to another backend.

The connection sets a fixed client_name of langbot_vector_client so it is identifiable in CLIENT LIST and monitoring dashboards.

Supported search types

Type Behavior
vector Pure KNN over the embedding field
full_text Term/phrase match over the indexed document text field
hybrid Metadata/text filter pre-selects candidates, then KNN ranks them

⚠️ Important: vector_weight is NOT honored

Valkey Search hybrid queries follow a filter-then-KNN model: the filter (and/or full-text clause) narrows the candidate set, and the KNN stage ranks the survivors by vector distance. There is no native weighted score fusion (unlike, e.g., SeekDB's RRF boost).

For interface compatibility the backend still accepts a vector_weight argument, but it is ignored — passing different weights does not change result ordering. The first time a non-default weight is supplied, the backend logs a one-time warning.

If weighted hybrid ranking is needed in the future, it can be added application-side (run vector KNN and full-text search separately and blend the scores). That is intentionally out of scope for this integration.

Metadata & filtering

Documents are stored as Valkey HASH keys under the prefix kb:{collection}:{id} with fields:

  • vector — the embedding, packed as little-endian FLOAT32
  • document — the raw text (indexed as TEXT for full-text/hybrid search)
  • file_id — promoted to an indexed TAG field so it is filterable
  • metadata_json — the full metadata dict, preserved verbatim as JSON

Only indexed fields are filterable. Currently that is file_id. Filters referencing non-indexed metadata keys are dropped with a warning (the same pragmatism used by the Milvus and pgvector backends). All other metadata still round-trips intact via metadata_json.

Supported filter operators (canonical Chroma-style where syntax): $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin. Multiple top-level keys are AND-ed.

Testing

Unit tests (filter mapping, float32 packing, reply parsing, import guard) run in the fast lane with no server:

uv run pytest tests/unit_tests/vector/test_valkey_search_filter.py -q

Integration tests are slow-gated on TEST_VALKEY_URL and require a running server:

podman run -d --name valkey-test-langbot -p 6380:6379 valkey/valkey-bundle:9.1.0
TEST_VALKEY_URL=valkey://localhost:6380 \
    uv run pytest tests/integration/vector/test_valkey_search.py -m slow -q

The default upstream fast CI lane (-m "not slow") skips these, matching the existing PostgreSQL migration-test precedent.

Troubleshooting

Symptom Cause / fix
Tests skip with "Valkey Search module not available" The server is plain Valkey without the Search module. Use the valkey/valkey-bundle image.
ConnectionError at call time Check host/port/auth; remember lazy_connect defers errors to first use.
Empty search results right after insert The Search indexer is asynchronous; results become visible within a short delay. The integration tests poll/retry to account for this.
Hybrid ranking ignores vector_weight Expected — see the caveat above.

Production considerations

  • Cluster mode: Valkey Search in cluster mode uses an additional coordination port. This integration targets standalone mode; cluster support is a future consideration.
  • Persistence: configure Valkey RDB/AOF persistence if the knowledge base must survive restarts; otherwise an in-memory store is ephemeral.
  • Security: set password/username and tls: true for any non-local deployment. Credentials are never written to logs.