mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-22 08:12:00 +02:00
🚀 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:
parent
46a96b9caa
commit
1e625f7557
5 changed files with 101 additions and 63 deletions
22
.env.example
22
.env.example
|
@ -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=
|
||||||
|
|
||||||
|
|
50
.github/ISSUE_TEMPLATE/QUESTION.yml
vendored
50
.github/ISSUE_TEMPLATE/QUESTION.yml
vendored
|
@ -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
|
|
72
api/cache/keyvRedis.js
vendored
72
api/cache/keyvRedis.js
vendored
|
@ -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.',
|
||||||
);
|
);
|
||||||
|
|
|
@ -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' });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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());
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue