当前位置: 首页 > news >正文

古怪的Lucene中文分词方案 —— CJKAnalyzer

Lucene 4.7 的分词器中已经有CJKAnalyzer了,这个分析器支持中文、日文、韩文和朝鲜文的分词。其过滤器CJKBigramFilter,既可以同时输出单字与双字切割,也可以选择只输出双字切割。

不过CJKAnalyzer是final类型,无法定制,需要复制出来:

//测试单双字同时分词的效果

	private static Analyzer newCjkAnalyzer() {
		return new StopwordAnalyzerBase(Version.LUCENE_47){
			protected TokenStreamComponents createComponents(String fieldName, Reader reader) {
				if (this.matchVersion.onOrAfter(Version.LUCENE_36)) {
					Tokenizer source = new StandardTokenizer(this.matchVersion, reader);
					TokenStream result = new CJKWidthFilter(source);
					result = new LowerCaseFilter(this.matchVersion, result);
					result = new CJKBigramFilter(result, 15, true);
					return new TokenStreamComponents(source, new StopFilter(this.matchVersion, result, this.stopwords));
				} else {
					Tokenizer source = new CJKTokenizer(reader);
					return new TokenStreamComponents(source, new StopFilter(this.matchVersion, source, this.stopwords));
				}
			}
		};
	}
	@Test
	public void testTokenStream() throws Exception {
		//自定义分词器
		Analyzer analyzer = newCjkAnalyzer();		
		//测试分词器
		TokenStream tokenStream = analyzer.tokenStream("test", "我是中国人The Spring Framework provides a spring programming and configuration model.");
		//添加一个引用,可以获得每个关键词
		CharTermAttribute charTermAttribute = tokenStream.addAttribute(CharTermAttribute.class);
		//添加一个偏移量的引用,记录了关键词的开始位置以及结束位置
		OffsetAttribute offsetAttribute = tokenStream.addAttribute(OffsetAttribute.class);
		//将指针调整到列表的头部
		tokenStream.reset();
		//遍历关键词列表,通过incrementToken方法判断列表是否结束
		while(tokenStream.incrementToken()) {
			System.out.println(charTermAttribute
					+ "start->" + offsetAttribute.startOffset()
					+ "end->" + offsetAttribute.endOffset());
		}
		tokenStream.close();
	}

测试字符串:我是中国人The Spring Framework

输出结果:

我 start->0 end->1
我是 start->0 end->2
是 start->1 end->2
是中 start->1 end->3
中 start->2 end->3
中国 start->2 end->4
国 start->3 end->4
国人 start->3 end->5
人 start->4 end->5
the start->5 end->8
spring start->9 end->15
framework start->16 end->25

同时输出单字与双字切割。分词方式很naive。

但是,对于搜索结果是否有影响呢?下面进行测试。


