Skip to content

Commit

Permalink
🚀 feat: Support Redis Clusters, Trusted Proxy Setting, And Toggle Mei…
Browse files Browse the repository at this point in the history
…lisearch Indexing (danny-avila#5963)

* refactor: Improve MeiliSearch integration with environment-based configuration for running index sync

* chore: Remove Question issue template from GitHub repository

* feat: Enable indexing in MeiliSearch configuration and clean up error handling in indexSync

* feat: Update .env.example to include optional indexing configuration

* refactor: rename env var for disabling index sync to MEILI_NO_SYNC

* Added the option to change the default trusted proxy

* feat: Add TRUST_PROXY configuration to .env.example for reverse proxy settings

* feat: Enhance Redis support with cluster configuration and TLS options

* feat(redis): add cluster support, environment config and url mapping

- Add Redis cluster configuration with isEnabled flag
- Configure prefix and max listeners settings
- Improve code formatting and readability
- Fix URL vs host parameter handling
- Update environment variables and regex patterns

---------

Co-authored-by: Gil Assunção <[email protected]>
Co-authored-by: Pedro Reis <[email protected]>
Co-authored-by: João Trigo Soares <[email protected]>
  • Loading branch information
4 people authored Feb 20, 2025
1 parent 46a96b9 commit 1e625f7
Show file tree
Hide file tree
Showing 5 changed files with 101 additions and 63 deletions.
22 changes: 19 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ DOMAIN_CLIENT=http://localhost:3080
DOMAIN_SERVER=http://localhost:3080

NO_INDEX=true
# Use the address that is at most n number of hops away from the Express application.
# req.socket.remoteAddress is the first hop, and the rest are looked for in the X-Forwarded-For header from right to left.
# A value of 0 means that the first untrusted address would be req.socket.remoteAddress, i.e. there is no reverse proxy.
# Defaulted to 1.
TRUST_PROXY=1

#===============#
# JSON Logging #
Expand Down Expand Up @@ -292,6 +297,10 @@ MEILI_NO_ANALYTICS=true
MEILI_HOST=http://0.0.0.0:7700
MEILI_MASTER_KEY=DrhYf7zENyR6AlUCKmnz0eYASOQdl6zxH7s7MKFSfFCt

# Optional: Disable indexing, useful in a multi-node setup
# where only one instance should perform an index sync.
# MEILI_NO_SYNC=true

#==================================================#
# Speech to Text & Text to Speech #
#==================================================#
Expand Down Expand Up @@ -495,16 +504,23 @@ HELP_AND_FAQ_URL=https://librechat.ai
# Google tag manager id
#ANALYTICS_GTM_ID=user provided google tag manager id

#===============#
# REDIS Options #
#===============#

# REDIS_URI=10.10.10.10:6379
# USE_REDIS=true

# USE_REDIS_CLUSTER=true
# REDIS_CA=/path/to/ca.crt

#==================================================#
# Others #
#==================================================#
# You should leave the following commented out #

# NODE_ENV=

# REDIS_URI=
# USE_REDIS=

# E2E_USER_EMAIL=
# E2E_USER_PASSWORD=

Expand Down
50 changes: 0 additions & 50 deletions .github/ISSUE_TEMPLATE/QUESTION.yml

This file was deleted.

72 changes: 69 additions & 3 deletions api/cache/keyvRedis.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,81 @@
const fs = require('fs');
const ioredis = require('ioredis');
const KeyvRedis = require('@keyv/redis');
const { isEnabled } = require('~/server/utils');
const logger = require('~/config/winston');

const { REDIS_URI, USE_REDIS } = process.env;
const { REDIS_URI, USE_REDIS, USE_REDIS_CLUSTER, REDIS_CA, REDIS_KEY_PREFIX, REDIS_MAX_LISTENERS } =
process.env;

let keyvRedis;
const redis_prefix = REDIS_KEY_PREFIX || '';
const redis_max_listeners = REDIS_MAX_LISTENERS || 10;

function mapURI(uri) {
const regex =
/^(?:(?<scheme>\w+):\/\/)?(?:(?<user>[^:@]+)(?::(?<password>[^@]+))?@)?(?<host>[\w.-]+)(?::(?<port>\d{1,5}))?$/;
const match = uri.match(regex);

if (match) {
const { scheme, user, password, host, port } = match.groups;

return {
scheme: scheme || 'none',
user: user || null,
password: password || null,
host: host || null,
port: port || null,
};
} else {
const parts = uri.split(':');
if (parts.length === 2) {
return {
scheme: 'none',
user: null,
password: null,
host: parts[0],
port: parts[1],
};
}

return {
scheme: 'none',
user: null,
password: null,
host: uri,
port: null,
};
}
}

if (REDIS_URI && isEnabled(USE_REDIS)) {
keyvRedis = new KeyvRedis(REDIS_URI, { useRedisSets: false });
let redisOptions = null;
let keyvOpts = {
useRedisSets: false,
keyPrefix: redis_prefix,
};

if (REDIS_CA) {
const ca = fs.readFileSync(REDIS_CA);
redisOptions = { tls: { ca } };
}

if (isEnabled(USE_REDIS_CLUSTER)) {
const hosts = REDIS_URI.split(',').map((item) => {
var value = mapURI(item);

return {
host: value.host,
port: value.port,
};
});
const cluster = new ioredis.Cluster(hosts, { redisOptions });
keyvRedis = new KeyvRedis(cluster, keyvOpts);
} else {
keyvRedis = new KeyvRedis(REDIS_URI, keyvOpts);
}
keyvRedis.on('error', (err) => logger.error('KeyvRedis connection error:', err));
keyvRedis.setMaxListeners(20);
keyvRedis.setMaxListeners(redis_max_listeners);
logger.info(
'[Optional] Redis initialized. Note: Redis support is experimental. If you have issues, disable it. Cache needs to be flushed for values to refresh.',
);
Expand Down
15 changes: 10 additions & 5 deletions api/lib/db/indexSync.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
const { MeiliSearch } = require('meilisearch');
const Conversation = require('~/models/schema/convoSchema');
const Message = require('~/models/schema/messageSchema');
const { isEnabled } = require('~/server/utils');
const { logger } = require('~/config');

const searchEnabled = process.env?.SEARCH?.toLowerCase() === 'true';
const searchEnabled = isEnabled(process.env.SEARCH);
const indexingDisabled = isEnabled(process.env.MEILI_NO_SYNC);
let currentTimeout = null;

class MeiliSearchClient {
Expand All @@ -23,8 +25,7 @@ class MeiliSearchClient {
}
}

// eslint-disable-next-line no-unused-vars
async function indexSync(req, res, next) {
async function indexSync() {
if (!searchEnabled) {
return;
}
Expand All @@ -33,10 +34,15 @@ async function indexSync(req, res, next) {
const client = MeiliSearchClient.getInstance();

const { status } = await client.health();
if (status !== 'available' || !process.env.SEARCH) {
if (status !== 'available') {
throw new Error('Meilisearch not available');
}

if (indexingDisabled === true) {
logger.info('[indexSync] Indexing is disabled, skipping...');
return;
}

const messageCount = await Message.countDocuments();
const convoCount = await Conversation.countDocuments();
const messages = await client.index('messages').getStats();
Expand Down Expand Up @@ -71,7 +77,6 @@ async function indexSync(req, res, next) {
logger.info('[indexSync] Meilisearch not configured, search will be disabled.');
} else {
logger.error('[indexSync] error', err);
// res.status(500).json({ error: 'Server error' });
}
}
}
Expand Down
5 changes: 3 additions & 2 deletions api/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,11 @@ const staticCache = require('./utils/staticCache');
const noIndex = require('./middleware/noIndex');
const routes = require('./routes');

const { PORT, HOST, ALLOW_SOCIAL_LOGIN, DISABLE_COMPRESSION } = process.env ?? {};
const { PORT, HOST, ALLOW_SOCIAL_LOGIN, DISABLE_COMPRESSION, TRUST_PROXY } = process.env ?? {};

const port = Number(PORT) || 3080;
const host = HOST || 'localhost';
const trusted_proxy = Number(TRUST_PROXY) || 1; /* trust first proxy by default */

const startServer = async () => {
if (typeof Bun !== 'undefined') {
Expand Down Expand Up @@ -53,7 +54,7 @@ const startServer = async () => {
app.use(staticCache(app.locals.paths.dist));
app.use(staticCache(app.locals.paths.fonts));
app.use(staticCache(app.locals.paths.assets));
app.set('trust proxy', 1); /* trust first proxy */
app.set('trust proxy', trusted_proxy);
app.use(cors());
app.use(cookieParser());

Expand Down

0 comments on commit 1e625f7

Please sign in to comment.