关于begincode搜索方案,我的想法是直接放入web模块 和主站一起上线。后来老杨的意见是主站业务和搜索业务独立成两个war项目,就是两个业务之间的耦合最小。
最终方案的选用是在搜索项目中暴露一个http接口,在有问题新增的时候就发送一个http请求给搜索端,这个请求中带有一个问题的标识(在这里我们也想过直接发送文章内容,这样就减少查询数据库的步骤,考虑到文章内容可能会太多,就舍弃了这个想法),然后搜索端就从数据库中查找这个问题,加入内存索引中,通过搜索项目中的一个提交线程,间隔固定的时间提交内存索引和合并索引。这个http请求的发送我们使用httpclient进行功能的实现,最大化的解耦。
还提供了一种方案是监听数据库的数据传入 可以使用数据库的binlog日志,有数据传入的时候就提醒搜索更新索引。
发送请求的包放入core模块下,在问题添加的handler层中插入封装了发送请求的httpclient 代码,这样只要传入一个小小的问题标识就能提醒搜索来操作索引库
具体实现
首先引入httpclient的maven依赖
'''
org.apache.httpcomponents
httpclient
4.5
具体httpclient的请求发送代码这里就不再贴出来,如果有不懂得同学可以参考最底部的git地址 查看源码
在lucene端的接收
package net.begincode.controller;
import net.begincode.core.handler.ProblemHandler;
import net.begincode.core.model.Problem;
import net.begincode.core.support.AuthPassport;
import net.begincode.utils.LuceneUtil;
import net.begincode.utils.PatternUtil;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
/**
* Created by Stay on 2016/10/7 15:45.
*/
@Controller
@RequestMapping(value="/http")
public class HttpController {
@Resource
private ProblemHandler problemHandler;
@RequestMapping(value="/createReceive",method = RequestMethod.POST)
@ResponseBody
public Object httpReceive(Problem problem){
Map map = new HashMap();
Problem pro= problemHandler.selectById(problem.getProblemId());
pro.setContent(PatternUtil.filterIndexContent(pro.getContent()));
LuceneUtil.createIndex(pro);
return map;
}
}
本段代码就暴露了一个 http://localhost:8081/http/createReceive.htm 这个端口 主站业务只要通过post请求 把标识传给lucene搜索端即可添加索引
LuceneUtil.createIndex(pro);这段代码的具体实现
package net.begincode.utils;
import net.begincode.core.model.Problem;
import net.begincode.index.Index;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.StringField;
import org.apache.lucene.document.TextField;
/**
* Created by Stay on 2016/10/7 15:52.
*/
public class LuceneUtil {
/**
* 新建问题添加进索引
*
* @param problem
*/
public static void createIndex(Problem problem) {
Index index = new Index("/test");
Document doc = new Document();
doc.add(new StringField("id", problem.getProblemId().toString(), Field.Store.YES));
doc.add(new StringField("solve", "0", Field.Store.YES));
doc.add(new Field("title", problem.getTitle(), TextField.TYPE_STORED));
doc.add(new Field("content", problem.getContent(), TextField.TYPE_STORED));
index.addDocument(doc);
}
}
索引的创建
我们需要的索引只有标题和内容 ,搜索是根据标题和内容的一起分词来实现关键词的搜索,而内容只要呈现一部分,内容还存有html标签,这个标签的去除,使用jsoup来实现 jsoup 是一款Java 的HTML解析器,可直接解析某个URL地址、HTML文本内容。它提供了一套非常省力的API,可通过DOM,CSS以及类似于jQuery的操作方法来取出和操作数据。总之,我们是要提取具体的文字,来实现分词搜索。
索引的管理
接下来 我们说具体的内存索引 合并索引 和硬盘索引的管理
创建一个manager包来管理这些索引 里面有两个类 一个是IndexConfig另一个是IndexManager
IndexConfig 具体的设置是在ConfigBean里面说明
package net.begincode.bean;
import net.begincode.analyzer.MyIkAnalyzer;
import org.apache.lucene.analysis.Analyzer;
/**
* Created by Stay on 2016/9/28 15:52.
*/
public class ConfigBean {
private String indexName = "/test";//索引名
private String indexPath = "d:/begincodeIndex";//索引硬盘路径
private Analyzer analyzer = new MyIkAnalyzer();//索引分词器
private double indexReopenMaxStaleSec = 10;
private double indexReopenMinStaleSec = 0.025;
private int indexCommitSeconds = 30;//索引写入磁盘时间间隔
'''//省略get set方法
}
package net.begincode.manager;
import net.begincode.bean.ConfigBean;
import java.util.HashSet;
/**
* Created by Stay on 2016/9/28 17:17.
*/
public class IndexConfig {
// 系统中配置多个索引
private static HashSet configBeans;
private static class DefaultIndexConfig {
private static final HashSet configBeansDefault = new HashSet();
static {
ConfigBean bean = new ConfigBean();
configBeansDefault.add(bean);
}
}
public static HashSet getConfig() {
if (configBeans == null) {
// 如果configBeans为空,返回系统默认值
return DefaultIndexConfig.configBeansDefault;
}
return configBeans;
}
/**
* 设置系统索引配置
* @param configBeans
*/
public static void setConfig(HashSet configBeans) {
IndexConfig.configBeans = configBeans;
}
}
IndexManager类代码过多 就不放出来了 基本思路是通过这个类能够拿到最新可用的索引来查找索引库中的数据,里面开辟了内存索引重读线程 和内存索引提交线程 这两个线程来管理索引 通过configBean里面来设置提交索引的时间间隔
最后就是通过spring的定时任务 在每天问答系统流量最少的时候来重新索引数据库中的索引 矫正错误数据
package net.begincode.task;
import net.begincode.core.handler.ProblemHandler;
import net.begincode.core.model.Problem;
import net.begincode.bean.ConfigBean;
import net.begincode.index.Index;
import net.begincode.manager.IndexConfig;
import net.begincode.utils.JsoupUtil;
import net.begincode.utils.PatternUtil;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.StringField;
import org.apache.lucene.document.TextField;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
/**
* Created by Stay on 2016/10/2 18:59.
*/
@Component
public class LuceneTask {
private static Logger logger = LoggerFactory.getLogger(LuceneTask.class);
@Resource
private ProblemHandler problemHandler;
/**
* 每天凌晨一点重新索引数据库中的内容
* @throws Exception
*/
@Scheduled(cron = "0 0 1 * * *")
public void taskIndex(){
List list = problemHandler.selectAllProblem();
HashSet set = new HashSet();
ConfigBean bean = new ConfigBean();
set.add(bean);
IndexConfig.setConfig(set);
Index index = new Index(bean.getIndexName());
index.deleteAll();
for (Problem problem : list) {
Document document = new Document();
document.add(new StringField("id", problem.getProblemId().toString(), Field.Store.YES));
document.add(new StringField("solve", problem.getSolve().toString(), Field.Store.YES));
document.add(new Field("title", problem.getTitle(), TextField.TYPE_STORED));
document.add(new Field("content", problem.getContent(), TextField.TYPE_STORED));
index.addDocument(document);
}
index.commit();
logger.info("索引库重新索引"+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
}
}
总结
最初的想法是搜索不用近实时的提供搜索,就是通过定时任务来索引前一天新加入进来的数据,会有效的减轻服务器的压力。不过为了用户的体验更好,还是做成了近实时索引。 关于解耦,由于是两个独立的项目 web项目中不需要任何的配置 就能完成索引的创建.这只是基本功能的实现,要具体的完整做好搜索还是需要不断的改进!
http://git.oschina.net/yangsj/begincode_wenda