Skip to content

Commit 4b3c12a

Browse files
committed
Feature - RSearch.hybridSearch() method added. #6922
1 parent 82cd296 commit 4b3c12a

25 files changed

Lines changed: 2214 additions & 39 deletions

redisson/src/main/java/org/redisson/RedissonSearch.java

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import org.redisson.api.search.aggregate.*;
2222
import org.redisson.api.search.index.*;
2323
import org.redisson.api.search.query.*;
24+
import org.redisson.api.search.query.hybrid.*;
2425
import org.redisson.client.RedisClient;
2526
import org.redisson.client.codec.Codec;
2627
import org.redisson.client.codec.DoubleCodec;
@@ -1090,4 +1091,207 @@ public List<String> getIndexes() {
10901091
public RFuture<List<String>> getIndexesAsync() {
10911092
return commandExecutor.readAsync((String) null, StringCodec.INSTANCE, RedisCommands.FT_LIST);
10921093
}
1094+
1095+
@Override
1096+
public HybridSearchResult hybridSearch(String indexName, HybridQueryArgs args) {
1097+
return commandExecutor.get(hybridSearchAsync(indexName, args));
1098+
}
1099+
1100+
@Override
1101+
@SuppressWarnings("MethodLength")
1102+
public RFuture<HybridSearchResult> hybridSearchAsync(String indexName, HybridQueryArgs args) {
1103+
HybridQueryParams options = (HybridQueryParams) args;
1104+
1105+
List<Object> cmdArgs = new ArrayList<>();
1106+
cmdArgs.add(indexName);
1107+
1108+
cmdArgs.add("SEARCH");
1109+
cmdArgs.add(options.getQuery());
1110+
1111+
if (options.getScorer() != null) {
1112+
cmdArgs.add("SCORER");
1113+
cmdArgs.add(options.getScorer());
1114+
}
1115+
1116+
if (options.getQueryScoreAlias() != null) {
1117+
cmdArgs.add("YIELD_SCORE_AS");
1118+
cmdArgs.add(options.getQueryScoreAlias());
1119+
}
1120+
1121+
VectorSimilarityParams vsimParams = options.getVectorSimilarityParams();
1122+
if (vsimParams == null) {
1123+
throw new IllegalArgumentException("vectorSimilarity is required for hybrid search");
1124+
}
1125+
cmdArgs.add("VSIM");
1126+
cmdArgs.add(vsimParams.getField());
1127+
cmdArgs.add(vsimParams.getParam());
1128+
1129+
if (vsimParams.getMode() == VectorSimilarityParams.VectorSearchMode.KNN) {
1130+
List<Object> knnArgs = new ArrayList<>();
1131+
knnArgs.add("K");
1132+
knnArgs.add(vsimParams.getKnnK());
1133+
if (vsimParams.getEfRuntime() != null) {
1134+
knnArgs.add("EF_RUNTIME");
1135+
knnArgs.add(vsimParams.getEfRuntime());
1136+
}
1137+
cmdArgs.add("KNN");
1138+
cmdArgs.add(knnArgs.size());
1139+
cmdArgs.addAll(knnArgs);
1140+
} else if (vsimParams.getMode() == VectorSimilarityParams.VectorSearchMode.RANGE) {
1141+
List<Object> rangeArgs = new ArrayList<>();
1142+
rangeArgs.add("RADIUS");
1143+
rangeArgs.add(vsimParams.getRangeRadius());
1144+
if (vsimParams.getRangeEpsilon() != null) {
1145+
rangeArgs.add("EPSILON");
1146+
rangeArgs.add(vsimParams.getRangeEpsilon());
1147+
}
1148+
cmdArgs.add("RANGE");
1149+
cmdArgs.add(rangeArgs.size());
1150+
cmdArgs.addAll(rangeArgs);
1151+
}
1152+
1153+
if (vsimParams.getScoreAlias() != null) {
1154+
cmdArgs.add("YIELD_SCORE_AS");
1155+
cmdArgs.add(vsimParams.getScoreAlias());
1156+
}
1157+
1158+
if (vsimParams.getFilter() != null) {
1159+
cmdArgs.add("FILTER");
1160+
cmdArgs.add(vsimParams.getFilter());
1161+
}
1162+
1163+
Combine combine = options.getCombine();
1164+
if (combine != null) {
1165+
cmdArgs.add("COMBINE");
1166+
if (combine instanceof CombineRrfParams) {
1167+
CombineRrfParams rrfParams = (CombineRrfParams) combine;
1168+
List<Object> combineArgs = new ArrayList<>();
1169+
if (rrfParams.getConstant() != null) {
1170+
combineArgs.add("CONSTANT");
1171+
combineArgs.add(rrfParams.getConstant());
1172+
}
1173+
if (rrfParams.getWindow() != null) {
1174+
combineArgs.add("WINDOW");
1175+
combineArgs.add(rrfParams.getWindow());
1176+
}
1177+
cmdArgs.add("RRF");
1178+
cmdArgs.add(combineArgs.size());
1179+
cmdArgs.addAll(combineArgs);
1180+
1181+
if (rrfParams.getScoreAlias() != null) {
1182+
cmdArgs.add("YIELD_SCORE_AS");
1183+
cmdArgs.add(rrfParams.getScoreAlias());
1184+
}
1185+
} else if (combine instanceof CombineLinearParams) {
1186+
CombineLinearParams linearParams = (CombineLinearParams) combine;
1187+
List<Object> combineArgs = new ArrayList<>();
1188+
if (linearParams.getAlpha() != null) {
1189+
combineArgs.add("ALPHA");
1190+
combineArgs.add(linearParams.getAlpha());
1191+
}
1192+
if (linearParams.getBeta() != null) {
1193+
combineArgs.add("BETA");
1194+
combineArgs.add(linearParams.getBeta());
1195+
}
1196+
if (linearParams.getWindow() != null) {
1197+
combineArgs.add("WINDOW");
1198+
combineArgs.add(linearParams.getWindow());
1199+
}
1200+
cmdArgs.add("LINEAR");
1201+
cmdArgs.add(combineArgs.size());
1202+
cmdArgs.addAll(combineArgs);
1203+
1204+
// YIELD_SCORE_AS for combined score
1205+
if (linearParams.getScoreAlias() != null) {
1206+
cmdArgs.add("YIELD_SCORE_AS");
1207+
cmdArgs.add(linearParams.getScoreAlias());
1208+
}
1209+
}
1210+
}
1211+
1212+
if (options.getLimitOffset() != null && options.getLimitCount() != null) {
1213+
cmdArgs.add("LIMIT");
1214+
cmdArgs.add(options.getLimitOffset());
1215+
cmdArgs.add(options.getLimitCount());
1216+
}
1217+
1218+
if (options.getSortFieldName() != null) {
1219+
cmdArgs.add("SORTBY");
1220+
if (options.getSortOrder() != null) {
1221+
cmdArgs.add(2);
1222+
} else {
1223+
cmdArgs.add(1);
1224+
}
1225+
cmdArgs.add(options.getSortFieldName());
1226+
if (options.getSortOrder() != null) {
1227+
cmdArgs.add(options.getSortOrder().name());
1228+
}
1229+
}
1230+
1231+
if (options.isNoSort()) {
1232+
cmdArgs.add("NOSORT");
1233+
}
1234+
1235+
if (options.getLoadFields() != null && !options.getLoadFields().isEmpty()) {
1236+
cmdArgs.add("LOAD");
1237+
cmdArgs.add(options.getLoadFields().size());
1238+
cmdArgs.addAll(options.getLoadFields());
1239+
}
1240+
1241+
if (options.getGroupBy() != null) {
1242+
for (GroupBy groupBy : options.getGroupBy()) {
1243+
GroupParams groupParams = (GroupParams) groupBy;
1244+
cmdArgs.add("GROUPBY");
1245+
cmdArgs.add(groupParams.getFieldNames().size());
1246+
cmdArgs.addAll(groupParams.getFieldNames());
1247+
if (groupParams.getReducers() != null) {
1248+
for (Reducer reducer : groupParams.getReducers()) {
1249+
ReducerParams reducerParams = (ReducerParams) reducer;
1250+
cmdArgs.add("REDUCE");
1251+
cmdArgs.add(reducerParams.getFunctionName());
1252+
cmdArgs.add(reducerParams.getArgs().size());
1253+
if (!reducerParams.getArgs().isEmpty()) {
1254+
cmdArgs.addAll(reducerParams.getArgs());
1255+
}
1256+
if (reducerParams.getAs() != null) {
1257+
cmdArgs.add("AS");
1258+
cmdArgs.add(reducerParams.getAs());
1259+
}
1260+
}
1261+
}
1262+
}
1263+
}
1264+
1265+
if (options.getExpressions() != null) {
1266+
for (Expression expr : options.getExpressions()) {
1267+
cmdArgs.add("APPLY");
1268+
cmdArgs.add(expr.getValue());
1269+
cmdArgs.add("AS");
1270+
cmdArgs.add(expr.getAs());
1271+
}
1272+
}
1273+
1274+
if (options.getPostFilter() != null) {
1275+
cmdArgs.add("FILTER");
1276+
cmdArgs.add(options.getPostFilter());
1277+
}
1278+
1279+
if (options.getParams() != null && !options.getParams().isEmpty()) {
1280+
List<Object> paramsArgs = new ArrayList<>();
1281+
for (Map.Entry<String, Object> entry : options.getParams().entrySet()) {
1282+
paramsArgs.add(entry.getKey());
1283+
paramsArgs.add(entry.getValue());
1284+
}
1285+
cmdArgs.add("PARAMS");
1286+
cmdArgs.add(paramsArgs.size());
1287+
cmdArgs.addAll(paramsArgs);
1288+
}
1289+
1290+
if (options.getTimeout() != null) {
1291+
cmdArgs.add("TIMEOUT");
1292+
cmdArgs.add(options.getTimeout().toMillis());
1293+
}
1294+
1295+
return commandExecutor.readAsync((String) null, codec, RedisCommands.HYBRID_SEARCH, cmdArgs.toArray());
1296+
}
10931297
}

