/**
* Redis Vector Memory
* Uses Redis Stack with vector similarity search (RediSearch)
*/
class extends="BaseVectorMemory" implements="IVectorMemory" {
/**
* Configure Redis connection
*/
function configure( required struct config ) {
super.configure( arguments.config );
variables.redisHost = arguments.config.redisHost ?: "localhost";
variables.redisPort = arguments.config.redisPort ?: 6379;
variables.redisPassword = arguments.config.redisPassword ?: "";
variables.indexName = "idx:#arguments.config.collection ?: 'ai_memory'#";
// Initialize Redis connection
initializeRedis();
// Create index if needed
ensureIndex();
return this;
}
/**
* Initialize Redis connection
*/
private function initializeRedis() {
variables.redisURL = "redis://#variables.redisHost#:#variables.redisPort#";
// Test connection
var response = redisCommand( "PING" );
if ( response != "PONG" ) {
throw( type="RedisConnectionException", message="Failed to connect to Redis" );
}
}
/**
* Ensure search index exists
*/
private function ensureIndex() {
// Check if index exists
try {
redisCommand( "FT.INFO", [ variables.indexName ] );
} catch ( any e ) {
// Create index with vector field
redisCommand( "FT.CREATE", [
variables.indexName,
"ON", "HASH",
"PREFIX", "1", "msg:#variables.key#:",
"SCHEMA",
"role", "TAG",
"content", "TEXT",
"embedding", "VECTOR", "HNSW", "6",
"TYPE", "FLOAT32",
"DIM", variables.dimensions,
"DISTANCE_METRIC", mapMetricToRedis( variables.metric ),
"timestamp", "NUMERIC", "SORTABLE"
] );
}
}
/**
* Add message with embedding
*/
function add( required any message ) {
var msg = isSimpleValue( arguments.message )
? { role: "user", content: arguments.message }
: arguments.message;
var embedding = generateEmbedding( msg.content );
var msgId = "msg:#variables.key#:#createUUID()#";
// Store as Redis hash
redisCommand( "HSET", [
msgId,
"role", msg.role,
"content", msg.content,
"embedding", serializeVector( embedding ),
"timestamp", getTickCount(),
"metadata", jsonSerialize( msg.metadata ?: {} )
] );
return this;
}
/**
* Get relevant messages using vector search
*/
function getRelevant( required string query, numeric limit = 5 ) {
var queryEmbedding = generateEmbedding( arguments.query );
var vectorBlob = serializeVector( queryEmbedding );
// Search using vector similarity
var results = redisCommand( "FT.SEARCH", [
variables.indexName,
"*=>[KNN #arguments.limit# @embedding $vector AS score]",
"PARAMS", "2", "vector", vectorBlob,
"SORTBY", "score",
"DIALECT", "2",
"RETURN", "4", "role", "content", "timestamp", "score"
] );
return parseRedisResults( results );
}
/**
* Get all messages
*/
function getAll() {
var pattern = "msg:#variables.key#:*";
var keys = redisCommand( "KEYS", [ pattern ] );
var messages = [];
keys.each( key => {
var data = redisCommand( "HGETALL", [ key ] );
messages.append( parseHashData( data ) );
} );
// Sort by timestamp
messages.sort( ( a, b ) => compare( a.timestamp, b.timestamp ) );
return messages;
}
/**
* Clear all messages
*/
function clear() {
var pattern = "msg:#variables.key#:*";
var keys = redisCommand( "KEYS", [ pattern ] );
if ( keys.len() ) {
redisCommand( "DEL", keys );
}
return this;
}
/**
* Count messages
*/
function count() {
var pattern = "msg:#variables.key#:*";
var keys = redisCommand( "KEYS", [ pattern ] );
return keys.len();
}
/**
* Execute Redis command via HTTP API or native client
*/
private function redisCommand( required string command, array args = [] ) {
// Simplified: Use HTTP API or native Redis client
// This would use actual Redis client library in production
var cmdArray = [ arguments.command ];
cmdArray.append( arguments.args, true );
// Implementation would call actual Redis client
// For example: redisClient.execute( cmdArray )
return executeRedisCommand( cmdArray );
}
/**
* Serialize vector for Redis storage
*/
private function serializeVector( required array vector ) {
// Convert array of floats to binary blob
var buffer = createObject( "java", "java.nio.ByteBuffer" )
.allocate( arguments.vector.len() * 4 );
arguments.vector.each( val => {
buffer.putFloat( val castAs float );
} );
return buffer.array();
}
/**
* Map metric to Redis format
*/
private function mapMetricToRedis( required string metric ) {
return {
"cosine": "COSINE",
"euclidean": "L2",
"dot": "IP"
}[ arguments.metric ] ?: "COSINE";
}
/**
* Parse Redis search results
*/
private function parseRedisResults( required array results ) {
var messages = [];
// Redis returns: [count, key1, [field1, value1, ...], key2, ...]
var count = results[ 1 ];
for ( var i = 2; i <= results.len(); i += 2 ) {
var fields = results[ i + 1 ];
var msg = {};
for ( var j = 1; j <= fields.len(); j += 2 ) {
msg[ fields[ j ] ] = fields[ j + 1 ];
}
messages.append( msg );
}
return messages;
}
/**
* Generate embedding
*/
private function generateEmbedding( required string text ) {
var result = aiEmbed(
input: arguments.text,
provider: variables.embeddingProvider,
model: variables.embeddingModel
);
return result.embeddings[ 1 ];
}
}