<think>我们需要在Node.js中使用Mongoose来接入本地知识库搜索,并整合AI(例如使用嵌入向量和向量搜索)。这里假设我们有一个本地知识库,存储为MongoDB中的文档,每个文档包含文本内容。我们将使用文本嵌入(embedding)技术将文本转换为向量,然后使用向量搜索来找到最相关的文档。
步骤:
1. 安装必要的依赖:mongoose, 一个嵌入模型(例如使用TensorFlow.js或调用外部API如OpenAI的嵌入API,但这里我们可能使用本地模型,比如`@tensorflow-models/universal-sentence-encoder`),以及一个支持向量搜索的MongoDB插件(或者使用MongoDB的Atlas Search,但如果是本地部署,我们可以使用其他方式,比如`mongodb`的`$vector`操作,但注意MongoDB原生支持向量搜索需要特定版本和配置)。
2. 定义Mongoose模式,其中包含文本字段和对应的向量字段(数组类型)。
3. 在保存文档之前,自动生成嵌入向量并存储。
4. 实现一个搜索函数,将查询文本转换为嵌入向量,然后在MongoDB中执行向量相似度搜索(例如使用$near操作符)。
注意:由于MongoDB在4.4版本后开始支持向量搜索,但功能有限,且通常需要Atlas。因此,我们也可以考虑使用其他向量数据库(如Milvus, FAISS等),但这里要求使用mongoose,所以我们假设使用MongoDB,并利用其进行向量搜索。
然而,在本地MongoDB中,我们可以使用`$vector`操作符(需要MongoDB 5.0+)?但实际上,在MongoDB 5.0中,向量搜索功能是通过Atlas Search提供的,本地MongoDB并不支持。因此,我们可能需要自己实现相似度计算(不推荐,效率低)或者使用其他方式。
替代方案:我们可以使用一个支持向量搜索的MongoDB插件,比如`mongo-vector-search`,或者使用MongoDB的聚合管道进行余弦相似度计算(需要自己计算点积和模)。但这样效率可能不高,尤其是当数据量大时。
因此,我们这里采用以下方法:
1. 使用一个本地嵌入模型(例如`sentence-transformers`,但这是在Python中,所以我们可以用TensorFlow.js的Universal Sentence Encoder模型在Node.js中生成嵌入向量)。
2. 将每个文档的嵌入向量存储在一个数组中。
3. 在搜索时,将查询文本转换为嵌入向量,然后使用MongoDB的聚合管道计算每个文档的余弦相似度,然后排序。
但是,这种方法在大数据集上会很慢,因为需要扫描整个集合。因此,我们只适用于小型知识库。
如果数据量大,建议使用专门的向量数据库,或者使用MongoDB Atlas的向量搜索功能。
下面我们以小型知识库为例,使用TensorFlow.js的Universal Sentence Encoder生成嵌入向量,然后使用MongoDB的聚合框架计算余弦相似度。
步骤详细:
1. 安装依赖:
- mongoose
- @tensorflow/tfjs-node (或者@tensorflow/tfjs-node-gpu,如果你有GPU)
- @tensorflow-models/universal-sentence-encoder
2. 定义Mongoose模型,包含字段:content(文本内容),embedding(数组类型,存储向量)。
3. 创建文档时,使用Universal Sentence Encoder生成嵌入向量并保存。
4. 搜索时,同样将查询文本转换为向量,然后使用聚合管道计算每个文档的余弦相似度,然后排序取前N个。
代码示例:
注意:由于嵌入向量生成是异步操作,我们需要在保存文档前异步生成向量。
首先,我们定义模式:
```javascript
const mongoose = require('mongoose');
const { Schema } = mongoose;
const knowledgeSchema = new Schema({
content: { type: String, required: true },
embedding: { type: [Number], required: true }
});
const Knowledge = mongoose.model('Knowledge', knowledgeSchema);
```
然后,我们初始化Universal Sentence Encoder模型:
```javascript
const tf = require('@tensorflow/tfjs-node');
const use = require('@tensorflow-models/universal-sentence-encoder');
let model;
async function loadModel() {
model = await use.load();
console.log('Model loaded');
}
loadModel();
```
接下来,我们创建添加文档的函数:
```javascript
async function addKnowledge(content) {
// 生成嵌入向量
const embedding = await generateEmbedding(content);
const knowledge = new Knowledge({ content, embedding });
await knowledge.save();
return knowledge;
}
async function generateEmbedding(text) {
// 使用模型生成嵌入向量
const embedding = await model.embed(text);
// 获取嵌入向量的值(一维数组)
const vector = await embedding.array();
// 注意:embedding.array()返回一个二维数组(因为可以批量处理),这里我们只有一个字符串,所以取第一个元素
return vector[0];
}
```
最后,实现搜索函数:
```javascript
async function searchKnowledge(query, limit = 5) {
// 生成查询文本的嵌入向量
const queryEmbedding = await generateEmbedding(query);
// 使用聚合管道计算每个文档的余弦相似度
const results = await Knowledge.aggregate([
{
$addFields: {
// 计算点积
dotProduct: {
$reduce: {
input: { $zip: { inputs: ["$embedding", queryEmbedding] } },
initialValue: 0,
in: {
$add: [
"$$value",
{ $multiply: [ { $arrayElemAt: ["$$this", 0] }, { $arrayElemAt: ["$$this", 1] } ] }
]
}
}
},
// 计算文档向量的模
magnitudeA: { $sqrt: { $sum: { $map: { input: "$embedding", as: "a", in: { $multiply: ["$$a", "$$a"] } } } } },
// 计算查询向量的模(这里可以提前计算一次,避免重复计算,但聚合管道中无法使用变量,所以每次计算)
}
},
{
$addFields: {
magnitudeB: { $sqrt: { $sum: { $map: { input: queryEmbedding, as: "b", in: { $multiply: ["$$b", "$$b"] } } } } }
}
},
{
$addFields: {
// 余弦相似度 = 点积 / (magnitudeA * magnitudeB)
similarity: {
$divide: [
"$dotProduct",
{ $multiply: ["$magnitudeA", "$magnitudeB"] }
]
}
}
},
{
$sort: { similarity: -1 } // 按相似度降序排序
},
{
$limit: limit
},
{
$project: {
content: 1,
similarity: 1,
_id: 1
}
}
]);
return results;
}
```
注意:上述聚合管道中计算余弦相似度的方法在数据量大时可能会很慢,因为它需要扫描整个集合并对每个文档进行向量计算。因此,这种方法只适用于小规模数据集。
对于大规模数据,我们应该使用支持向量索引的数据库,如MongoDB Atlas(它支持向量搜索索引)或者集成其他向量数据库。
另外,在计算点积时,我们使用了$zip将两个数组(文档向量和查询向量)组合成一对一对的,然后相乘并累加。计算模也是类似。
使用示例:
```javascript
async function main() {
await mongoose.connect('mongodb://localhost:27017/knowledge_db');
// 添加一些文档(确保模型已加载)
// await addKnowledge("JavaScript is a programming language");
// await addKnowledge("Python is an interpreted, high-level programming language");
const results = await searchKnowledge("What is JavaScript?");
console.log(results);
}
main();
```
注意:Universal Sentence Encoder生成的向量是512维的,因此存储和计算都会有一定的开销。
为了提高性能,我们可以考虑以下优化:
1. 在添加文档时,预计算并存储每个向量的模(magnitudeA),这样在搜索时就只需要计算查询向量的模和点积。
2. 使用近似最近邻(ANN)算法,但MongoDB本身不支持,所以可能需要借助其他工具。
另外,我们也可以考虑使用其他轻量级的嵌入模型来减少向量维度和计算量。
总结:这种方法适用于小型知识库,对于大规模数据,建议使用专门的向量数据库。
最后,注意关闭TensorFlow.js的日志输出(避免过于冗长):
```javascript
tf.enableProdMode();
```
完整代码示例:</think># Node.js + Mongoose 接入 AI 本地知识库搜索解决方案
下面我将详细介绍如何使用 Node.js 和 Mongoose 构建一个本地 AI 知识库搜索系统,支持向量嵌入和语义搜索功能。
## 系统架构
```
前端应用
│
└── HTTP API (Express)
│
├── 知识库管理模块 (Mongoose)
│ ├── 知识文档存储
│ └── 向量嵌入存储
│
└── AI 搜索模块
├── 文本嵌入模型 (TensorFlow.js)
└── 向量相似度计算
```
## 完整实现代码
### 1. 安装所需依赖
```bash
npm install mongoose express @tensorflow/tfjs-node @tensorflow-models/universal-sentence-encoder cors body-parser
```
### 2. MongoDB 连接和模式定义
```javascript:db.js
const mongoose = require('mongoose');
// 连接 MongoDB
const connectDB = async () => {
try {
await mongoose.connect('mongodb://localhost:27017/knowledge_db', {
useNewUrlParser: true,
useUnifiedTopology: true,
});
console.log('MongoDB connected successfully');
} catch (err) {
console.error('MongoDB connection error:', err);
process.exit(1);
}
};
// 知识文档模式
const knowledgeSchema = new mongoose.Schema({
title: { type: String, required: true },
content: { type: String, required: true },
category: { type: String, enum: ['技术', '产品', '市场', '其他'], default: '其他' },
createdAt: { type: Date, default: Date.now },
updatedAt: { type: Date, default: Date.now },
// 向量嵌入字段
embedding: { type: [Number], required: true, select: false },
});
// 添加文本索引
knowledgeSchema.index({ title: 'text', content: 'text' });
// 添加向量索引(需要 MongoDB 5.0+)
knowledgeSchema.index({ embedding: 'cosmosSearch' });
const Knowledge = mongoose.model('Knowledge', knowledgeSchema);
module.exports = { connectDB, Knowledge };
```
### 3. AI 嵌入服务
```javascript:embeddingService.js
const tf = require('@tensorflow/tfjs-node');
const use = require('@tensorflow-models/universal-sentence-encoder');
// 加载模型(单例)
let model;
async function loadModel() {
if (!model) {
console.log('Loading Universal Sentence Encoder model...');
model = await use.load();
console.log('Model loaded successfully');
}
return model;
}
// 生成文本嵌入向量
async function generateEmbedding(text) {
try {
const model = await loadModel();
const embeddings = await model.embed([text]);
const vector = await embeddings.array();
return vector[0]; // 返回一维数组
} catch (error) {
console.error('Embedding generation error:', error);
throw error;
}
}
// 计算余弦相似度
function cosineSimilarity(vecA, vecB) {
if (vecA.length !== vecB.length) {
throw new Error('Vectors must have the same length');
}
const dotProduct = vecA.reduce((sum, a, i) => sum + a * vecB[i], 0);
const magnitudeA = Math.sqrt(vecA.reduce((sum, a) => sum + a * a, 0));
const magnitudeB = Math.sqrt(vecB.reduce((sum, b) => sum + b * b, 0));
return magnitudeA && magnitudeB
? dotProduct / (magnitudeA * magnitudeB)
: 0;
}
module.exports = { generateEmbedding, cosineSimilarity };
```
### 4. Express API 服务
```javascript:server.js
const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');
const { connectDB, Knowledge } = require('./db');
const { generateEmbedding, cosineSimilarity } = require('./embeddingService');
const app = express();
const PORT = process.env.PORT || 5000;
// 中间件
app.use(cors());
app.use(bodyParser.json({ limit: '10mb' }));
// 连接数据库
connectDB();
// 添加知识文档
app.post('/api/knowledge', async (req, res) => {
try {
const { title, content, category } = req.body;
// 生成嵌入向量
const embedding = await generateEmbedding(`${title} ${content}`);
// 创建新文档
const newKnowledge = new Knowledge({
title,
content,
category,
embedding
});
await newKnowledge.save();
res.status(201).json({
success: true,
data: newKnowledge
});
} catch (error) {
console.error('Error adding knowledge:', error);
res.status(500).json({
success: false,
message: 'Failed to add knowledge',
error: error.message
});
}
});
// 语义搜索(使用向量相似度)
app.post('/api/search/semantic', async (req, res) => {
try {
const { query } = req.body;
// 生成查询向量
const queryVector = await generateEmbedding(query);
// 获取所有知识文档(实际应用中应分页)
const allKnowledge = await Knowledge.find({}).lean();
// 计算相似度并排序
const results = allKnowledge.map(doc => {
const similarity = cosineSimilarity(queryVector, doc.embedding);
return {
...doc,
similarity,
embedding: undefined // 不返回向量数据
};
}).sort((a, b) => b.similarity - a.similarity);
res.json({
success: true,
results
});
} catch (error) {
console.error('Semantic search error:', error);
res.status(500).json({
success: false,
message: 'Semantic search failed',
error: error.message
});
}
});
// 混合搜索(文本 + 语义)
app.post('/api/search/hybrid', async (req, res) => {
try {
const { query, category } = req.body;
// 文本搜索(使用 MongoDB 全文索引)
const textResults = await Knowledge.find(
{ $text: { $search: query }, ...(category && { category }) },
{ score: { $meta: "textScore" } }
).sort({ score: { $meta: "textScore" } }).lean();
// 语义搜索
const queryVector = await generateEmbedding(query);
const allKnowledge = await Knowledge.find(
category ? { category } : {}
).lean();
const semanticResults = allKnowledge.map(doc => {
const similarity = cosineSimilarity(queryVector, doc.embedding);
return {
...doc,
similarity
};
}).sort((a, b) => b.similarity - a.similarity);
// 合并结果(加权评分)
const combinedResults = mergeSearchResults(textResults, semanticResults);
res.json({
success: true,
results: combinedResults
});
} catch (error) {
console.error('Hybrid search error:', error);
res.status(500).json({
success: false,
message: 'Hybrid search failed',
error: error.message
});
}
});
// 合并搜索结果的辅助函数
function mergeSearchResults(textResults, semanticResults) {
// 创建文档ID到分数的映射
const scoreMap = new Map();
// 文本搜索结果评分(使用倒序位置加权)
textResults.forEach((doc, index) => {
const positionScore = 1 - (index / textResults.length);
scoreMap.set(doc._id.toString(), positionScore * 0.6); // 60%权重
});
// 语义搜索结果评分
semanticResults.forEach((doc, index) => {
const positionScore = 1 - (index / semanticResults.length);
const currentScore = scoreMap.get(doc._id.toString()) || 0;
scoreMap.set(doc._id.toString(), currentScore + (positionScore * 0.4)); // 40%权重
});
// 创建合并结果集
const allDocs = [...textResults, ...semanticResults];
const uniqueDocs = Array.from(new Map(allDocs.map(doc => [doc._id.toString(), doc])).values());
// 添加合并分数并排序
return uniqueDocs.map(doc => {
const combinedScore = scoreMap.get(doc._id.toString()) || 0;
return {
...doc,
combinedScore,
embedding: undefined // 不返回向量数据
};
}).sort((a, b) => b.combinedScore - a.combinedScore);
}
// 启动服务器
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
```
### 5. 测试脚本
```javascript:testScript.js
const axios = require('axios');
const { generateEmbedding } = require('./embeddingService');
const API_BASE = 'http://localhost:5000/api';
// 添加测试数据
async function addSampleData() {
const knowledgeItems = [
{
title: "JavaScript 闭包",
content: "闭包是函数和声明该函数的词法环境的组合。闭包允许函数访问其外部作用域中的变量,即使外部函数已经执行完毕。",
category: "技术"
},
{
title: "React Hooks 原理",
content: "React Hooks 是 React 16.8 引入的特性,它允许你在函数组件中使用状态和其他 React 特性。Hooks 基于闭包实现,通过调用顺序来跟踪状态。",
category: "技术"
},
{
title: "产品市场匹配",
content: "产品市场匹配(Product-Market Fit)是指产品能够满足市场需求的程度。当用户主动推荐你的产品时,通常表明达到了产品市场匹配。",
category: "产品"
},
{
title: "敏捷开发原则",
content: "敏捷开发强调个体和互动高于流程和工具,可工作的软件高于详尽的文档,客户合作高于合同谈判,响应变化高于遵循计划。",
category: "产品"
}
];
for (const item of knowledgeItems) {
try {
const response = await axios.post(`${API_BASE}/knowledge`, item);
console.log(`Added: ${item.title}`);
} catch (error) {
console.error(`Failed to add ${item.title}:`, error.response?.data || error.message);
}
}
}
// 执行语义搜索测试
async function testSemanticSearch() {
const queries = [
"函数和作用域的关系",
"React 的状态管理",
"如何确定产品是否满足市场",
"软件开发方法论"
];
for (const query of queries) {
try {
console.log(`\nSearching for: "${query}"`);
const response = await axios.post(`${API_BASE}/search/semantic`, { query });
console.log(`Top results (semantic):`);
response.data.results.slice(0, 3).forEach((result, index) => {
console.log(`${index + 1}. ${result.title} (相似度: ${result.similarity.toFixed(4)})`);
});
} catch (error) {
console.error(`Search failed for "${query}":`, error.response?.data || error.message);
}
}
}
// 执行混合搜索测试
async function testHybridSearch() {
const queries = [
{ query: "React 状态", category: "技术" },
{ query: "产品开发方法", category: "产品" }
];
for (const { query, category } of queries) {
try {
console.log(`\nHybrid search for: "${query}" in category ${category}`);
const response = await axios.post(`${API_BASE}/search/hybrid`, { query, category });
console.log(`Top results (hybrid):`);
response.data.results.slice(0, 3).forEach((result, index) => {
console.log(`${index + 1}. ${result.title} (综合评分: ${result.combinedScore.toFixed(4)})`);
});
} catch (error) {
console.error(`Hybrid search failed for "${query}":`, error.response?.data || error.message);
}
}
}
// 运行测试
async function runTests() {
await addSampleData();
await new Promise(resolve => setTimeout(resolve, 2000)); // 等待数据插入完成
await testSemanticSearch();
await testHybridSearch();
}
runTests();
```
## 关键组件解释
### 1. 向量嵌入技术
- 使用 TensorFlow.js 的 Universal Sentence Encoder 模型生成文本嵌入向量
- 将文本转换为 512 维向量,捕获语义信息
- 嵌入向量存储在 MongoDB 中,支持高效检索
### 2. 搜索算法
- **语义搜索**:计算查询向量与知识库向量的余弦相似度
- **混合搜索**:结合文本搜索(关键词匹配)和语义搜索(向量相似度)
- **加权评分**:对两种搜索结果进行加权合并,提供更全面的结果
### 3. MongoDB 优化
- **文本索引**:加速关键词搜索
- **向量存储**:使用数组类型存储嵌入向量
- **选择性投影**:避免返回大型向量数据
### 4. 性能优化策略
1. **模型预热**:在服务启动时加载嵌入模型,避免首次请求延迟
2. **批量处理**:实际应用中应批量处理嵌入生成
3. **向量索引**:MongoDB 5.0+ 支持向量索引,加速相似度搜索
4. **结果缓存**:对常见查询结果进行缓存
## 部署与扩展建议
### 部署方案
```bash
# 1. 启动 MongoDB 服务
mongod --dbpath /path/to/data
# 2. 启动 Node.js 服务
node server.js
# 3. 运行测试脚本
node testScript.js
```
### 扩展策略
1. **水平扩展**:
- 使用 PM2 集群模式启动多个 Node.js 实例
- 添加 MongoDB 分片集群处理大规模数据
2. **性能优化**:
```javascript
// 使用更高效的相似度计算
function optimizedCosineSimilarity(vecA, vecB) {
let dot = 0, magA = 0, magB = 0;
for (let i = 0; i < vecA.length; i++) {
dot += vecA[i] * vecB[i];
magA += vecA[i] * vecA[i];
magB += vecB[i] * vecB[i];
}
return dot / (Math.sqrt(magA) * Math.sqrt(magB));
}
```
3. **替代向量数据库**:
- 对于超大规模知识库,考虑使用专用向量数据库:
```mermaid
graph LR
A[Node.js应用] --> B[Redis]
A --> C[Pinecone]
A --> D[Weaviate]
```
4. **模型优化**:
- 使用量化模型减少内存占用
- 探索更高效的嵌入模型(如 MiniLM)
## 常见问题解决方案
### 问题1:嵌入生成速度慢
**解决方案**:
```javascript
// 批量生成嵌入向量
async function generateBatchEmbeddings(texts) {
const model = await loadModel();
const embeddings = await model.embed(texts);
return await embeddings.array();
}
// 在添加文档时使用
const texts = documents.map(doc => `${doc.title} ${doc.content}`);
const embeddings = await generateBatchEmbeddings(texts);
```
### 问题2:大规模数据相似度计算慢
**解决方案**:
```javascript
// 使用近似最近邻算法 (ANN)
const hnsw = require('hnswlib-node');
// 创建 ANN 索引
const index = new hnsw.HierarchicalNSW('cosine', 512);
index.initIndex(maxElements);
// 添加向量到索引
documents.forEach((doc, i) => {
index.addPoint(doc.embedding, i);
});
// 搜索最近邻
const result = index.searchKnn(queryVector, k);
```
### 问题3:向量存储占用空间大
**解决方案**:
```javascript
// 使用向量量化减少存储
function quantizeVector(vector, bits = 8) {
const min = Math.min(...vector);
const max = Math.max(...vector);
const range = max - min;
const step = range / (Math.pow(2, bits) - 1);
return vector.map(value => {
return Math.round((value - min) / step);
});
}
// 存储量化向量
knowledgeSchema.add({
quantizedEmbedding: { type: Buffer } // 存储为二进制
});
```
## 总结
本解决方案实现了:
1. 使用 Node.js + Mongoose 构建知识库管理系统
2. 集成 TensorFlow.js 实现本地文本嵌入
3. 实现语义搜索和混合搜索功能
4. 提供完整的 API 接口和测试脚本
系统特点:
- **完全本地化**:无需外部 API,保护数据隐私
- **灵活扩展**:支持从中小型到大型知识库
- **混合搜索**:结合关键词和语义搜索优势
- **开源技术栈**:基于开源技术,无授权费用