feat: add Valkey destination connector#747
Conversation
Adds a Valkey destination connector using valkey-glide that: - Stores document chunks with vector embeddings as Valkey hashes - Creates FT Search index with HNSW vector field for semantic retrieval - Supports connection via host/port/username/password or URI - Supports TTL on uploaded keys - Uses batch writes for first upload, individual hset when index active - Maps GLIDE exceptions to unstructured-ingest error types - Removes dependency on valkey-glide-sync (async-only client) Integration tests (7 passing): - Upload with vector index creation - Upload with TTL - Upload without embeddings (no index created) - Connection via URI - Precheck failure on bad connection - Idempotent re-upload (same data, no duplicates) - Incremental upload (different data, same index) Signed-off-by: Anna Tao <annatao2004@gmail.com>
- Add element_id and embeddings type validation (Critical #1) - Document partial batch failure semantics (Critical Unstructured-IO#2) - Use @asynccontextmanager for client lifecycle (Warning Unstructured-IO#3) - Make request_timeout configurable (Warning Unstructured-IO#4) - Default ssl=True for production safety (Warning Unstructured-IO#5) - Replace bare except with ft.list() for index check (Warning Unstructured-IO#6) - Extract _build_fields and _validate_element_id helpers (Warning Unstructured-IO#7) - Pin valkey-glide~=2.4 upper bound (Warning Unstructured-IO#8) - Add TOCTOU race comment (Warning Unstructured-IO#9) - Add cluster mode note to key_prefix description (Nit Unstructured-IO#12) - Rename _sync_create_destination to _create_destination_with_client (Nit Unstructured-IO#11) - Add valkey-glide-sync for native sync client in precheck/create_destination - Remove asyncio.run() entirely — sync hooks use sync client directly - Add tests for input validation and client cleanup Signed-off-by: Anna Tao <annatao2004@gmail.com>
…or kwargs for config - Validate embedding dimension per-element against expected index schema dimension (derived from first element). Raises WriteError on mismatch instead of silently storing misaligned vectors. - Refactor GlideClientConfiguration construction to pass all options (including credentials) via constructor kwargs instead of post-construction attribute assignment. Defensive against future immutability changes. Addresses review findings from round 2 (Jonathan-Improving). Signed-off-by: Anna Tao <annatao2004@gmail.com>
- Default port to 6379 when URI omits port (parsed.port or 6379) - Route bare raise in create_destination through error mapper - Fix import ordering in __init__.py (valkey between vectara and zendesk) - Extract duplicated test cleanup into shared _cleanup() helper - Add parametrized unit test for _map_glide_error branches - Add TTL test covering individual write path (pre-existing index) - Regenerate uv.lock to include valkey-glide-sync Signed-off-by: Anna Tao <annatao2004@gmail.com>
- Fix @requires_dependencies to check glide_sync for create_destination - Add early-return in _map_glide_error for already-translated errors - Validate embedding ndim after np.array conversion (catch nested lists) - Extract _resolve_connection_params() to DRY URI parsing - Add _delete_by_record_id: delete stale chunks before re-ingestion - Fix auth substring match: use startswith instead of 'in' - Make distance_metric configurable (COSINE/L2/IP) - Add record_id TAG field to schema + stored on each hash - Extract _build_schema() and _get_distance_metric() helpers Signed-off-by: Anna Tao <annatao2004@gmail.com>
- Use correct GLIDE FtSearchResponse format: [count, {key: {fields}, ...}]
- Robust TAG escaping for all RediSearch special characters
- Batch delete via single client.delete(keys_list) call
- Add FtSearchOptions with 10k limit + warning for overflow
- Narrow exception handling: only catch RequestError 'no such index'
- Add test_valkey_destination_reingestion_deletes_stale proving happy path
- Fix incremental test to use different record IDs per source document
Signed-off-by: Anna Tao <annatao2004@gmail.com>
- Use Batch(is_atomic=True) for HSET+EXPIRE in _write_individual to prevent TTL-less keys on partial failure - Validate URI scheme against recognized values (valkey/valkeys/redis/rediss) and reject typos like 'vakeys://' with clear error - Change distance_metric type from str to Literal['COSINE', 'L2', 'IP'] for pydantic schema validation and IDE support - Add tests for all three: scheme rejection, valid schemes with TLS mapping, metric validation, and atomic TTL on individual write path Signed-off-by: Anna Tao <annatao2004@gmail.com>
- Critical: robust TAG escaping via regex (covers . / and all separators) - Add regression test with path-like identifier (s3://bucket/file.pdf) - Sanitize precheck error message (no credential leak) - Add 'noperm' to auth error prefix matching - Store full metadata as metadata_json for retrieval - Early return for empty data in run_data_async - Revert sub-batching (causes HNSW timeout), keep per-element atomic writes Signed-off-by: Anna Tao <annatao2004@gmail.com>
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
There was a problem hiding this comment.
6 issues found across 5 files
Shadow auto-approve: would not auto-approve because issues were found.
Re-trigger cubic
- Make _index_exists() call unconditional in run_data_async (P1) - Parse URI username as fallback in _resolve_connection_params (P2) - Add pagination loop in _delete_by_record_id for >10k keys (P2) - Fix import sort order in __init__.py (.valkey before .vectara) (P2) - Wrap test bodies in try/finally for guaranteed cleanup (P2) - Replace time.sleep with await asyncio.sleep in async tests (P3) Signed-off-by: Anna Tao <annatao2004@gmail.com>
There was a problem hiding this comment.
1 issue found across 3 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="unstructured_ingest/processes/connectors/valkey.py">
<violation number="1" location="unstructured_ingest/processes/connectors/valkey.py:366">
P2: Text-only uploads can fail before any HSET writes because `run_data_async` now always executes `FT.LIST` via `_index_exists`, even when embeddings are absent. Conditioning this check on `vector_length` preserves no-embedding ingestion on plain Valkey deployments.</violation>
</file>
Shadow auto-approve: would not auto-approve because issues were found.
Re-trigger cubic
Make _index_exists gracefully return False when FT.LIST throws 'unknown command' (ValkeySearch not loaded), preserving text-only uploads on plain Valkey while keeping unconditional index detection for stale key cleanup. Signed-off-by: Anna Tao <annatao2004@gmail.com>
There was a problem hiding this comment.
1 issue found across 1 file (changes from recent commits).
Shadow auto-approve: would not auto-approve because issues were found.
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
Refactor _index_exists into _check_index returning (index_exists, search_module_available) tuple. When embeddings are present but the search module is missing, raise DestinationConnectionError before writing any hashes — preventing orphaned data. Preserves batch-then- create-index order (write before index creation) to avoid HNSW timeout. Signed-off-by: Anna Tao <annatao2004@gmail.com>
There was a problem hiding this comment.
1 issue found across 1 file (changes from recent commits).
Shadow auto-approve: would not auto-approve because issues were found.
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
Adds test_valkey_destination_vector_upload_fails_without_search_module that patches _check_index to return (False, False) and verifies DestinationConnectionError is raised before any HSET writes. Signed-off-by: Anna Tao <annatao2004@gmail.com>
There was a problem hiding this comment.
0 issues found across 1 file (changes from recent commits).
Shadow auto-approve: would not auto-approve. Auto-approval blocked by 1 unresolved issue from previous reviews.
Re-trigger cubic
Signed-off-by: Anna Tao <annatao2004@gmail.com>
Summary
Adds a Valkey destination connector using valkey-glide that stores document chunks with vector embeddings in Valkey hashes and creates FT Search indexes with HNSW vector fields for semantic retrieval.
Why
Valkey is a high-performance, open-source key-value store (BSD-licensed fork of Redis) with first-class vector search support. This connector enables unstructured-ingest users to push chunked + embedded documents directly into Valkey for low-latency semantic retrieval without requiring a separate vector database.
Features
DestinationConnectionError,UserAuthError,WriteError)valkeyconnector type in the destination registryTesting
7 integration tests (require
valkey/valkey-bundleon localhost:6379):test_valkey_destination_uploadtest_valkey_destination_with_ttltest_valkey_destination_with_uritest_valkey_precheck_failureDestinationConnectionErroron bad connectiontest_valkey_destination_without_embeddingstest_valkey_destination_idempotenttest_valkey_destination_incremental_uploadDependencies
valkey-glide>=2.0(added as optional extravalkeyin pyproject.toml)numpy(already a project dependency)Changes
unstructured_ingest/processes/connectors/valkey.py— connector implementationunstructured_ingest/processes/connectors/__init__.py— registry entrytest/integration/connectors/test_valkey.py— integration testspyproject.toml— valkey extra dependencyuv.lock— lockfile update