Repository navigation
Conversation
vishal-bala
requested changes
Oct 27, 2026
vishal-bala
left a comment
Collaborator
There was a problem hiding this comment.
Thanks for the contribution! That's a useful addition, glad to have it 😄
Just a few minor comments and we should be good to go
Routing from async code (LangGraph nodes, FastAPI handlers, MCP servers) blocked the event loop twice per call: once to embed, once on the FT.AGGREGATE round trip. SemanticCache already has async lookups; the router had none. aroute() and aroute_many() embed with the vectorizer's aembed and run the same aggregate request on a lazily created AsyncSearchIndex over the same index, so they return what router() and route_many() return. Request building and row processing are shared with the sync path. adisconnect() closes the async connection. Mutating methods stay sync.
Address review on redis#784: - _get_async_index reads the URL, connection kwargs, lib_name and client ownership from self._index instead of keeping copies on the router, which also makes the from_existing patch unnecessary. - aroute/aroute_many take everything after vector keyword-only, so a positional distance_threshold can't be read as an aggregation method. - Docstrings note that SSL, unix-socket, Sentinel and Cluster clients should pass redis_url for async routing, that vectorizers without a native aembed still block on embedding, and that adisconnect() must be called before switching event loops. - Replace the six async tests with one that blocks the sync embed and sync aggregate paths, so it fails if aroute takes either.
Yarmoluk
force-pushed
the
feat/router-async-query
branch
from
October 27, 2026 15:11
ee6052d to
8a70416
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
Closes #783.
SemanticRouterhad no async query path, so routing from async apps (LangGraph nodes, FastAPI handlers, MCP servers) blocked the event loop twice per call: on the embedding and on theFT.AGGREGATEround trip.SemanticCachealready has async lookups; the router had none.Changes
aroute()andaroute_many()Async counterparts of
router(...)androute_many(...). They embed withvectorizer.aembedand run the same aggregate request on anAsyncSearchIndexover the same index, so they return exactly what the sync methods return. Everything aftervectoris keyword-only, so a positionaldistance_threshold(deprecated inroute_many) can't be read as an aggregation method._get_route_matchesis split into request building (_route_matches_request) and row processing (_route_matches_from_result), shared by the sync and async paths so the two cannot drift._build_aggregate_requestis untouched, so this does not collide with fix(router): apply per-route thresholds before max_k truncation #766.SemanticCache), derived from the syncSearchIndex: when the sync index owns its client and has a URL, the async index connects from that URL, connection kwargs andlib_name; otherwise the caller's client is converted withRedisConnectionFactory.sync_to_async_redis.from_existing(redis_url=...)builds an owned client from the URL, so it takes the URL branch too.adisconnect()closes the async connection;aindexexposes the async index (None before first use).add_route,remove_route, ...) stay sync to keep the change small.Release Notes
SemanticRoutergainsaroute(),aroute_many(),adisconnect()and anaindexproperty. The async index is created on first use. Routers given an SSL, unix-socket, Sentinel orRedisClusterclient can't convert it to async; passredis_urlto the router for async routing in those setups. Vectorizers without a nativeaembed(e.g. the defaultHFTextVectorizer) still embed synchronously; the Redis call is non-blocking either way. The async client is bound to the event loop that made the first call; calladisconnect()before switching loops.Reviewer Brief
Start here:
redisvl/extensions/router/semantic.py,_get_async_index. It decides how the async index reaches Redis, and everyaroute/aroute_manycall goes through it.Where it could fail
redisvl/extensions/router/semantic.py:557: the URL branch is taken only when the sync index owns its client and has a URL. Check that a router built withredis_client=plusredis_url=(both given) converts the client rather than connecting from the URL, since the client is what the sync path uses.redisvl/extensions/router/semantic.py:567: the client-conversion branch raises on the first async call for SSL, unix-socket, Sentinel and Cluster clients (sync_to_async_redislimitation, pre-existing). The docstrings say to passredis_urlinstead; nothing prevents the mismatch at construction.Not verified: cluster mode (async path unsupported for
RedisClusterclients); non-blocking embedding with a native-async vectorizer (the tests useHFTextVectorizer, which embeds synchronously, so only the Redis call is exercised async); Redis < 7 error mapping (shared code, unreachable on the test container).Beyond CI:
test_async_routing_matches_sync(tests/integration/test_semantic_router.py:822) fails withAssertionError: the async path made a blocking callwhenarouteis changed to call the syncembed, and passes on this branch;test_aroute_from_existing_url_connects_from_urlpasses with the async index derived from the sync one, so the earlierfrom_existingpatch is no longer needed.Since the last brief:
8a70416applies the review: the async index is derived fromself._index(no router-side copies of URL/kwargs/lib_name, nofrom_existingchange), keyword-only arguments aftervector, the three docstring notes, and the six async tests replaced by one that blocks the sync paths.Reviewed
8a70416· Claude Code (claude-fable-5-1) · lenses: correctness, tests. Ran:pytest tests/integration/test_semantic_router.py(42 passed, 1 skipped; Redis via the repo's testcontainers compose),pytest tests/unit -k "router or create_index"(50 passed),make check-types(120 files, no issues),make format.🤖 Generated with Claude Code
Note
Low Risk
New async API reuses the existing FT.AGGREGATE flow and SemanticCache-style connection setup; main caveats are documented (cluster client conversion, event-loop binding), not new security or data paths.
Overview
Adds non-blocking routing for async apps via
aroute()andaroute_many(), mirroringrouter()/route_many()by usingvectorizer.aembedand a lazily createdAsyncSearchIndexon the same Redis index.The sync lookup path is refactored so request building (
_route_matches_request), Redis error handling (_raise_aggregate_error), and result parsing (_route_matches_from_result) are shared with_aget_route_matches, reducing drift between sync and async.adisconnect()and anaindexproperty manage the optional async client (URL-based when the router owns its connection, otherwisesync_to_async_redis).Integration tests assert async results match sync, that the async path does not call blocking
embed/aggregate, and that routers loaded withfrom_existing(redis_url=...)wire async Redis from the URL.Reviewed by Cursor Bugbot for commit 8a70416. Bugbot is set up for automated code reviews on this repo. Configure here.