本文共 17690 字,大约阅读时间需要 58 分钟。
---------------------------------------------------------------------------------------------------------------
我提倡“少理论,少模型”,“多实践”,“多应用”的学习态度,今天带来的是关于站内搜索的基础使用,目前检索这个领域有非常多优秀的框架了,但是身为一个全文检索领域的经典祖先,我们还是需要了解和掌握其精华的。本文主要内容有Lucene简介、索引库的建立、关键字搜索、检索分页、网站排名优化、分词、搜索结果高亮等。要求掌握其基本开发流程,并可以使用servlet+easyUI+lucene+jsp+js等技术做一个简易的站内搜索的功能模块。
Lucene是apache软件基金会发布的一个开放源代码的全文检索引擎工具包,由资深全文检索专家Doug Cutting所撰写,它是一个全文检索引擎的架构,提供了完整的创建索引和查询索引,以及部分文本分析的引擎,Lucene的目的是为软件开发人员提供一个简单易用的工具包,以方便在目标系统中实现全文检索的功能,或者是以此为基础建立起完整的全文检索引擎,Lucene在全文检索领域是一个经典的祖先,现在很多检索引擎都是在其基础上创建的,思想是相通的。
即:Lucene是根据关健字来搜索的文本搜索工具,只能在某个网站内部搜索文本内容,不能跨网站搜索。
Lucene只能进行站内搜索。需要有索引库。
Lucene中存的就是一系列的二进制压缩文件和一些控制文件,它们位于计算机的硬盘上,
这些内容统称为索引库,索引库有二部份组成:
(1)原始记录
存入到索引库中的原始文本,例如:传智是一家IT培训机构
(2)词汇表
按照一定的拆分策略(即分词器)将原始记录中的每个字符拆开后,存入一个供将来搜索的表
(1)SQL只能针对数据库表存储,不能直接针对硬盘上的文本搜索
(2)SQL没有相关度排名
(3)SQL搜索结果没有关健字高亮显示
(4)SQL需要数据库的支持,数据库本身需要内存开销较大,例如:Oracle
(5)SQL搜索有时较慢,尤其是数据库不在本地时,超慢,例如:Oracle
创建索引库:
1) 创建JavaBean对象
2) 创建Docment对象
3) 将JavaBean对象所有的属性值,均放到Document对象中去,属性名可以和JavaBean相同或不同
4) 创建IndexWriter对象
5) 将Document对象通过IndexWriter对象写入索引库中
6) 关闭IndexWriter对象
开发步骤:
1、新建一个javaweb工程,
private Integer id; //索引号 private String title; //标题 private String content; //内容get ,set方法,还有toString一下,有参和无参构造函数都要加上。
2、在demo1中邪一个方法,createIndexDB()
public void createIndexDB() throws IOException{ //创建article对象 Article at=new Article(1, "lucene","Lucene是apache软件基金会发布的一个开放源代码的全文检索引擎工具包"); //创建Document对象 Document doc=new Document(); //将article对象中的3个属性分别绑定到document对象中 /* * * 参数1:document对象中的属性名叫xid,article对象中的属性名叫id * 参数2:document对象中的xid的值,与article对象中相同 * 参数3:是否将xid属性值存入词汇表,YES表示会存入,NO表示不会存入 * 参数4:是否将xid属性值进行分词,ANALYZED表示会进行词汇拆分 ,NOT_ANALYZED不会进行拆分 * * 项目中提倡非id值进行词汇拆分,提倡非id值存入词汇表 */ doc.add(new Field("xid",at.getId().toString(),Store.YES,Index.ANALYZED)); doc.add(new Field("xtitle",at.getTitle().toString(),Store.YES,Index.ANALYZED)); doc.add(new Field("xcontent",at.getContent().toString(),Store.YES,Index.ANALYZED)); //将document对象写入lucene索引库 /* * 参数1:lucene索引库最终对应于硬盘中的目录 * 参数2:将文本拆分的策略,一个策略就是一个具体的实现类 * 参数3:最多将文本拆分的词汇,LIMITED表示1万个,只取前1万个 * */ Directory d=FSDirectory.open(new File("D:/IndexDB")); Analyzer a=new StandardAnalyzer(Version.LUCENE_30); MaxFieldLength mfl=MaxFieldLength.LIMITED; IndexWriter iw=new IndexWriter(d, a, mfl); iw.addDocument(doc); //关闭字符流对象 iw.close(); }
我们发现以上方法创建索引比较麻烦,所以我们可以封装一下,新建一个工具类LuceneUtil.java,在这个类中把以下四个内容进行封装,各自取get,set方法。
private static Directory d; //索引库存放的目录 private static Version version; //版本号 private static Analyzer analyzer; //词汇拆分的策略 private static MaxFieldLength mfl; //最大分词数用静态块来防止外界new该帮助类;
//防止外界new该帮助类 static{ try { d=FSDirectory.open(new File("D:/IndexDB")); version=Version.LUCENE_30; analyzer=new StandardAnalyzer(version); mfl=MaxFieldLength.LIMITED; } catch (IOException e) { e.printStackTrace(); throw new RuntimeException(e); } }将需要的对象进行封装:
//将javabean转成document对象 public static Document javabean2Document(Object obj) throws Exception{ Document document=new Document(); //获取obj引用的对象字节码 Class clazz=obj.getClass(); java.lang.reflect.Field[] reflectFields=clazz.getDeclaredFields(); //迭代 for(java.lang.reflect.Field reflectField:reflectFields){ //反射 reflectField.setAccessible(true); //获取属性名, String name=reflectField.getName(); String methodName="get"+name.substring(0,1).toUpperCase()+name.substring(1); //获取方法 Method method=clazz.getMethod(methodName, null); //执行方法 String value=method.invoke(obj, null).toString(); //加入到document对象中去 document.add(new Field(name,value,Store.YES,Index.ANALYZED)); } //返回document对象 return document; } //将document转成avabean对象 public static Object document2Javabean(Document document, Class clazz) throws InstantiationException, IllegalAccessException, InvocationTargetException{ Object obj=clazz.newInstance(); java.lang.reflect.Field[] reflectFields = clazz.getDeclaredFields(); for(java.lang.reflect.Field field : reflectFields){ field.setAccessible(true); String name= field.getName(); String value = document.get(name); BeanUtils.setProperty(obj,name,value); } return obj; }
以上步骤都完成后,就可以直接用工具类来对索引库进行操作了
创建单个索引库
@Test //创建索引库 public void createIndexDB() throws Exception{ Article at=new Article(1, "lucene","Lucene是apache软件基金会发布的一个开放源代码的全文检索引擎工具包"); //创建Document对象 Document doc=LuceneUtil.javabean2Document(at); IndexWriter iw=new IndexWriter(LuceneUtil.getD(), LuceneUtil.getAnalyzer(), LuceneUtil.getMfl()) ; iw.addDocument(doc); iw.close(); }创建多个索引对象
@Test public void addAll() throws Exception{ IndexWriter iw=new IndexWriter(LuceneUtil.getD(), LuceneUtil.getAnalyzer(), LuceneUtil.getMfl()); Article at1=new Article(1, "lucene","Lucene是apache软件基金会发布的一个开放源代码的全文检索引擎工具包"); Document document1=LuceneUtil.javabean2Document(at1); iw.addDocument(document1); Article at2=new Article(2, "lucene","Lucene是根据关健字来搜索的文本搜索工具"); Document document2=LuceneUtil.javabean2Document(at2); iw.addDocument(document2); Article at3=new Article(3, "lucene","Lucene在全文检索领域是一个经典的祖先,现在很多检索引擎都是在其基础上创建的,思想是相通的"); Document document3=LuceneUtil.javabean2Document(at3); iw.addDocument(document3); Article at4=new Article(4, "lucene","Lucene它是一个全文检索引擎的架构,提供了完整的创建索引和查询索引"); Document document4=LuceneUtil.javabean2Document(at4); iw.addDocument(document4); iw.close(); }修改索引、
@Test public void update() throws Exception{ Article newArticle=new Article(4, "lucene","4是一个全文检索引擎的架构,提供了完整的创建索引和查询索引"); Document document=LuceneUtil.javabean2Document(newArticle); IndexWriter iw=new IndexWriter(LuceneUtil.getD(), LuceneUtil.getAnalyzer(), LuceneUtil.getMfl()); iw.updateDocument(new Term("id","4"), document); iw.close(); }查询:
@Test public void findAll() throws Exception{ String keywords="是"; List删除atl=new ArrayList (); QueryParser queryParser=new QueryParser(LuceneUtil.getVersion(),"content",LuceneUtil.getAnalyzer()); Query query=queryParser.parse(keywords); IndexSearcher is=new IndexSearcher(LuceneUtil.getD()); TopDocs td=is.search(query,100); for(int i=0;i
@Test public void delete() throws LockObtainFailedException, IOException{ IndexWriter iw=new IndexWriter(LuceneUtil.getD(), LuceneUtil.getAnalyzer(), LuceneUtil.getMfl()); iw.deleteDocuments(new Term("id","2")); iw.close(); } public void deleteAll() throws CorruptIndexException, LockObtainFailedException, IOException{ IndexWriter indexWriter = new IndexWriter(LuceneUtil.getD(),LuceneUtil.getAnalyzer(),LuceneUtil.getMfl()); indexWriter.deleteAll(); indexWriter.close(); }
根据关键字查询索引库中的内容:
1) 创建IndexSearcher对象
2) 创建QueryParser对象
3) 创建Query对象来封装关键字
4) 用IndexSearcher对象去索引库中查询符合条件的前100条记录,不足100条记录的以实际为准
5) 获取符合条件的编号
6) 用indexSearcher对象去索引库中查询编号对应的Document对象
7) 将Document对象中的所有属性取出,再封装回JavaBean对象中去,并加入到集合中保存,以备将之用
/** * 根据关键字从索引库中搜索符合条件的记录 * * @throws IOException * @throws ParseException * */ @Test public void findIndexDB() throws IOException, ParseException{ //搜索"源"这个字 String keyword="apache"; List使用工具类:其实就是前面那一节里面的查询。atl=new ArrayList (); Directory d=FSDirectory.open(new File("D:/IndexDB")); Analyzer a=new StandardAnalyzer(Version.LUCENE_30); MaxFieldLength mfl=MaxFieldLength.LIMITED; //创建IndexSearch字符流对象 IndexSearcher is=new IndexSearcher(d); /* * 参数1:使用分词器的版本,qitqt * 参数2:针对document对象中的哪个属性进行搜索 */ Version version=Version.LUCENE_30; Analyzer analyzer=new StandardAnalyzer(version); QueryParser queryParser=new QueryParser(version,"xcontent",analyzer); //封装查询关键字 Query query=queryParser.parse(keyword); /* * 参数1:查询对象及封装关键字的对象 * 参数2:MAX_RECORD表示如果根据关键字搜索出来的内容教多,只取前MAX_RECORD个 * */ int MAX_RECORD=100; TopDocs td=is.search(query,MAX_RECORD); //迭代词汇表中符合条件的编号 for(int i=0;i
//根据关键字查询 @Test public void findIndexDB() throws Exception { String keywords="是"; Listatl=new ArrayList (); QueryParser queryParser=new QueryParser(LuceneUtil.getVersion(),"content",LuceneUtil.getAnalyzer()); Query query=queryParser.parse(keywords); IndexSearcher is=new IndexSearcher(LuceneUtil.getD()); TopDocs td=is.search(query,100); for(int i=0;i
单字段:
QueryParser queryParser = new QueryParser(LuceneUtil.getVersion(),"content",LuceneUtil.getAnalyzer());
多字段
QueryParser queryParser = new MultiF
//格式对象 Formatter formatter = new SimpleHTMLFormatter("",""); //关键字对象 Scorer scorer = new QueryScorer(query); //高亮对象 Highlighter highlighter = new Highlighter(formatter,scorer);
采用一种算法,将中英文本中的字符拆分开来,形成词汇,以待用户输入关健字后搜索
使用分词器的原因
因为用户输入的搜索的内容是一段文本中的一个关健字,和原始表中的内容有差别, 但作为搜索引擎来讲,又得将相关的内容搜索出来,此时就得采用分词器来最大限度 匹配原始表中的内容
分词器工作流程
步一:按分词器拆分出词汇 步二:去除停用词和禁用词 步三:如果有英文,把英文字母转为小写,即搜索不分大小写
//测试Lucene内置和第三方分词器的分词效果public class TestAnalyzer { private static void testAnalyzer(Analyzer analyzer, String text) throws Exception { System.out.println("当前使用的分词器:" + analyzer.getClass()); TokenStream tokenStream = analyzer.tokenStream("content",new StringReader(text)); tokenStream.addAttribute(TermAttribute.class); while (tokenStream.incrementToken()) { TermAttribute termAttribute = tokenStream.getAttribute(TermAttribute.class); System.out.println(termAttribute.term()); } } public static void main(String[] args) throws Exception { testAnalyzer(new StandardAnalyzer(LuceneUtil.getVersion()), "采用一种算法aa,将中英文本中的字符拆分开来,形成词汇,以待用户输入关健字后搜索"); testAnalyzer(new FrenchAnalyzer(LuceneUtil.getVersion()), "采用一种算法aa,将中英文本中的字符拆分开来,形成词汇,以待用户输入关健字后搜索"); testAnalyzer(new CJKAnalyzer(LuceneUtil.getVersion()), "采用一种算法aa,将中英文本中的字符拆分开来,形成词汇,以待用户输入关健字后搜索"); testAnalyzer(new IKAnalyzer(), "指令汇科技实业呀"); } }
testAnalyzer(new IKAnalyzer(), "指令汇科技实业呀");
导入IKAnalyzer分词器核心jar包,IKAnalyzer3.2.0Stable.jar
将IKAnalyzer.cfg.xml和stopword.dic和mydict.dic文件复制到MyEclipse的src目录下, 再进行配置,在配置时,首行需要一个空行。
mydict.dic内容如下:首行空出来: 指令汇科技IK Analyzer 扩展配置 /mydict.dic /surname.dic
//设置合并因子,即满足3个cfs文件合并 iw.addDocument(doc); iw.setMergeFactor(3);
索引库 内存索引库 硬盘索引库
//合并.cfs文件,解决文件的大小和数量问题 @Test public void type1() throws Exception{ Article at=new Article(1, "lucene","Lucene是apache软件基金会发布的一个开放源代码的全文检索引擎工具包",10); //创建Document对象 Document doc=LuceneUtil.javabean2Document(at); IndexWriter iw=new IndexWriter(LuceneUtil.getD(),LuceneUtil.getAnalyzer(),LuceneUtil.getMfl()); iw.optimize(); iw.close(); } //合并.cfs文件,解决文件的大小和数量问题 @Test public void type2() throws Exception{ Article at=new Article(2, "lucene","Lucene是apache软件基金会发布的一个开放源代码的全文检索引擎工具包",10); //创建Document对象 Document doc=LuceneUtil.javabean2Document(at); IndexWriter iw=new IndexWriter(LuceneUtil.getD(),LuceneUtil.getAnalyzer(),LuceneUtil.getMfl()); //设置合并因子,即满足3个cfs文件合并 iw.addDocument(doc); iw.setMergeFactor(3); iw.close(); }*/ //合并.cfs文件,解决文件的大小和数量问题 @Test public void type4() throws Exception{ Article at=new Article(2, "lucene","Lucene是apache软件基金会发布的一个开放源代码的全文检索引擎工具包",10); Document doc=LuceneUtil.javabean2Document(at); //硬盘索引库 Directory fsDirectory=FSDirectory.open(new File("D:/IndexDB")); //内存索引库 Directory ramDirectory=new RAMDirectory(fsDirectory); //指向硬盘索引库的字符流,true表示如果内存索引库中和硬盘索引库中有系统的document对象时,先删除硬盘索引库的值 IndexWriter fsIw=new IndexWriter(fsDirectory,LuceneUtil.getAnalyzer(),true,LuceneUtil.getMfl()); //指向内存索引库的字符流 IndexWriter ramIw=new IndexWriter(ramDirectory,LuceneUtil.getAnalyzer(),LuceneUtil.getMfl()); //将document对象写入内存索引库 ramIw.addDocument(doc); ramIw.close(); //将内存索引库中所有的document对象同步到硬盘索引中 fsIw.addIndexesNoOptimize(ramDirectory); fsIw.close(); }
Lucene中的显示结果次序与相关度得分有关 ScoreDoc.score; 默认情况下,Lucene是按相关度得分排序的,得分高排在前,得分低排在后 如果相关度得分相同,按插入索引库的先后次序排序。
//增加document对象索引库中 @Test public void add() throws Exception{ //Article at=new Article(1, "lucene","Lucene是apache软件基金会发布的一个开放源代码的全文检索引擎工具包",10); //Article at=new Article(2, "lucene2","Lucene2是apache软件基金会发布的一个开放源代码的全文检索引擎工具包",20); Article at=new Article(3, "新lucene3","Lucene3是apache软件基金会发布的一个开放源代码的全文检索引擎工具包",30); //Article at=new Article(4, "lucene4","Lucene4是是软件基金会发布是时的一个开放源是代码的全文是检索引擎是工具包",30); //Article at=new Article(5, "lucene5","是是是是是是是是是是是是是是是是是是是是是是是是是是是是是是是是是是是是是是",30); //创建Document对象 Document doc=LuceneUtil.javabean2Document(at); IndexWriter iw=new IndexWriter(LuceneUtil.getD(),LuceneUtil.getAnalyzer(),LuceneUtil.getMfl()); doc.setBoost(20F); iw.addDocument(doc); iw.close(); }
//获取document对象的评分 float score=sd.score;
强行提高得分 doc.setBoost(100F); 搜索结果是按某个或某些字段高低排序来显示的结果.
//创建排序的条件 /* * 参数1:id表示依据document对象中的那个字段排序,里如id * 参数2:sortField.INT表示document对象中该字段的类型, * 参数3:true表示降序,类似于order by * * */ Sort sort=new Sort(new SortField("id",SortField.INT,true)); TopDocs td=is.search(query, null,100,sort);
按多个字段排序
//创建排序的条件 /* * 参数1:id表示依据document对象中的那个字段排序,里如id * 参数2:sortField.INT表示document对象中该字段的类型, * 参数3:true表示降序,类似于order by * * */ Sort sort=new Sort( new SortField("count",SortField.INT,true), new SortField("count",SortField.INT,true )); TopDocs td=is.search(query, null,100,sort);
同之前的Artucle.java
private Integer id; //索引号 private String title; //标题 private String content; //内容get ,set方法,还有toString一下,有参和无参构造函数都要加上。
同时还要一个分页的bean,进行封装
private Integer currPageNO;//当前页号 private Integer perPageSize = 3;//每页显示记录数,默认为2条 private Integer allRecordNO;//总记录数OK private Integer allPageNO;//总页数OK private Listget ,set方法,还有toString一下,有参和无参构造函数都要加上。articleList = new ArrayList ();//内容
新建ArticleDao.java
//根据关键字,获取总记录数,返回总的记录数 public int getAllRecord(String keywords) throws Exception{ Listatl=new ArrayList (); QueryParser queryParser=new QueryParser(LuceneUtil.getVersion(),"content",LuceneUtil.getAnalyzer()); Query query=queryParser.parse(keywords); IndexSearcher is=new IndexSearcher(LuceneUtil.getD()); TopDocs td=is.search(query,5); //返回符合条件的真实总记录数,不受search(query,5)中定义的5的影响 //return td.totalHits; //17条 return td.scoreDocs.length; //5条 } //根据关键字,批量查询记录 /* * start:从第几条记录的索引号开始查询,索引号从0开始 * size:最多查询几条记录,不满足最多数目时,以实际为准 * 返回集合 */ public List findAll(String keywords,int start,int size) throws Exception{ List atl=new ArrayList (); QueryParser queryParser=new QueryParser(LuceneUtil.getVersion(),"content",LuceneUtil.getAnalyzer()); Query query=queryParser.parse(keywords); IndexSearcher is=new IndexSearcher(LuceneUtil.getD()); TopDocs td=is.search(query,100); int middle=Math.min(start+size,td.totalHits); for(int i=start;i
新建一个ArticleService.java ,对分页进行处理
private ArticleDao articleDao=new ArticleDao(); //根据关键字和页号,查询内容 public PageBean show(String keywords,int currPageNO) throws Exception{ PageBean page=new PageBean(); //封装当前页号 page.setCurrPageNO(currPageNO); //封装总记录数 int allRecordNO=articleDao.getAllRecord(keywords); page.setAllRecordNO(allRecordNO); //封装总页数 int allPageNO=0; if(page.getAllRecordNO() % page.getPerPageSize() ==0){ allPageNO=page.getAllRecordNO() / page.getPerPageSize(); }else{ allPageNO=page.getAllRecordNO() /page.getPerPageSize()+1; } page.setAllPageNO(allPageNO); //封装内容 int size=page.getPerPageSize(); int start=(page.getCurrPageNO()-1)*size; Listart=articleDao.findAll(keywords, start, size) ; page.setArticleList(art); return page; }
使用一个servlet来对数据进行控制。doGet和doPost方法中进行处理。
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { this.doPost(request, response); } public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); try { //获取关键字 String keywords=request.getParameter("keywords"); if(keywords==null || keywords.trim().length()==0){ keywords="是"; } //获取当前页号 String temp=request.getParameter("page"); if(temp==null || temp.trim().length()==0){ temp="1"; } //调用业务层 ArticleService articleService=new ArticleService(); PageBean page=articleService.show(keywords,Integer.parseInt(temp)); //构造map对象 Mapmap=new LinkedHashMap (); map.put("total", page.getAllPageNO()); map.put("rows", page.getArticleList()); //将map转成json JSONArray jsonArray=JSONArray.fromObject(map); String jsonJAVA=jsonArray.toString(); //去掉两边的[]符号 jsonJAVA=jsonJAVA.substring(1,jsonJAVA.length()-1); //以流的方式将json文本输出到DateGrid组件中 response.setContentType("text/html;charset=UTF-8"); PrintWriter pw=response.getWriter(); pw.write(jsonJAVA); pw.flush(); pw.close(); } catch (Exception e) { e.printStackTrace(); throw new RuntimeException(e); } }
我们使用一个jsp来书写界面。先把我们需要的js包,jquery,easyui相关的jar导入到项目的lib目录中,然后在这个jsp文件中引入我们所需要的文件。命名为list.jsp.
在这里就暂时假设你已经学会使用easyui了,如果对使用easyui还有什么问题的,欢迎查看我的下一篇文章。
界面效果如下。
源码下载地址: (基础部分,前两章节)
(应用部分,后四章节的内容)
小结:这里对于lucene的使用都是比较基础的部分,重要的要是学会基本的编码流程,开发思想,学以致用,做一个有思想有深度的人,不要变成一个只会敲代码的机器。