Lucene
- 全文检索有关的概念:
- 数据的分类:两种
- 结构化数据:指的是具有固定格式或者是有限长度的数据 例如:数据库中的数据 元数据等
- 非结构化数据:指的是不定长或者是没有固定格式的数据 例如:邮件 文档中的数据(生活中常见的数据都是非结构化的数据)
- 数据的查询方式:
- 结构化数据的查询方式:使用SQL语句(结构化查询语言)使用简单 速度快。
- 在数据库中进行查询非常容易的原因:数据库中数据的存储是有规律的,数据的长度和格式是固定的。
- 非结构化数据的查询方式:两种
- 顺序扫描法: 就是将一个个的文档从头到尾的进行扫描,直到扫描完所有的文件(但是这样的方式在文件的内容或者是个数非常多的时候效率低下)
- 全文扫描:将非结构化数据中的一部分信息提取出来,重新组织,使其变得有一定结构,然后对此有一定结构的数据进行搜索,从而达到搜索相对较快的目的。这部分从非结构化数据中提取出的然后重新组织的信息,我们称之为索引(这样的方式就是将非结构化的数据 转变成结构化的数据 通过先建立索引 再查询索引)
- 索引建立存在的问题:创建索引的过程是非常耗时的,但是索引一旦创建就可以多次使用,全文检索主要处理的是查询,所以耗时间创建索引是值得的
- 结构化数据的查询方式:使用SQL语句(结构化查询语言)使用简单 速度快。
- 如何实现全文检索:
- 使用
Lucene
实现全文检索。Lucene
是apache
下的一个开放源代码的全文检索引擎工具包。提供了完整的查询引擎和索引引擎,部分文本分析引擎。Lucene
的目的是为软件开发人员提供一个简单易用的工具包,以方便的在目标系统中实现全文检索的功能
- 使用
- 全文检索使用的场景:(适用在数据量大 数据结构不固定的场景)
- 搜索引擎:百度、360搜索、谷歌、搜狗等
- 站内搜索:论坛搜索 微博 文章搜索等
- 电商搜索:淘宝搜索 京东搜索等
- 数据的分类:两种
- Lucene实现全文检索的过程:
- 总的来说分两步:创建索引(对要搜索的原始内容进行索引构建一个索引库) 和 查询索引(从索引库中搜索内容)
- 创建索引的步骤:
- 获取原始文档:原始文档是指要索引和搜索的内容。原始内容包括互联网上的网页、数据库中的数据、磁盘上的文件等 这里的原始文档是磁盘中的文件。
- 创建文档对象:获取原始内容的目的是为了索引,在索引前需要将原始内容创建成文档(Document)文档中包括一个一个的域(Field),域中存储内容。这里我们可以将磁盘上的一个文件当成一个document,Document中包括一些Field(file_name文件名称、file_path文件路径、file_size文件大小、file_content文件内容),如下图:
- 注意:
- 每个Document可以有多个Field,不同的Document可以有不同的Field,同一个Document可以有相同的Field(域名和域值都相同)
- 文档都有一个唯一的编号 就是文档的id
- 注意:
- 分析文档:对域中的内容进行分析,分析的过程是经过对原始文档提取单词、将字母转为小写、去除标点符号、去除停用词等过程生成最终的语汇单元
- 每个单词叫做一个
Term
,不同的域中拆分出来的相同的单词是不同的term。term中包含两部分一部分是文档的域名,另一部分是单词的内容
- 每个单词叫做一个
- 创建索引: 对所有文档分析得出的语汇单元进行索引,索引的目的是为了搜索,最终要实现只搜索被索引的语汇单元从而找到Document(文档)
- 注意:创建索引是对语汇单元索引,通过词语找文档,这种索引的结构叫倒排索引结构 倒排索引结构也叫反向索引结构,包括索引和文档两部分,索引即词汇表,它的规模较小,而文档集合较大
- 获取原始文档:原始文档是指要索引和搜索的内容。原始内容包括互联网上的网页、数据库中的数据、磁盘上的文件等 这里的原始文档是磁盘中的文件。
- 查询索引的步骤:
- 用户查询的接口:
- 全文检索系统提供用户搜索的界面供用户提交搜索的关键字,搜索完成展示搜索结果(就像是百度中的搜索框)但是Lucene不提供制作用户搜索界面的功能,需要根据自己的需求开发搜索界面
- 全文检索系统提供用户搜索的界面供用户提交搜索的关键字,搜索完成展示搜索结果(就像是百度中的搜索框)但是Lucene不提供制作用户搜索界面的功能,需要根据自己的需求开发搜索界面
- 创建查询: 用户输入查询关键字执行搜索之前需要先构建一个查询对象,查询对象中可以指定查询要搜索的Field文档域、查询关键字等,查询对象会生成具体的查询语法 例如:
fileName:lucene
表示的是要搜索的是Field域中的内容包含lucene关键字的文档 - 执行查询:根据查询语法在倒排索引词典表中分别找出对应搜索词的索引,从而找到索引所链接的文档链表 比如搜索语法为“fileName:lucene”表示搜索出fileName域中包含Lucene的文档。搜索过程就是在索引上查找域为fileName,并且关键字为Lucene的term,并根据term找到文档id列表
- 渲染结果:以一个友好的界面将查询结果展示给用户 例如:关键字高亮显示 以相关度进行排列等。
- 用户查询的接口:
- 创建索引的步骤:
- 总的来说分两步:创建索引(对要搜索的原始内容进行索引构建一个索引库) 和 查询索引(从索引库中搜索内容)
- Lucene使用:
- 环境配置:
- 下载:Lucene是开发全文检索功能的工具包,从官方网站下载 并解压。
- 注意:不同的Lucene版本和 jdk版本对应的关系。(不同的版本对编译环境有不同的要求 这里使用的是lucene-7.4.0 jdk要求是1.8以上)
- 下载:Lucene是开发全文检索功能的工具包,从官方网站下载 并解压。
- 导入入门程序中所需要的jar包
- 创建索引:
-
创建索引的步骤:
- 创建一个IndexWriter对象。
- 构造方法中的参数
- 指定索引库的存放位置Directory对象
- 指定一个IndexWriterConfig对象。
- 创建Document对象。
- 创建field对象,将field添加到document对象中。
- 使用IndexWriter对象将Document对象写入索引库,此过程进行索引创建。并将索引和document对象写入索引库。
- 关闭IndexWriter对象
-
代码实现:
// 创建索引 @Test public void createIndex() throws Exception{ // 创建一个Directory对象 // 这样的形式是将Directory对象保存在内存中 速度快 但是 当程序终止的时候 索引就消失了 这样的方式不常使用 //Directory directory = new RAMDirectory(); // 一般是将索引库保存到磁盘中 Directory directory = FSDirectory.open(new File("E:\\lucene\\index").toPath()); // 基于directory对象来创建IndexWriter对象 // IndexWriterConfig包含了一些配置 这里使用的是标准的默认分析器(无参的构造函数) IndexWriter indexWriter = new IndexWriter(directory, new IndexWriterConfig()); // 读取磁盘中的文件 对应的每一个文件创建一个文档对象 File file = new File("E:\\lucene\\searchsource"); File[] files = file.listFiles(); for (File file1 : files) { // 取出文件名 String name = file1.getName(); // 取出文件的路径 String path = file1.getPath(); // 取出文件的内容 这里使用的是commons io来进行文件的内容的读取 String string = FileUtils.readFileToString(file1, "utf-8"); // 文件的大小 long l = FileUtils.sizeOf(file1); // 创建域 field 执行不同类型的数据进行 不同域的创建 // 参数; 1 参数1 field名称 2 field内容 3 是否是存储 Field fieldName = new TextField("name", name, Field.Store.YES); Field fieldPath = new TextField("path", path, Field.Store.YES); Field fieldContent = new TextField("content", string, Field.Store.YES); Field fieldValue = new LongPoint("value", l);// 长整型使用LongPoint来创建 int类型 使用IntPoint来创建 // 创建文档对象 Document document = new Document(); // 想文档对象中添加域对象 field document.add(fieldName); document.add(fieldPath); document.add(fieldContent); document.add(fieldValue); // 将文档对象写入索引库 indexWriter.addDocument(document); } // 关闭indexWriter对象 indexWriter.close(); }
-
使用Luke工具查看索引文件
- luke的版本是luke-7.4.0,跟lucene的版本对应的。可以打开7.4.0版本的lucene创建的索引库。需要注意的是此版本的Luke是jdk9编译的,所以要想运行此工具还需要jdk9才可以
- luke的版本是luke-7.4.0,跟lucene的版本对应的。可以打开7.4.0版本的lucene创建的索引库。需要注意的是此版本的Luke是jdk9编译的,所以要想运行此工具还需要jdk9才可以
-
- 查询索引:
- 查询索引的步骤:
- 创建一个
Directory
对象,也就是索引库存放的位置 - 创建一个
indexReader
对象,需要指定Directory对象。 - 创建一个
indexsearcher
对象,需要指定IndexReader对象 - 创建一个
TermQuery
对象,指定查询的域和查询的关键词。 - 执行查询。
- 返回查询结果。遍历查询结果并输出。
- 关闭
IndexReader
对象
- 创建一个
- 代码实现:
// 搜索索引 @Test public void searchIndex() throws Exception{ // 创建一个Directory对象 存储到磁盘中 Directory directory = FSDirectory.open(new File("E:\\lucene\\index").toPath()); // 创建一个indexReader对象 IndexReader indexReader = DirectoryReader.open(directory); // 创建一个indexSearcher对象 构造方法中需要传入 indexReader对象 IndexSearcher indexSearcher = new IndexSearcher(indexReader); // 创建一个Query对象 是一个抽象类: 创建子类对象TermQuery对象 Query query = new TermQuery(new Term("name", "spring")); // 执行查询 得到topDocs对象 search 中传入的参数:query对象 int 指定的是返回记录的条数(最大记录数 ) TopDocs search = indexSearcher.search(query, 10); // 取出查询的记录总数: System.out.println("查询的记录总数:"+ search.totalHits); // 取出文档列表 ScoreDoc[] scoreDocs = search.scoreDocs; for (ScoreDoc scoreDoc : scoreDocs) { // 取出文档的id int doc = scoreDoc.doc; // 根据id来进行取出文档对象 取出来的和创建的对象是一致的 Document document = indexSearcher.doc(doc); System.out.println(document.get("name")); System.out.println(document.get("path")); // System.out.println(document.get("content")); System.out.println(document.get("value")); System.out.println("--------------------"); } // 进行文档对象的关闭 indexReader.close(); }
- 查询索引的步骤:
- 分析器:
- 默认使用 :标准分析器
StandardAnalyzer
- 查看分析器的分析效果: 使用
Analyzer
对象的tokenStream
方法返回一个TokenStream
对象。词对象中包含了最终分词结果 - 代码实现:
@Test public void tokenStream() throws Exception{ // 创建一个analyzer 对象 使用的是StandardAnalyzer对象 Analyzer analyzer = new StandardAnalyzer(); // 使用分析器对象的tokenStream方法获取一个TokenSteam 对象 TokenStream tokenStream = analyzer.tokenStream("", "lucene是一个java开发全文搜索的工具包"); //向TokenStream对象中设置一个引用 相当于设置一个指针 CharTermAttribute charTermAttribute = tokenStream.addAttribute(CharTermAttribute.class); // 调用TokenStream对象的reset方法 不调用会抛出异常 tokenStream.reset(); // 使用while进行遍历 while (tokenStream.incrementToken()){ System.out.println(charTermAttribute.toString()); } // 关闭tokenStream对象 tokenStream.close(); }
- 存在的问题:支持中文但是对中文的进行分词结果不适用(会将每一个字进行分词 而进行搜索的时候都是使用词进行搜索 标准的分析器不适合中文)
- 中文分析器:
- SmartChineseAnalyzer :对中文支持较好,但扩展性差,扩展词库,禁用词库和同义词库等不好处理
- IKAnalyzer
- 使用前需要注意的是:
hotword.dic
和ext_stopword.dic
文件的格式为UTF-8,注意是无BOM 的UTF-8 编码。也就是说禁止使用windows记事本编辑扩展词典文件 可以使用EditPlus
或者是IDEA
等进行编辑 - 使用方式:
- 把jar包添加到工程中
- 把配置文件和扩展词典和停用词词典添加到classpath下
- 自定义分析器使用方式 代码实现:
@Test public void addDocument() throws Exception { //索引库存放路径 Directory directory = FSDirectory.open(new File("D:\\temp\\index").toPath()); // IndexWriterConfig包含了一些配置 在创建对象的时候将自定义的分析器传入 IndexWriterConfig config = new IndexWriterConfig(new IKAnalyzer()); //创建一个indexwriter对象 IndexWriter indexWriter = new IndexWriter(directory, config); //... }
- 使用前需要注意的是:
- 默认使用 :标准分析器
- 环境配置:
- 索引库的维护:就是对索引库的增删改查
-
前提条件:索引库的维护需要使用
IndexWriter
对象private IndexWriter indexWriter; @Before public void init() throws Exception{ // 创建一个indexWriter对象 需要使用IK分析器: 需要使用对索引对象进行写操作 创建indexWriter对象 Directory directory = FSDirectory.open(new File("E:\\lucene\\index").toPath()); // 基于directory对象来创建IndexWriter对象 IndexWriterConfig包含了一些配置 这里使用的是标准的默认分析器 indexWriter = new IndexWriter(directory, new IndexWriterConfig(new IKAnalyzer())); }
-
索引库的添加:
-
Field域的属性添加的依据:
- 是否分析:是否对域的内容进行分词处理。前提是我们要对域的内容进行查询。
- 是否索引:将Field分析后的词或整个Field值进行索引,只有索引方可搜索到。比如:商品名称、商品简介分析后进行索引,订单号、身份证号不用分析但也要索引,这些将来都要作为查询条件。
- 是否存储:将Field值存储在文档中,存储在文档中的Field才可以从Document中获取比如:商品名称、订单号,凡是将来要从Document中获取的Field都要存储(就是是否要将内容展示给用户)
-
添加文档的代码实现:
@Test public void addDocument() throws Exception{ // 创建一个Document对象 Document document = new Document(); // 向对象中添加域 document.add(new TextField("name", "新添加的文件", Field.Store.YES)); document.add(new TextField("content", "新添加的文件内容", Field.Store.NO)); document.add(new StoredField("path", "E:\\lucene\\searchsource")); // 将文档存储到索引库中 indexWriter.addDocument(document); // 将indexWriter关闭 indexWriter.close(); }
-
不同Field类的说明:
Field类 数据类型 Analyzed是否分析 Indexed是否索引 Stored是否存储 说明 tringField(FieldName, FieldValue,Store.YES)) 字符串 N Y Y或者N 这个Field用来构建一个字符串Field,但是不会进行分析,会将整个串存储在索引中,比如(订单号,姓名等)是否存储在文档中用Store.YES或Store.NO决定 ngPoint(String name, long… point) Long类型 Y Y N 可以使用LongPoint、IntPoint等类型存储数值类型的数据。让数值类型可以进行索引。但是不能存储数据,如果想存储数据还需要使用StoredField toredField(FieldName, FieldValue) 重载方法,支持多种类型 N N Y 这个Field用来构建不同类型Field不分析,不索引,但要Field存储在文档中 extField(FieldName, FieldValue, Store.NO)或TextField(FieldName, reader) 字符串或者流 Y Y Y或者N 如果是一个Reader, lucene猜测内容比较多,会采用Unstored的策略. -
-
索引库的删除:两种方式
-
删除全部:将索引目录的索引信息全部删除,直接彻底删除,无法恢复
@Test public void deleteIndex() throws Exception{ // 删除全部文档: 两种方式之一 全部删除 indexWriter.deleteAll(); // 进行indexWriter的关闭: indexWriter.close(); }
-
指定查询条件进行删除:通过
Query
或者是Term
对象进行删除@Test public void deleteDocumentByQuery() throws Exception{ // 删除文档通过Query 或者是 Term:文件名中包含apache的 进行的是按照query 查询 或者是 term关键词来删除 indexWriter.deleteDocuments(new Term("name", "apache")); indexWriter.close(); }
-
-
索引库的更新:(就是先进行删除 再进行创建)
@Test public void updateDocument() throws Exception{ // 跟新文档的操作是通过先删除在创建 // 创建一个Document对象 Document document= new Document(); // 象文档对象中添加域 document.add(new TextField("name","更新后的内容", Field.Store.YES)); // 更新操作: indexWriter.updateDocument(new Term("name","spring"), document); // 关闭索引库: indexWriter.close(); }
-
索引库的查询:
- 对要搜索的信息创建Query查询对象 创建的方式有两种:
- 使用Lucene提供
Query子类
- 使用`QueryParse解析查询表达式
- 使用Lucene提供
TermQuery
-
说明: TermQuery,通过项查询,TermQuery不使用分析器所以建议匹配不分词的Field域查询,比如订单号、分类ID号等。指定要查询的域和要查询的关键词
-
代码实现:
//使用Termquery查询 @Test public void testTermQuery() throws Exception { Directory directory = FSDirectory.open(new File("D:\\temp\\index").toPath()); IndexReader indexReader = DirectoryReader.open(directory); IndexSearcher indexSearcher = new IndexSearcher(indexReader); //创建查询对象 Query query = new TermQuery(new Term("content", "lucene")); //执行查询 TopDocs topDocs = indexSearcher.search(query, 10); //共查询到的document个数 System.out.println("查询结果总数量:" + topDocs.totalHits); //遍历查询结果 for (ScoreDoc scoreDoc : topDocs.scoreDocs) { Document document = indexSearcher.doc(scoreDoc.doc); System.out.println(document.get("filename")); //System.out.println(document.get("content")); System.out.println(document.get("path")); System.out.println(document.get("size")); } //关闭indexreader indexSearcher.getIndexReader().close(); }
-
- 数值范围查询:
-
代码实现:
@Test public void rangeQuery() throws Exception{ // 创建一个query对象 使用的是LongPoint 参数 域名称 最小的值 最大的值 Query size = LongPoint.newRangeQuery("value", 0L, 100L); // 执行查询: TopDocs search = indexSearcher.search(size, 10); long totalHits = search.totalHits; System.out.println("总记录数"+totalHits); ScoreDoc[] scoreDocs = search.scoreDocs; for (ScoreDoc scoreDoc : scoreDocs) { int doc = scoreDoc.doc; // 根据id来进行取出文档对象 取出来的和创建的对象是一致的 Document document = indexSearcher.doc(doc); System.out.println(document.get("name")); System.out.println(document.get("path")); System.out.println(document.get("content")); System.out.println(document.get("value")); System.out.println("--------------------"); } indexReader.close(); }
-
- 使用
QueryParser
进行查询:(就是先进行分词 在进行查询)-
代码实现:
@Test public void testQueryParser() throws Exception{ // 这样的形式 是先进行分词在进行查询 // 创建一个QueryParser对象 两个参数: 1 默认的搜索域 2 分析器对象 QueryParser queryParser = new QueryParser("name", new IKAnalyzer()); //使用QueryParser对象来创建一个query对象 Query query = queryParser.parse("lucene是一个java开发全文搜索的工具包"); // 执行查询 TopDocs search = indexSearcher.search(query,5); long totalHits = search.totalHits; System.out.println("总记录数"+totalHits); ScoreDoc[] scoreDocs = search.scoreDocs; for (ScoreDoc scoreDoc : scoreDocs) { int doc = scoreDoc.doc; // 根据id来进行取出文档对象 取出来的和创建的对象是一致的 Document document = indexSearcher.doc(doc); System.out.println(document.get("name")); System.out.println(document.get("path")); System.out.println(document.get("content")); System.out.println(document.get("value")); System.out.println("--------------------"); } indexReader.close(); }
-
- 对要搜索的信息创建Query查询对象 创建的方式有两种:
-