//首先我们要在Nodejs中安装 我们的分词库@node-rs/jieba,这个分词不像jieba安装时会踩非常多的雷,而且一半的机率都是安装失败,node-rs/jieba比jieba库要快20-30%;安装分词库是为了更好达到搜索的效果
这个库直接npm install @node-rs/jieba即可
代码分为三个,将标题,内容分词后插入mongodb数据库,insertContent.js
第二个为搜索代码,对标题和内容进行全文搜索search.js
第三个为页面显示代码,对搜索出来的结果进行显示
//insertContent.js
const {Jieba}=require("@node-rs/jieba");
const {dict}=require("@node-rs/jieba/dict");
const {MongoClient}=require('mongodb');//异步函数插入标题和内容
async function insertDocument(title,text)
{
//连接Mongodb数据库const client=new MongoClient('mongodb://ychj:123456@localhost:27017/?authSource=employees');try{await client.connect();const db=client.db('employees'); //要连接的数据库名const collection=db.collection("blog"); //要连接的集合名//使用jieba分词const jieba=Jieba.withDict(dict);//对标题进行分词const titleWords=jieba.cut(title);//对文本内容进行分词const contentWords=text.split('\n').map(paragraph=>jieba.cut(paragraph).join(' '));//将分词结果存储到Mongodbconst result=await collection.insertOne({"id":1,"title":titleWords.join(' '),"content":contentWords.join('\n')});//后面文本跟了一个'\n'是为了给文章分段落console.log('文档插入成功',result.insertedId);}finally{await client.close();}
}//为什么对标题进行分词,一般搜索文章是搜索标题,分词可以提高搜索率
//如果要对文本进行分词和分段落,那么段落得用\n来标识,不分段落那么出来的文章内容全是一段了
const title="nodejs使用nodejieba";
const text="高性能: Nodejieba的底层实现采用了C++,通过Node.js的插件机制与JavaScript集成,因此具有较高的性能。这使得Nodejieba在处理大规模文本数据时表现出色.\n支持多种分词模式: Nodejieba支持多种分词模式,包括精确模式、搜索引擎模式和新词识别模式。这使得它适用于不同的应用场景,可以根据需求选择合适的分词模式。\n用户自定义词典: 用户可以通过自定义词典来增加或修改分词器的词汇,以适应特定领域或特定项目的需求。这种灵活性使Nodejieba更适用于定制化的分词任务.\n";//插入数据库
insertDocument(title,text).catch(console.error);
//搜索代码
//search.js
const {MongoClient}=require('mongodb');
const {Jieba}=require("@node-rs/jieba");
const {dict}=require("@node-rs/jieba/dict");
const fs=require('fs');//搜索函数
async function searchDocuments(words){const client=new MongoClient('mongodb://ychj:123456@localhost:27017/?authSource=employees');try{await client.connect();const db=client.db('employees'); //连接数据库名const collection=db.collection('blog'); //连接集合名//搜索词const cursor=collection.find({$text:{"$search":words}});//转化为数组const docs=await cursor.toArray();//临时随机文件名const outputFile=getRandomFileName();//对大文件的数据流,为什么要用数据流,因为搜索出来的结果如果非常大,如上千条,我们不能存储在内存中,而是存在一个临时的随机文件中//避免占用或撑爆我们的内存,所以直接写入临时文件当中,然后显示 的时候再读取const writableStream=fs.createWriteStream(outputFile);//将读取到的搜索结果存储为json文件格式,用一个数组将其包含当中writableStream.write('[');let isFirst=true;docs.forEach((doc)=>{if(!isFirst){writableStream.write(',\n');}const formattitle=doc.title.replace(/\s+/g,'');//先按段落分成数据,然后再将文章内的空格去除掉,在段落末尾加上\nconst formatcontent=doc.content.split('\n').map(paragraph=>paragraph.replace(/\s+/g,'')).join('\n');const id=doc.id;//将各属性id,title,content组合成对象形式,然后再转化为json格式写入临时的json文件中const result={id:id,title:formattitle,content:formatcontent};writableStream.write(JSON.stringify(result));isFirst=false;});writableStream.write(']');writableStream.end();console.log('所有文档处理完结');}finally{await client.close();}
}//生成临时的随机json文件
function getRandomFileName(){return `output_${Math.random().toString(36).substring(2,9)}.json`;
}//对搜索的词进行分解析和分词,为什么要对搜索的词的要进行分解?因为用户搜索时都是连贯不会分词
//所以我们要对用户输入的词进行分词才能更好搜索出结果来,如果不分词可能搜索不出用户想要的结果
const text='mongodb和jieba';
const jieba=Jieba.withDict(dict);
const CutWords=jieba.cut(text);
const sreachCutWords=CutWords.join(' ');//分词后用空格进行间隔
//生成搜索结果
searchDocuments(sreachCutWords).catch(console.error);
//展示搜索结果页面
//showSearch.js
const http=require('http');
const fs=require('fs');
const readline=require('readline');const server=http.createServer((req,res)=>{if(req.url==="/show"){//这时是用一个简单的方法来获取了临时的随机文件名,生产过程中应该和搜索页面是一起的const outputFile="./output_03b5mml.json";const fileStream=fs.createReadStream(outputFile); //读取临时文件const rl=readline.createInterface({input:fileStream,crlfDelay:Infinity //不同操作系统使用不同的换行符,linux;\n,window:\r\n});res.writeHead(200,{"Content-type":'text/html; charset=utf-8'});res.write('<html><body><pre>');//按行读取所有的json文件内容let jsonData='';rl.on('line',line=>{jsonData+=line;});rl.on('close',()=>{try{const records=JSON.parse(jsonData);//解析json文件,并将其转化为对象数组//循环出数组中的每个json文件对象,并发送给http,//当然在实现中你可能是前后端分离的,这里应该是前端收到json文件并解析为对象数组,然后排序插入到前端文档中records.forEach(record=>{res.write(`<div>`);res.write(`ID:${record.id}<br>`);res.write(`标题:${record.title}<br>`);res.write(`内容:${record.content}<br>`);res.write(`</div><hr>`);})}catch(err){console.error('解析JSON数据时出错:',err);res.write('解析json数据时出错。');}res.write('</pre></body></html');res.end();})}
});
server.listen(3000,()=>{console.log('Server is running on http://localhost:3000');
})
在window打开cmd运行showSearch.js,然后在浏览器中输入http://localhost:3000/show 则会显示出搜索结果
对于在Mongodb中设置全文本索引,比如上面代码中的title,content
db.blog.createIndex({“title”:“text”,“content”:“text”});
即设置成功
注意:设置全文本索引的成本非常高,会比普通 的索引 更严重的性能问题,因为所有字符串都会被分解,分词,并保存到一个地方,
拥有全文本索引的集合写入性能都比其他的集合要差,在分片和迁移时速度都较慢,因为要重新的进行索引 ,而且吃内存
搜索出来的结果非常大时,可以采用以下优化策略来提高性能和效率:
- 使用流(Stream)处理数据
对于大文本数据,使用流可以有效减少内存占用,并提高处理速度。流可以让你按块处理数据,而不是一次性将整个数据加载到内存中。在 Node.js 中,可以使用 fs.createReadStream() 和 fs.createWriteStream() 来创建读写流。 - 减少 write() 的次数
频繁调用 write() 方法会显著降低写入速度,并增加内存占用。可以通过缓存一定量的数据,然后一次性写入,来减少 write() 的调用次数。例如,可以设置一个缓存大小,当达到该大小时再执行写入操作。 - 使用管道(Pipe)传输数据
管道(pipe())方法可以将一个流的输出直接传递给另一个流,避免了手动处理事件监听和数据传输。这不仅可以简化代码,还能提高效率。例如,可以将查询结果直接通过管道传输到另一个流中进行处理或存储。 - 逐行处理数据
如果数据是按行分隔的,可以使用 readline 模块逐行读取数据。这样可以避免一次性加载整个文件内容到内存中,从而减少内存占用。逐行处理数据还可以让你在处理每一行时进行必要的格式化或分析。 - 使用分块处理
将大文件分成更小的块进行处理,可以有效避免内存不足的问题。通过定义一个块大小并使用循环读取文件,每次只处理一个块,然后将处理结果写入到目标文件或进行其他操作。 - 选择合适的数据处理方法
在处理大规模数据时,选择合适的数据处理方法至关重要。例如,在统计换行符数量的实验中,使用 indexOf 方法比手动逐字节检查快了大约 10-20%。这表明在某些情况下,使用内置的优化方法可以显著提高性能。