redisson/src/main/java/org/redisson/api/RSearch.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,10 @@
2323
import org.redisson.api.search.index.IndexInfo;
2424
import org.redisson.api.search.index.IndexOptions;
2525
import org.redisson.api.search.index.FieldIndex;
26+
import org.redisson.api.search.query.hybrid.HybridSearchResult;
2627
import org.redisson.api.search.query.QueryOptions;
2728
import org.redisson.api.search.query.SearchResult;
29+
import org.redisson.api.search.query.hybrid.HybridQueryArgs;
2830

2931
import java.util.List;
3032
import java.util.Map;
@@ -71,6 +73,28 @@ public interface RSearch extends RSearchAsync {
7173
*/
7274
SearchResult search(String indexName, String query, QueryOptions options);
7375

76+
/**
77+
* Performs hybrid search combining text search and vector similarity
78+
* using the FT.HYBRID command.
79+
* <p>
80+
* Requires Redis Stack 8.4.0 or higher.
81+
* <p>
82+
* Usage example:
83+
* <pre>
84+
* SearchResult result = search.hybridSearch("myIndex",
85+
* HybridQueryArgs.query("laptop")
86+
* .vectorSimilarity("@embedding", "$vec")
87+
* .nearestNeighbors(10)
88+
* .params(Map.of("vec", vectorBytes))
89+
* .limit(0, 10));
90+
* </pre>
91+
*
92+
* @param indexName the name of the index
93+
* @param args hybrid query arguments
94+
* @return search result
95+
*/
96+
HybridSearchResult hybridSearch(String indexName, HybridQueryArgs args);
97+
7498
/**
7599
* Executes aggregation over defined index using defined query.
76100
* <p>

redisson/src/main/java/org/redisson/api/RSearchAsync.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@
2121
import org.redisson.api.search.index.FieldIndex;
2222
import org.redisson.api.search.index.IndexInfo;
2323
import org.redisson.api.search.index.IndexOptions;
24+
import org.redisson.api.search.query.hybrid.HybridSearchResult;
2425
import org.redisson.api.search.query.QueryOptions;
2526
import org.redisson.api.search.query.SearchResult;
27+
import org.redisson.api.search.query.hybrid.HybridQueryArgs;
2628

2729
import java.util.List;
2830
import java.util.Map;
@@ -69,6 +71,18 @@ public interface RSearchAsync {
6971
*/
7072
RFuture<SearchResult> searchAsync(String indexName, String query, QueryOptions options);
7173

74+
/**
75+
* Performs hybrid search combining text search and vector similarity
76+
* using the FT.HYBRID command.
77+
* <p>
78+
* Requires <b>Redis 8.4.0 and higher.</b>
79+
*
80+
* @param indexName the name of the index
81+
* @param args hybrid query arguments
82+
* @return search result
83+
*/
84+
RFuture<HybridSearchResult> hybridSearchAsync(String indexName, HybridQueryArgs args);
85+
7286
/**
7387
* Executes aggregation over defined index using defined query.
7488
* <p>

redisson/src/main/java/org/redisson/api/RSearchReactive.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
import org.redisson.api.search.index.IndexOptions;
2626
import org.redisson.api.search.query.QueryOptions;
2727
import org.redisson.api.search.query.SearchResult;
28+
import org.redisson.api.search.query.hybrid.HybridQueryArgs;
29+
import org.redisson.api.search.query.hybrid.HybridSearchResult;
2830
import reactor.core.publisher.Mono;
2931

3032
import java.util.List;
@@ -72,6 +74,18 @@ public interface RSearchReactive {
7274
*/
7375
Mono<SearchResult> search(String indexName, String query, QueryOptions options);
7476

77+
/**
78+
* Performs hybrid search combining text search and vector similarity
79+
* using the FT.HYBRID command.
80+
* <p>
81+
* Requires Redis Stack 8.4.0 or higher.
82+
*
83+
* @param indexName the name of the index
84+
* @param args hybrid query arguments
85+
* @return search result
86+
*/
87+
Mono<HybridSearchResult> hybridSearch(String indexName, HybridQueryArgs args);
88+
7589
/**
7690
* Executes aggregation over defined index using defined query.
7791
* <p>

redisson/src/main/java/org/redisson/api/RSearchRx.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
import org.redisson.api.search.index.IndexOptions;
2929
import org.redisson.api.search.query.QueryOptions;
3030
import org.redisson.api.search.query.SearchResult;
31+
import org.redisson.api.search.query.hybrid.HybridQueryArgs;
32+
import org.redisson.api.search.query.hybrid.HybridSearchResult;
3133

3234
import java.util.List;
3335
import java.util.Map;
@@ -74,6 +76,18 @@ public interface RSearchRx {
7476
*/
7577
Single<SearchResult> search(String indexName, String query, QueryOptions options);
7678

79+
/**
80+
* Performs hybrid search combining text search and vector similarity
81+
* using the FT.HYBRID command.
82+
* <p>
83+
* Requires Redis Stack 8.4.0 or higher.
84+
*
85+
* @param indexName the name of the index
86+
* @param args hybrid query arguments
87+
* @return search result
88+
*/
89+
Single<HybridSearchResult> hybridSearch(String indexName, HybridQueryArgs args);
90+
7791
/**
7892
* Executes aggregation over defined index using defined query.
7993
* <p>
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/**
2+
* Copyright (c) 2013-2024 Nikita Koksharov
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.redisson.api.search.query.hybrid;
17+
18+
/**
19+
* Combine configuration for hybrid search fusion methods.
20+
* <p>
21+
* Supports Reciprocal Rank Fusion (RRF) and Linear combination methods.
22+
*
23+
* @author Nikita Koksharov
24+
*/
25+
public interface Combine {
26+
27+
/**
28+
* Creates a Reciprocal Rank Fusion (RRF) combine configuration.
29+
* <p>
30+
* RRF combines rankings from text search and vector similarity using
31+
* the formula: score = 1 / (constant + rank)
32+
*
33+
* @return RRF configuration step
34+
*/
35+
static CombineReciprocalRankFusionStep reciprocalRankFusion() {
36+
return new CombineRrfParams();
37+
}
38+
39+
/**
40+
* Creates a Linear combination configuration.
41+
* <p>
42+
* Linear combination uses weighted scores: score = alpha * text_score + beta * vector_score
43+
*
44+
* @return Linear configuration step
45+
*/
46+
static CombineLinearStep linear() {
47+
return new CombineLinearParams();
48+
}
49+
50+
}

0 commit comments

Comments
 (0)