← Files RedisARCHIVED FILE
skills/redis-search/references/hybrid-search.md
5.01 KB · Oct 3, 2026 · 06:20 UTC
# Combine Lexical and Vector Search Correctly
Two patterns address two different needs:
- **Filter-narrowed vector search** (works on every Redis with vector support): write a normal `FT.SEARCH` with a TAG/NUMERIC pre-filter on the left side of the `=>[KNN ...]` clause. The pre-filter shrinks the candidate set; KNN then runs only over survivors.
- **Blended lexical + vector ranking with explicit fusion** (Redis ≥ 8.4.0): use `FT.HYBRID`, which runs a `SEARCH` leg and a `VSIM` leg in parallel and fuses their rankings via Reciprocal Rank Fusion (`COMBINE RRF`) or a weighted score blend (`COMBINE LINEAR`).
**Correct: pre-filtered KNN** (works on all Redis 8.x and the RediSearch module).
```
# Filter to mountain bikes under $500, then KNN over the survivors
FT.SEARCH idx:bicycle "(@type:{mountain} @price:[100 500])=>[KNN 10 @description_embeddings $vec AS score]"
SORTBY score
PARAMS 2 vec "<vector_blob>"
RETURN 4 model brand price score
DIALECT 2
```
**Correct: FT.HYBRID** — requires Redis ≥ 8.4.0.
```
# Blend lexical ("mountain bicycle") + vector similarity with RRF fusion
FT.HYBRID idx:bicycle
SEARCH "mountain bicycle"
VSIM @description_embeddings $vec
KNN 2 K 10
COMBINE RRF 10 # RRF <count> — number of fused results to keep
PARAMS 2 vec "<vector_blob>"
LIMIT 0 10
DIALECT 2
# Weighted (LINEAR) — α weights the SEARCH score, β the VSIM score
FT.HYBRID idx:bicycle
SEARCH "mountain bicycle" YIELD_SCORE_AS lex_score
VSIM @description_embeddings $vec YIELD_SCORE_AS vec_score
KNN 2 K 20
COMBINE LINEAR 4 ALPHA 0.4 BETA 0.6
PARAMS 2 vec "<vector_blob>"
DIALECT 2
```
## When to use which
| Goal | Use |
|------|-----|
| "Find vectors near $vec, but only within category X and price < $500." | Pre-filtered KNN inside `FT.SEARCH` (works everywhere). |
| "Rank documents by a blend of lexical relevance and semantic similarity." | `FT.HYBRID` (Redis ≥ 8.4.0). |
| "Same goal but on Redis < 8.4.0." | Run two separate queries client-side and fuse the rankings yourself (rough fallback; loses cross-leg score calibration). |
**Incorrect:** Running an unfiltered KNN and then filtering client-side, or assuming `FT.HYBRID` exists on older Redis.
```
# Bad: KNN across the whole index, then filter client-side — burns vector work on rows you'd drop.
FT.SEARCH idx:bicycle "*=>[KNN 1000 @description_embeddings $vec AS score]"
SORTBY score
PARAMS 2 vec "<vector_blob>"
DIALECT 2
# Then in the application: drop any row where type != "mountain" or price not in [100, 500].
```
```python
# Bad (client mirror): same anti-pattern in Python — fetch 1000, filter in memory.
results = r.ft("idx:bicycle").search(
Query("*=>[KNN 1000 @description_embeddings $vec AS score]")
.sort_by("score").dialect(2),
query_params={"vec": vec_blob})
mountain = [r for r in results.docs if r.type == "mountain" and 100 <= int(r.price) <= 500]
```
## Performance notes
- Pre-filter with `TAG` and `NUMERIC` fields — these are cheap and dramatically cut the KNN candidate set.
- For `FT.HYBRID`, the `KNN <count> K <k>` clause inside `VSIM` controls how many vector neighbours feed the fusion stage; the outer `LIMIT` controls how many results you return.
- `COMBINE RRF` needs no tuning; `COMBINE LINEAR` needs calibrated α/β — start at 0.5/0.5 and adjust based on relevance evals.
## Client mirrors
```python
# redis-py — STEP_START hybrid_search
# Mirrors doctests/query_combined.py
import numpy as np
from redis import Redis
from redis.commands.search.query import Query
r = Redis()
vec_blob = np.array(query_embedding, dtype=np.float32).tobytes()
q = (Query("(@type:{mountain} @price:[100 500])=>[KNN 10 @description_embeddings $vec AS score]")
.sort_by("score").return_fields("model", "brand", "price", "score")
.dialect(2).paging(0, 10))
r.ft("idx:bicycle").search(q, query_params={"vec": vec_blob})
# STEP_END
```
```java
// Jedis — STEP_START hybrid_search
import redis.clients.jedis.UnifiedJedis;
import redis.clients.jedis.search.Query;
try (UnifiedJedis jedis = new UnifiedJedis("redis://localhost:6379")) {
Query q = new Query(
"(@type:{mountain} @price:[100 500])=>[KNN 10 @description_embeddings $vec AS score]")
.setSortBy("score", true)
.returnFields("model", "brand", "price", "score")
.addParam("vec", vecBlob)
.dialect(2)
.limit(0, 10);
jedis.ftSearch("idx:bicycle", q);
}
// STEP_END
```
RedisVL `VectorQuery` with filter expressions and the `HybridQuery` wrapper for `FT.HYBRID` live in [clients/python-redisvl.md](clients/python-redisvl.md).
## Upstream sources
- redis-py: [`doctests/query_combined.py`](https://github.com/redis/redis-py/blob/master/doctests/query_combined.py)
- Jedis: [`VectorSearchExample.java`](https://github.com/redis/jedis/blob/master/src/test/java/io/redis/examples/VectorSearchExample.java)
- Reference: [Hybrid Queries](https://redis.io/docs/latest/develop/interact/search-and-query/query/combined/), [FT.HYBRID](https://redis.io/docs/latest/commands/ft.hybrid/)
SHA-256: 58e39b948df1080dd1fbc8cd24d05d633ee248b40b59607cbd5978ce09da88f2