* 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>
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.
What is Valkey Search?
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-glideclient 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 FLOAT32document— the raw text (indexed as TEXT for full-text/hybrid search)file_id— promoted to an indexed TAG field so it is filterablemetadata_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/usernameandtls: truefor any non-local deployment. Credentials are never written to logs.