import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.Tokenizer;
import org.apache.lucene.analysis.cjk.CJKBigramFilter;
import org.apache.lucene.analysis.cjk.CJKTokenizer;
import org.apache.lucene.analysis.cjk.CJKWidthFilter;
import org.apache.lucene.analysis.core.LowerCaseFilter;
import org.apache.lucene.analysis.core.StopFilter;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.analysis.standard.StandardTokenizer;
import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
import org.apache.lucene.analysis.tokenattributes.OffsetAttribute;
import org.apache.lucene.analysis.util.StopwordAnalyzerBase;
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.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.IndexWriterConfig.OpenMode;
import org.apache.lucene.queryparser.classic.ParseException;
import org.apache.lucene.queryparser.classic.QueryParser;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.search.highlight.*;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.util.Version;
import org.junit.jupiter.api.Test;
import org.knziha.metaline.Metaline; // 元线模块,代码定义多行字符串
import test.CMN; // 打印类


	public static void main(String[] args) throws IOException, ParseException {
		Analyzer analyzer = new StandardAnalyzer(Version.LUCENE_47);
		// analyzer = newCjkAnalyzer();
		Directory index = FSDirectory.open(new File("G:/lucene-demo-index")); // MemoryIndex不太会用

		// indexing
		// 1 create index-writer
		String[][] entries  = new String[][]{
			new String[]{"0", ""}
			, new String[]{"1", "人民可以得到更多实惠"}
			, new String[]{"2", "中国人民银行"}
			, new String[]{"2", "洛杉矶人,洛杉矶居民"}
			, new String[]{"2", "民族,人民"}
			, new String[]{"2", "工人居民"}
		};
		
		// 2 write index
		if(true)
		{
			IndexWriterConfig config = new IndexWriterConfig(Version.LUCENE_47, analyzer);
			config.setOpenMode(OpenMode.CREATE);
			CMN.rt();
			IndexWriter writer = new IndexWriter(index, config);
			for (int i = 0; i < entries.length; i++) {
				Document doc = new Document();
				doc.add(new TextField("entry", entries[i][0], Field.Store.YES));
				doc.add(new StringField("bookName", "NAME", Field.Store.YES));
				// CMN.Log(text);
				doc.add(new TextField("content", entries[i][1], Field.Store.YES));
				writer.addDocument(doc);
			}
			writer.close();
			CMN.pt("索引时间::");
		}

		// search
		if(true)
		{
			CMN.rt();
			IndexReader reader = DirectoryReader.open(index);
			IndexSearcher searcher = new IndexSearcher(reader);
			
			Query query = new QueryParser(Version.LUCENE_47, "content", analyzer).parse("人民");

			CMN.Log(styles);
			
			CMN.Log("query: ", query);

			int hitsPerPage = 100;
			// 3 do search
			TopDocs docs = searcher.search(query, hitsPerPage);
			ScoreDoc[] hits = docs.scoreDocs;

			CMN.Log("found " + hits.length + " results", docs.totalHits);


			QueryScorer scorer=new QueryScorer(query); //显示得分高的片段(摘要)
			Fragmenter fragmenter=new SimpleSpanFragmenter(scorer);
			SimpleHTMLFormatter simpleHTMLFormatter=new SimpleHTMLFormatter("<b><font color='red'>","</font></b>");
			Highlighter highlighter=new Highlighter(simpleHTMLFormatter,scorer);
			highlighter.setTextFragmenter(fragmenter);

			
			
			for(ScoreDoc hit : hits) {
				CMN.Log("<br/><br/>\r\n");
				int docId = hit.doc;
				Document doc = searcher.doc(docId);
				String text = doc.get("content");
				//CMN.Log("<h1 class='title'><a href=''>"+doc.get("entry")+"</a> </h1>");
				if(text!=null) {
					//if(false)
					try {
						String bookName = doc.get("bookName");
						//bookName = "简明英汉汉英词典";
						String dt = "<span class='dt'>"+bookName+"</span>";
						/*把权重高的显示出来*/
						TokenStream tokenStream=analyzer.tokenStream("desc", new StringReader(text));
						String str = highlighter.getBestFragment(tokenStream, text);
						CMN.Log("<div class='preview'>"/*+dt*/+str+(" ("+hit.score+") ")+"</div>");
						continue;
					} catch (InvalidTokenOffsetsException e) {
						e.printStackTrace();
					}
					CMN.Log("<br/>---15字::", text.substring(0, Math.min(15, text.length())));
				}
			}
			CMN.pt("搜索时间:");

测试结果:


StandardAnalyzer

族, (0.85355335)


(0.70710677)


中国 银行 (0.53033006)


可以得到更多实惠 (0.44194174)


洛杉矶 ,洛杉矶居 (0.44194174)

newCJKAnalyzer

族, 人民 (1.1007859)


中国 人民银行 (0.7476838)


人民可以得到更多实惠 (0.62306976)


人居民 (0.5015489)


洛杉矶 ,洛杉矶居 (0.31346804)

CJKAnalyzer

民族, 人民 (0.8784157)


中国 人民银行 (0.614891)


人民可以得到更多实惠 (0.43920785)

关键词搜索“人民”,使用 StandardAnalyzer 之时,词条“工人居民”居然排在了“人民银行”等词条的前面。

可见,虽然CJKAnalyzer的分词方法较为简陋,但对于搜搜双字词语还是有帮助的,排序不会像 StandardAnalyzer 那样杂乱。同时输出单字与双字,还可搜索到更多结果。

相关文章:

  • SPDK vhost-user结合SPDK NVMe-oF RDMA性能调优
  • mysql 创建函数
  • 支持十亿级密态数据、低代码,蚂蚁集团发布隐语开放平台
  • 关于kafka常见名词解释,你了解多少?
  • 吴恩达深度学习笔记(四)——深度学习的实践层面
  • KNN-KG论文学习笔记
  • DOM与BOM与Echarts
  • 13c++呵呵老师【pawn移动组件与碰撞】
  • 简明介绍 n-gram
  • 前端培训丁鹿学堂:es7_es11常用新特性(三)
  • 深入浅出总结求解菲波那切数列的五种方法
  • TCP滑动窗口机制(重要)
  • [2008][note]腔内级联拉曼发射的,二极管泵浦多频调Q laser——
  • [基础服务] CentOS 7.x 安装NodeJS环境并搭建Hexo
  • 设计一个支持多版本的APP的后端服务
  • 【Leetcode】101. 对称二叉树
  • bootstrap创建登录注册页面
  • gitlab-ci配置详解(一)
  • HTTP中GET与POST的区别 99%的错误认识
  • JavaSE小实践1:Java爬取斗图网站的所有表情包
  • LeetCode刷题——29. Divide Two Integers(Part 1靠自己)
  • OSS Web直传 (文件图片)
  • php面试题 汇集2
  • React16时代,该用什么姿势写 React ?
  • 百度地图API标注+时间轴组件
  • 从零开始的webpack生活-0x009:FilesLoader装载文件
  • 反思总结然后整装待发
  • 浅谈web中前端模板引擎的使用
  • 如何在 Intellij IDEA 更高效地将应用部署到容器服务 Kubernetes ...
  • #14vue3生成表单并跳转到外部地址的方式
  • #WEB前端(HTML属性)
  • (离散数学)逻辑连接词
  • (六)vue-router+UI组件库
  • (太强大了) - Linux 性能监控、测试、优化工具
  • ..回顾17,展望18
  • .NET C# 使用 SetWindowsHookEx 监听鼠标或键盘消息以及此方法的坑
  • .NET Micro Framework初体验
  • .NET版Word处理控件Aspose.words功能演示:在ASP.NET MVC中创建MS Word编辑器
  • /etc/fstab和/etc/mtab的区别
  • @Bean, @Component, @Configuration简析
  • [Android]创建TabBar
  • [BT]BUUCTF刷题第9天(3.27)
  • [BUUCTF NewStarCTF 2023 公开赛道] week3 crypto/pwn
  • [BUUCTF]-Reverse:reverse3解析
  • [Gradle] 在 Eclipse 下利用 gradle 构建系统
  • [IE9] IE9 beta版下载链接
  • [java基础揉碎]方法的重写/覆盖
  • [noip模拟]计蒜姬BFS
  • [one_demo_12]递归打印*\n*.*.\n*..*..\n图形
  • [poj3686]The Windy's(费用流)
  • [StartingPoint][Tier0]Synced
  • [UGUI]实现从一个道具栏拖拽一个UI道具到另一个道具栏
  • [VS] 诊断工具,检测内存泄漏,进行内存调优
  • [WeChall] Prime Factory (Training, Math) 的解决方法
  • [翻译]Gallery Server Pro ----用于分享相片,视频,音频及其他媒体的ASP.NET相册[Carol]...