Skip to content

Commit 77aaa15

Browse files
committed
feat: add LangChain4J integration with EmbeddingStore, ContentRetriever, ChatMemoryStore, and DocumentStore
Implements complete LangChain4J integration for RedisVL: - **RedisVLEmbeddingStore**: Full EmbeddingStore implementation with vector search - Implements new search(EmbeddingSearchRequest) API (non-deprecated) - Supports removeAll() variants (by IDs, by filter placeholder, and all documents) - Automatic COSINE distance to similarity score conversion - Batch operations via addAll() - Metadata storage and retrieval via JSON - **RedisVLContentRetriever**: ContentRetriever for RAG workflows - Integrates with EmbeddingStore for semantic search - Builder pattern with configurable maxResults and minScore - Automatic query embedding and result conversion - **RedisVLChatMemoryStore**: Chat memory storage for conversation history - Persistent message storage with session/memory ID support - Full CRUD operations (get, update, delete messages) - **RedisVLDocumentStore**: Binary document storage utility - Stores documents (PDFs, images) with metadata - Supports retrieval and deletion operations - **Utils**: Added score conversion helpers - normCosineDistance(): Redis distance (0-2) -> similarity (0-1) - denormCosineDistance(): similarity (0-1) -> Redis distance (0-2) - **IndexSchema field alias support**: - Modified parseField() to read "as", "separator", and "caseSensitive" from attrs map - Enables field aliases for TAG and NUMERIC fields in JSON schemas - Critical for clean query syntax (e.g., @category instead of @$.category) - **RedisVLEmbeddingStore JSON storage fixes**: - Vector field resolution: Prefer aliases over JSONPath names to avoid special chars ($, .) - Document parsing: Handle JSON storage where vector queries return docs under "$" key - Proper field extraction from nested JSON for text and metadata retrieval
1 parent 598f7e6 commit 77aaa15

14 files changed

