🚀 feat: Support Redis Clusters, Trusted Proxy Setting, And Toggle Meilisearch Indexing (#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 <gil.assuncao@parceiros.nos.pt>
Co-authored-by: Pedro Reis <pedro.malheiro@parceiros.nos.pt>
Co-authored-by: João Trigo Soares <joao.soares@parceiros.nos.pt>
This commit is contained in:
Danny Avila 2025-02-20 17:39:12 -05:00 committed by GitHub
parent 46a96b9caa
commit 1e625f7557
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 101 additions and 63 deletions

View file

@ -20,6 +20,11 @@ DOMAIN_CLIENT=http://localhost:3080
DOMAIN_SERVER=http://localhost:3080 DOMAIN_SERVER=http://localhost:3080
NO_INDEX=true 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 # # JSON Logging #
@ -292,6 +297,10 @@ MEILI_NO_ANALYTICS=true
MEILI_HOST=http://0.0.0.0:7700 MEILI_HOST=http://0.0.0.0:7700
MEILI_MASTER_KEY=DrhYf7zENyR6AlUCKmnz0eYASOQdl6zxH7s7MKFSfFCt 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 # # Speech to Text & Text to Speech #
#==================================================# #==================================================#
@ -495,6 +504,16 @@ HELP_AND_FAQ_URL=https://librechat.ai
# Google tag manager id # Google tag manager id
#ANALYTICS_GTM_ID=user provided 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 # # Others #
#==================================================# #==================================================#
@ -502,9 +521,6 @@ HELP_AND_FAQ_URL=https://librechat.ai
# NODE_ENV= # NODE_ENV=
# REDIS_URI=
# USE_REDIS=
# E2E_USER_EMAIL= # E2E_USER_EMAIL=
# E2E_USER_PASSWORD= # E2E_USER_PASSWORD=

View file

@ -1,50 +0,0 @@
name: Question
description: Ask your question
title: "[Question]: "
labels: ["❓ question"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill this!
- type: textarea
id: what-is-your-question
attributes:
label: What is your question?
description: Please give as many details as possible
placeholder: Please give as many details as possible
validations:
required: true
- type: textarea
id: more-details
attributes:
label: More Details
description: Please provide more details if needed.
placeholder: Please provide more details if needed.
validations:
required: true
- type: dropdown
id: browsers
attributes:
label: What is the main subject of your question?
multiple: true
options:
- Documentation
- Installation
- UI
- Endpoints
- User System/OAuth
- Other
- type: textarea
id: screenshots
attributes:
label: Screenshots
description: If applicable, add screenshots to help explain your problem. You can drag and drop, paste images directly here or link to them.
- type: checkboxes
id: terms
attributes:
label: Code of Conduct
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/danny-avila/LibreChat/blob/main/.github/CODE_OF_CONDUCT.md)
options:
- label: I agree to follow this project's Code of Conduct
required: true

View file

@ -1,15 +1,81 @@
const fs = require('fs');
const ioredis = require('ioredis');
const KeyvRedis = require('@keyv/redis'); const KeyvRedis = require('@keyv/redis');
const { isEnabled } = require('~/server/utils'); const { isEnabled } = require('~/server/utils');
const logger = require('~/config/winston'); 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; 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)) { 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.on('error', (err) => logger.error('KeyvRedis connection error:', err));
keyvRedis.setMaxListeners(20); keyvRedis.setMaxListeners(redis_max_listeners);
logger.info( 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.', '[Optional] Redis initialized. Note: Redis support is experimental. If you have issues, disable it. Cache needs to be flushed for values to refresh.',
); );

View file

@ -1,9 +1,11 @@
const { MeiliSearch } = require('meilisearch'); const { MeiliSearch } = require('meilisearch');
const Conversation = require('~/models/schema/convoSchema'); const Conversation = require('~/models/schema/convoSchema');
const Message = require('~/models/schema/messageSchema'); const Message = require('~/models/schema/messageSchema');
const { isEnabled } = require('~/server/utils');
const { logger } = require('~/config'); 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; let currentTimeout = null;
class MeiliSearchClient { class MeiliSearchClient {
@ -23,8 +25,7 @@ class MeiliSearchClient {
} }
} }
// eslint-disable-next-line no-unused-vars async function indexSync() {
async function indexSync(req, res, next) {
if (!searchEnabled) { if (!searchEnabled) {
return; return;
} }
@ -33,10 +34,15 @@ async function indexSync(req, res, next) {
const client = MeiliSearchClient.getInstance(); const client = MeiliSearchClient.getInstance();
const { status } = await client.health(); const { status } = await client.health();
if (status !== 'available' || !process.env.SEARCH) { if (status !== 'available') {
throw new Error('Meilisearch not available'); throw new Error('Meilisearch not available');
} }
if (indexingDisabled === true) {
logger.info('[indexSync] Indexing is disabled, skipping...');
return;
}
const messageCount = await Message.countDocuments(); const messageCount = await Message.countDocuments();
const convoCount = await Conversation.countDocuments(); const convoCount = await Conversation.countDocuments();
const messages = await client.index('messages').getStats(); const messages = await client.index('messages').getStats();
@ -71,7 +77,6 @@ async function indexSync(req, res, next) {
logger.info('[indexSync] Meilisearch not configured, search will be disabled.'); logger.info('[indexSync] Meilisearch not configured, search will be disabled.');
} else { } else {
logger.error('[indexSync] error', err); logger.error('[indexSync] error', err);
// res.status(500).json({ error: 'Server error' });
} }
} }
} }

View file

@ -22,10 +22,11 @@ const staticCache = require('./utils/staticCache');
const noIndex = require('./middleware/noIndex'); const noIndex = require('./middleware/noIndex');
const routes = require('./routes'); 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 port = Number(PORT) || 3080;
const host = HOST || 'localhost'; const host = HOST || 'localhost';
const trusted_proxy = Number(TRUST_PROXY) || 1; /* trust first proxy by default */
const startServer = async () => { const startServer = async () => {
if (typeof Bun !== 'undefined') { if (typeof Bun !== 'undefined') {
@ -53,7 +54,7 @@ const startServer = async () => {
app.use(staticCache(app.locals.paths.dist)); app.use(staticCache(app.locals.paths.dist));
app.use(staticCache(app.locals.paths.fonts)); app.use(staticCache(app.locals.paths.fonts));
app.use(staticCache(app.locals.paths.assets)); 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(cors());
app.use(cookieParser()); app.use(cookieParser());