如何使用 Lucene 做网站高亮搜索功能?
现在基本上所有网站都支持搜索功能,现在搜索的工具有很多,比如Solr、Elasticsearch,它们都是基于 Lucene 实现的,各有各的使用场景。Lucene 比较灵活,中小型项目中使用的比较多,我个人也比较喜欢用。
1.效果展示
我前段时间做了一个网站,搜索功能用的就是 Lucene 技术,效果还可以,支持中文高亮显示,支持标题和摘要同时检索,若能检索出,均高亮展示等功能,可以看下效果。
点击查看更清晰
可以看出,搜索 “微服务” 之后,可以将相关的资源全部检索出来,不管是标题包含还是摘要包含都可以检索出来。
这是比较精确的匹配,还有非精确的匹配也支持,比如我搜索 “Java项目实战”,看看结果如何。
点击查看更清晰
可以看出,如果不能完全精确匹配,Lucene 也可以做模糊匹配,将最接近搜索的内容给检索出来,展示在页面上。
我个人还是比较喜欢使用 Lucene 的,关于 Lucene 全文检索的原理我就不浪费篇幅介绍了,谷歌百度有一大堆原理。这篇文章主要来分享下如何使用 Lucene 做到这个功能。
2.依赖导入
使用 Lucene 有几个核心的依赖需要导入到项目中,上面展示的这个效果涉及到中文的分词,所以中文分词依赖也需要导入。
<!-- Lucence核心包 -->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-core</artifactId>
<version>5.3.1</version>
</dependency>
<!-- Lucene查询解析包 -->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-queryparser</artifactId>
<version>5.3.1</version>
</dependency>
<!--支持分词高亮 -->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-highlighter</artifactId>
<version>5.3.1</version>
</dependency>
<!--支持中文分词 -->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-analyzers-smartcn</artifactId>
<version>5.3.1</version>
</dependency>
3.建立分词索引
使用 Lucene 首先要建立索引,然后再查询。如何建立索引呢?为了更好的说明问题,我在这写一个 demo:直接对字符串内容建立索引。
因为在实际项目中,绝大部分情况是获取到一些文本字符串(比如从表中查询出来的结果),然后对该文本字符串建立索引。
索引建立的过程,先要获取 IndexWriter 对象,然后将相关的内容生成索引,索引的 Key 可以自己根据项目中的情况来自定义,value 是自己处理过的文本,或者从数据库中查询出来的文本。生成的时候,我们需要使用中文分词器。代码如下:
public class ChineseIndexer {
/**
* 存放索引的位置
*/
private Directory dir;
//准备一下用来测试的数据
//用来标识文档
private Integer ids[] = {1, 2, 3};
private String citys[] = {"上海", "南京", "青岛"};
private String descs[] = {
"上海是个繁华的城市。",
"南京是一个文化的城市南京,简称宁,是江苏省会,地处中国东部地区,
长江下游,濒江近海。全市下辖11个区,总面积6597平方公里,2013年建
成区面积752.83平方公里,常住人口818.78万,其中城镇人口659.1万人。
[1-4] “江南佳丽地,金陵帝王州”,南京拥有着6000多年文明史、近2600
年建城史和近500年的建都史,是中国四大古都之一,有“六朝古都”、
“十朝都会”之称,是中华文明的重要发祥地,历史上曾数次庇佑华夏之正
朔,长期是中国南方的政治、经济、文化中心,拥有厚重的文化底蕴和丰富
的历史遗存。[5-7] 南京是国家重要的科教中心,自古以来就是一座崇文重
教的城市,有“天下文枢”、“东南第一学”的美誉。截至2013年,南京有
高等院校75所,其中211高校8所,仅次于北京上海;国家重点实验室25所、
国家重点学科169个、两院院士83人,均居中国第三。[8-10] 。",
"青岛是一个美丽的城市。"
};
/**
* 生成索引
* @param indexDir
* @throws Exception
*/
public void index(String indexDir) throws Exception {
dir = FSDirectory.open(Paths.get(indexDir));
// 先调用 getWriter 获取IndexWriter对象
IndexWriter writer = getWriter();
for(int i = 0; i < ids.length; i++) {
Document doc = new Document();
// 把上面的数据都生成索引,分别用id、city和desc来标识
doc.add(new IntField("id", ids[i], Field.Store.YES));
doc.add(new StringField("city", citys[i], Field.Store.YES));
doc.add(new TextField("desc", descs[i], Field.Store.YES));
//添加文档
writer.addDocument(doc);
}
//close了才真正写到文档中
writer.close();
}
/**
* 获取IndexWriter实例
* @return
* @throws Exception
*/
private IndexWriter getWriter() throws Exception {
//使用中文分词器
SmartChineseAnalyzer analyzer = new SmartChineseAnalyzer();
//将中文分词器配到写索引的配置中
IndexWriterConfig config = new IndexWriterConfig(analyzer);
//实例化写索引对象
IndexWriter writer = new IndexWriter(dir, config);
return writer;
}
public static void main(String[] args) throws Exception {
new ChineseIndexer().index("D:\lucene2");
}
}
这里我们用 ID、city、desc 分别代表 ID、城市名称和城市描述,用他们作为关键字来建立索引,后面我们获取内容的时候,主要来获取城市描述。
南京的描述我故意写的长一点,因为下文检索的时候,根据不同的关键字会检索到不同部分的信息,有个权重的概念在里面。
然后执行一下 main 方法,将索引保存到 D:lucene2 中。
4. 中文分词查询
中文分词查询效果是:将查询出来的关键字标红加粗。它的原理很简单:需要计算出一个得分片段,这是什么意思呢?
比如上面那个文本中我搜索 “南京文化” 跟搜索 “南京文明”,应该会返回不同的结果,这个结果是根据计算出的得分片段来确定的。
这么说,大家可能不太明白,我举个更加通俗的例子,比如有一段文本:“你好,我的名字叫倪升武,科大讯飞软件开发工程师……江湖人都叫我武哥,我一直觉得,人与人之间讲的是真诚,而不是套路。……”。
如果我搜 “倪升武”,可能会给我返回结果:“我的名字叫倪升武,科大讯飞软件开发工程师”;
如果我搜 “武哥”,可能会给我返回结果:“江湖人都叫我武哥,我一直觉得”。这就是根据搜索关键字来计算一段文本不同地方的得分,将最符合的部分搜出来。
明白了原理,我们看一下代码,我把详细的步骤写在注释中了,避免大篇幅阐述。
public class ChineseSearch {
private static final Logger logger = LoggerFactory.getLogger(ChineseSearch.class);
public static List<String> search(String indexDir, String q) throws Exception {
//获取要查询的路径,也就是索引所在的位置
Directory dir = FSDirectory.open(Paths.get(indexDir));
IndexReader reader = DirectoryReader.open(dir);
IndexSearcher searcher = new IndexSearcher(reader);
//使用中文分词器
SmartChineseAnalyzer analyzer = new SmartChineseAnalyzer();
//由中文分词器初始化查询解析器
QueryParser parser = new QueryParser("desc", analyzer);
//通过解析要查询的String,获取查询对象
Query query = parser.parse(q);
//记录索引开始时间
long startTime = System.currentTimeMillis();
//开始查询,查询前10条数据,将记录保存在docs中
TopDocs docs = searcher.search(query, 10);
//记录索引结束时间
long endTime = System.currentTimeMillis();
logger.info("匹配{}共耗时{}毫秒", q, (endTime - startTime));
logger.info("查询到{}条记录", docs.totalHits);
//如果不指定参数的话,默认是加粗,即<b><b/>
SimpleHTMLFormatter simpleHTMLFormatter = new SimpleHTMLFormatter("<b><font color=red>","</font></b>");
//根据查询对象计算得分,会初始化一个查询结果最高的得分
QueryScorer scorer = new QueryScorer(query);
//根据这个得分计算出一个片段
Fragmenter fragmenter = new SimpleSpanFragmenter(scorer);
//将这个片段中的关键字用上面初始化好的高亮格式高亮
Highlighter highlighter = new Highlighter(simpleHTMLFormatter, scorer);
//设置一下要显示的片段
highlighter.setTextFragmenter(fragmenter);
//取出每条查询结果
List<String> list = new ArrayList<>();
for(ScoreDoc scoreDoc : docs.scoreDocs) {
//scoreDoc.doc相当于docID,根据这个docID来获取文档
Document doc = searcher.doc(scoreDoc.doc);
logger.info("city:{}", doc.get("city"));
logger.info("desc:{}", doc.get("desc"));
String desc = doc.get("desc");
//显示高亮
if(desc != null) {
TokenStream tokenStream = analyzer.tokenStream("desc", new StringReader(desc));
String summary = highlighter.getBestFragment(tokenStream, desc);
logger.info("高亮后的desc:{}", summary);
list.add(summary);
}
}
reader.close();
return list;
}
}
5.功能测试
到这里,最核心的功能都实现好了,我们可以自己写个小接口来调用下,看看效果。
@Controller
@RequestMapping("/lucene")
public class IndexController {
@GetMapping("/test")
public String test(Model model) {
// 索引所在的目录
String indexDir = "D:\lucene2";
// 要查询的字符
String q = "南京文化";
try {
List<String> list = ChineseSearch.search(indexDir, q);
model.addAttribute("list", list);
} catch (Exception e) {
e.printStackTrace();
}
return "result";
}
}
在 result.html 页面做一个简单的展示操作:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div th:each="desc : ${list}">
<div th:utext="${desc}"></div>
</div>
</body>
</html>
上面我们搜索的是 “南京文化”,来看下检索出来的结果是什么。
再将搜索关键字改成 “南京文明”,看下命中的效果如何?
可以看出,不同的关键词,它会计算一个得分片段,也就是说不同的关键字会命中同一段文本中不同位置的内容,然后将关键字根据我们自己设定的形式高亮显示。
从结果中可以看出,Lucene 也可以很智能地将关键字拆分命中,这在实际项目中会很好用。
如何使用 Lucene 做网站高亮搜索功能?的更多相关文章
- 瞎折腾之 Lucene.Net + MVC 搜索功能(上)
前言 首先,关于Lucene.Net 的文章已经很多了.我这次决定写出来只是为了练练手,虽然在别人看来没什么用,但是自己确实是手动实践了一把.我个人觉得还是有意义的.爱折腾.敢于实践.才能有所收获,才 ...
- 【Lucene】Apache Lucene全文检索引擎架构之搜索功能3
上一节主要总结了一下Lucene是如何构建索引的,这一节简单总结一下Lucene中的搜索功能.主要分为几个部分,对特定项的搜索:查询表达式QueryParser的使用:指定数字范围内搜索:指定字符串开 ...
- Lucene5.5.4入门以及基于Lucene实现博客搜索功能
前言 一直以来个人博客的搜索功能很蹩脚,只是自己简单用数据库的like %keyword%来实现的,所以导致经常搜不到想要找的内容,而且高亮显示.摘要截取等也不好实现,所以决定采用Lucene改写博客 ...
- Lucene 搜索功能
搜索过程 图解: 主要 API: IndexSearcher: //所有搜索都通过 IndexSearcher 进行,他们将调用该类中重载的 search() 方法 Query: ...
- 【Lucene3.6.2入门系列】第03节_简述Lucene中常见的搜索功能
package com.jadyer.lucene; import java.io.File; import java.io.IOException; import java.text.SimpleD ...
- Apache Lucene(全文检索引擎)—搜索
目录 返回目录:http://www.cnblogs.com/hanyinglong/p/5464604.html 本项目Demo已上传GitHub,欢迎大家fork下载学习:https://gith ...
- JAVAEE——宜立方商城07:Linux上搭建Solr服务、数据库导入索引库、搜索功能的实现
1. 学习计划 1.Solr服务搭建 2.Solrj使用测试 3.把数据库中的数据导入索引库 4.搜索功能的实现 2. Solr服务搭建 2.1. Solr的环境 Solr是java开发. 需要安装j ...
- 商城06——solr索引库搭建&solr搜索功能实现&图片显示问题解决
1. 课程计划 1.搜索工程的搭建 2.linux下solr服务的搭建 3.Solrj使用测试 4.把数据库中的数据导入索引库 5.搜索功能的实现 2. 搜索工程搭建 要实现搜索功能,需要搭建 ...
- Lucene.net 高亮显示搜索词
网站搜索关键词,往往搜索的结果中,要把用户搜索的词突出显示出来,这就是高亮搜索词的含义.而lucene也恰恰支持这样的操作.在此,我用的是盘古的组件,代码如下: PanGu.HighLight.Sim ...
随机推荐
- vue非父子组件之间的通信
https://www.cnblogs.com/chengduxiaoc/p/7099552.html //vm.$emit( event, arg ) //触发当前实例上的事件 //vm.$on ...
- centos7之添加开机启动服务/脚本
一.添加开机启动脚本 #!/bin/bash # THIS FILE IS ADDED FOR COMPATIBILITY PURPOSES # # It is highly advisable to ...
- 7.docker日志收集
默认情况下,docker logs或者docker service logs显示命令的输出,就像在终端中以交互方式运行命令时一样.UNIX和Linux命令通常开在运行时间上三个I / O流,所谓的 S ...
- 【UOJ295】【ZJOI2017】线段树 倍增
题目大意 http://uoj.ac/problem/295 题解 考虑像 zkw 线段树一样,从 \([l-1,l-1],[r+1,r+1]\) 这两个区间开始往上跳,直到两个指针碰到一起为止. 先 ...
- Flask 蓝图,数据库链接
蓝图 使用场景 如果代码非常多,要进行归类.不同的功能放在不同的文件,把相关的视图函数也放进去. 蓝图也就是对flask的目录结构进行分配(应用于小,中型的程序) 当然对于大型项目也可以通过 ur ...
- FMT 与 子集(逆)卷积
本文参考了 Dance of Faith 大佬的博客 我们定义集合并卷积 \[ h_{S} = \sum_{L \subseteq S}^{} \sum_{R \subseteq S}^{} [L \ ...
- Maze HDU - 4035(期望dp)
When wake up, lxhgww find himself in a huge maze. The maze consisted by N rooms and tunnels connecti ...
- 【洛谷P1516】青蛙的约会
题目大意:给定 \(a,b,c\),求线性同余方程 \(ax+by=c\) 的最小正整数解. 题解:首先判断方程是否有解,若 c 不能整出 a 与 b 的最大公约数,则无解.若有解,则利用扩展欧几里得 ...
- react-native命令行打包APK报错
我是根据react-native官网进行的react-native打包APK 我的步骤为 第一步就是生成密钥 第二步就是将密钥拷入 第三步:在\android\gradle.properties写入 ...
- python之OpenCv(三)---基本绘图
opencv 提供了绘制直线.圆形.矩形等基本绘图的功能 1.绘直线 cv2.line(画布,起点坐标,终点坐标,颜色,宽度) 例如: cv2.line(image,(20,60),(300,400) ...