+3146
-1
lines changed
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
package com.redis.vl.langchain4j;
2+
3+
import com.redis.vl.query.Filter;
4+
import dev.langchain4j.store.embedding.filter.comparison.*;
5+
import dev.langchain4j.store.embedding.filter.logical.And;
6+
import dev.langchain4j.store.embedding.filter.logical.Not;
7+
import dev.langchain4j.store.embedding.filter.logical.Or;
8+
9+
/**
10+
* Maps LangChain4J Filter types to RedisVL Filter queries.
11+
*
12+
* <p>Converts dev.langchain4j.store.embedding.filter.Filter to com.redis.vl.query.Filter for use
13+
* with RedisVL SearchIndex.
14+
*
15+
* <p>Supported filters:
16+
*
17+
* <ul>
18+
* <li>Comparison: IsEqualTo, IsNotEqualTo, IsGreaterThan, IsGreaterThanOrEqualTo, IsLessThan,
19+
* IsLessThanOrEqualTo, IsIn, IsNotIn
20+
* <li>Logical: And, Or, Not
21+
* </ul>
22+
*/
23+
public class LangChain4JFilterMapper {
24+
25+
/**
26+
* Map LangChain4J Filter to RedisVL Filter.
27+
*
28+
* @param filter LangChain4J filter (can be null)
29+
* @return RedisVL Filter, or wildcard (*) filter if input is null
30+
*/
31+
public static Filter map(dev.langchain4j.store.embedding.filter.Filter filter) {
32+
if (filter == null) {
33+
// Return wildcard filter for "match all"
34+
return Filter.custom("*");
35+
}
36+
37+
// Comparison filters
38+
if (filter instanceof IsEqualTo) {
39+
return mapEqual((IsEqualTo) filter);
40+
} else if (filter instanceof IsNotEqualTo) {
41+
return mapNotEqual((IsNotEqualTo) filter);
42+
} else if (filter instanceof IsGreaterThan) {
43+
return mapGreaterThan((IsGreaterThan) filter);
44+
} else if (filter instanceof IsGreaterThanOrEqualTo) {
45+
return mapGreaterThanOrEqual((IsGreaterThanOrEqualTo) filter);
46+
} else if (filter instanceof IsLessThan) {
47+
return mapLessThan((IsLessThan) filter);
48+
} else if (filter instanceof IsLessThanOrEqualTo) {
49+
return mapLessThanOrEqual((IsLessThanOrEqualTo) filter);
50+
} else if (filter instanceof IsIn) {
51+
return mapIn((IsIn) filter);
52+
} else if (filter instanceof IsNotIn) {
53+
return mapNotIn((IsNotIn) filter);
54+
}
55+
56+
// Logical filters
57+
else if (filter instanceof And) {
58+
return mapAnd((And) filter);
59+
} else if (filter instanceof Or) {
60+
return mapOr((Or) filter);
61+
} else if (filter instanceof Not) {
62+
return mapNot((Not) filter);
63+
}
64+
65+
throw new UnsupportedOperationException(
66+
"Unsupported filter type: " + filter.getClass().getName());
67+
}
68+
69+
private static Filter mapEqual(IsEqualTo filter) {
70+
String key = filter.key();
71+
Object value = filter.comparisonValue();
72+
73+
// Try numeric first
74+
if (value instanceof Number) {
75+
Number num = (Number) value;
76+
if (value instanceof Integer || value instanceof Long) {
77+
return Filter.numeric(key).eq(num.intValue());
78+
} else {
79+
return Filter.numeric(key).eq(num.doubleValue());
80+
}
81+
}
82+
83+
// String values - use tag for better performance
84+
return Filter.tag(key, value.toString());
85+
}
86+
87+
private static Filter mapNotEqual(IsNotEqualTo filter) {
88+
return Filter.not(mapEqual(new IsEqualTo(filter.key(), filter.comparisonValue())));
89+
}
90+
91+
private static Filter mapGreaterThan(IsGreaterThan filter) {
92+
String key = filter.key();
93+
Object value = filter.comparisonValue();
94+
95+
if (!(value instanceof Number)) {
96+
throw new IllegalArgumentException(
97+
"Greater than comparison only supports numeric values, got: " + value.getClass());
98+
}
99+
100+
Number num = (Number) value;
101+
if (value instanceof Integer || value instanceof Long) {
102+
return Filter.numeric(key).gt(num.intValue());
103+
} else {
104+
return Filter.numeric(key).gt(num.doubleValue());
105+
}
106+
}
107+
108+
private static Filter mapGreaterThanOrEqual(IsGreaterThanOrEqualTo filter) {
109+
String key = filter.key();
110+
Object value = filter.comparisonValue();
111+
112+
if (!(value instanceof Number)) {
113+
throw new IllegalArgumentException(
114+
"Greater than or equal comparison only supports numeric values, got: "
115+
+ value.getClass());
116+
}
117+
118+
Number num = (Number) value;
119+
if (value instanceof Integer || value instanceof Long) {
120+
return Filter.numeric(key).gte(num.intValue());
121+
} else {
122+
return Filter.numeric(key).gte(num.doubleValue());
123+
}
124+
}
125+
126+
private static Filter mapLessThan(IsLessThan filter) {
127+
String key = filter.key();
128+
Object value = filter.comparisonValue();
129+
130+
if (!(value instanceof Number)) {
131+
throw new IllegalArgumentException(
132+
"Less than comparison only supports numeric values, got: " + value.getClass());
133+
}
134+
135+
Number num = (Number) value;
136+
if (value instanceof Integer || value instanceof Long) {
137+
return Filter.numeric(key).lt(num.intValue());
138+
} else {
139+
return Filter.numeric(key).lt(num.doubleValue());
140+
}
141+
}
142+
143+
private static Filter mapLessThanOrEqual(IsLessThanOrEqualTo filter) {
144+
String key = filter.key();
145+
Object value = filter.comparisonValue();
146+
147+
if (!(value instanceof Number)) {
148+
throw new IllegalArgumentException(
149+
"Less than or equal comparison only supports numeric values, got: " + value.getClass());
150+
}
151+
152+
Number num = (Number) value;
153+
if (value instanceof Integer || value instanceof Long) {
154+
return Filter.numeric(key).lte(num.intValue());
155+
} else {
156+
return Filter.numeric(key).lte(num.doubleValue());
157+
}
158+
}
159+
160+
private static Filter mapIn(IsIn filter) {
161+
String key = filter.key();
162+
var values = filter.comparisonValues();
163+
164+
if (values == null || values.isEmpty()) {
165+
return Filter.custom("*"); // Match all if no values
166+
}
167+
168+
// Convert all values to strings for tag filter
169+
String[] tagValues = values.stream().map(Object::toString).toArray(String[]::new);
170+
return Filter.tag(key, tagValues);
171+
}
172+
173+
private static Filter mapNotIn(IsNotIn filter) {
174+
return Filter.not(mapIn(new IsIn(filter.key(), filter.comparisonValues())));
175+
}
176+
177+
private static Filter mapAnd(And filter) {
178+
Filter left = map(filter.left());
179+
Filter right = map(filter.right());
180+
return Filter.and(left, right);
181+
}
182+
183+
private static Filter mapOr(Or filter) {
184+
Filter left = map(filter.left());
185+
Filter right = map(filter.right());
186+
return Filter.or(left, right);
187+
}
188+
189+
private static Filter mapNot(Not filter) {
190+
return Filter.not(map(filter.expression()));
191+
}
192+
}

0 commit comments

Comments
 (0)