赞
踩
原来的方式实现搜索功能,我们的搜索流程如下图:如果用户比较少而且数据库的数据量比较小,那么这种方式实现搜索功能在企业中是比较常见的。
现在的方案(使用Lucene),如下图:为了解决数据库压力和速度的问题,我们的数据库就变成了索引库,我们使用Lucene的API的来操作服务器上的索引库。这样完全和数据库进行了隔离
先举一个例子:
例如我们使用新华字典查询汉字,新华字典有偏旁部首的目录(索引),我们查字首先查这个目录,找到这个目录中对应的偏旁部首,就可以通过这个目录中的偏旁部首找到这个字所在的位置(文档)。
Lucene会对文档建立倒排索引
1、 提取资源中关键信息, 建立索引 (目录)
2、 搜索时,根据关键字(目录),找到资源的位置
计算机索引程序通过扫描文章中的每一个词,对每一个词建立一个索引,指明该词在文章中出现的次数和位置,当用户查询时,检索程序就根据事先建立的索引进行查找,并将查找的结果反馈给用户的检索方式
Lucene是apache软件基金会4 jakarta项目组的一个子项目,是一个开放源代码的全文检索引擎工具包,但它不是一个完整的全文检索引擎,而是一个全文检索引擎的架构,提供了完整的查询引擎和索引引擎,部分文本分析引擎(英文与德文两种西方语言)。
Lucene的目的是为软件开发人员提供一个简单易用的工具包,以方便的在目标系统中实现全文检索的功能,或者是以此为基础建立起完整的全文检索引擎。
目前已经有很多应用程序的搜索功能是基于 Lucene 的,比如 Eclipse 的帮助系统的搜索功能。Lucene能够为文本类型的数据建立索引,所以你只要能把你要索引的数据格式转化的文本的,Lucene 就能对你的文档进行索引和搜索。比如你要对一些 HTML 文档,PDF 文档进行索引的话你就首先需要把HTML 文档和 PDF 文档转化成文本格式的,然后将转化后的内容交给 Lucene 进行索引,然后把创建好的索引文件保存到磁盘或者内存中,最后根据用户输入的查询条件在索引文件上进行查询。不指定要索引的文档的格式也使 Lucene 能够几乎适用于所有的搜索应用程序。
其中:
对文档索引的过程,将用户要搜索的文档内容进行索引,索引存储在索引库(index)中。
原始内容是指要索引和搜索的内容。
原始内容包括互联网上的网页、数据库中的数据、磁盘上的文件等。
从互联网上、数据库、文件系统中等获取需要搜索的原始信息,这个过程就是信息采集,采集数据的目的是为了对原始内容进行索引。
采集数据分类:
在Internet上采集信息的软件通常称为爬虫或蜘蛛,也称为网络机器人,爬虫访问互联网上的每一个网页,将获取到的网页内容存储起来。
获取原始内容的目的是为了索引,在索引前需要将原始内容创建成文档(Document),文档中包括一个一个的域(Field),域中存储内容。
这里我们可以将磁盘上的一个文件当成一个document,Document中包括一些Field,如下图:
注意:每个Document可以有多个Field,不同的Document可以有不同的Field,同一个Document可以有相同的Field(域名和域值都相同)
将原始内容创建为包含域(Field)的文档(document),需要再对域中的内容进行分析,分析成为一个一个的单词。
比如下边的文档经过分析如下:
对所有文档分析得出的语汇单元进行索引,索引的目的是为了搜索,最终要实现只搜索被索引的语汇单元从而找到Document(文档)。
创建索引是对语汇单元索引,通过关键字找文档,这种索引的结构叫倒排索引结构。
倒排索引结构是根据内容(词汇)找文档,如下图:
倒排索引结构也叫反向索引结构,包括索引和文档两部分,索引即词汇表,它的规模较小,而文档集合较大。
通过关键字找到对应的文档号,通过文档号找到文档:
搜索就是用户输入关键字,从索引中进行搜索的过程。根据关键字搜索索引,根据索引找到对应的文档,从而找到要搜索的内容。
就是使用搜索的角色,用户可以是自然人,也可以是远程调用的程序。
全文检索系统提供用户搜索的界面供用户提交搜索的关键字,搜索完成展示搜索结果。如下图:
Lucene不提供制作用户搜索界面的功能,需要根据自己的需求开发搜索界面。
用户输入查询关键字执行搜索之前需要先构建一个查询对象,查询对象中可以指定查询要查询关键字、
要搜索的Field文档域等,查询对象会生成具体的查询语法,比如:
name:手机 : 表示要搜索name这个Field域中,内容为“手机”的文档。
name:华为 AND 手机 : 表示要搜索即包括关键字“华为” 并且也包括“手机”的文档。
搜索索引过程:
以一个友好的界面将查询结果展示给用户,用户根据搜索结果找自己想要的信息,为了帮助用户很快找到自己的结果,提供了很多展示的效果,比如搜索结果中将关键字高亮显示,百度提供的快照等。
数据库准备(略)
dao准备(略)
项目准备(略)
在目录中创建一个用于存放索引文件和文档文件的目录(文件名是英文)
执行代码:
/** * 创建索引库 */ @Test public void createIndexTest() throws Exception { //1. 采集数据 SkuDao skuDao = new SkuDaoImpl(); List<Sku> skuList = skuDao.querySkuList(); //文档集合 List<Document> docList = new ArrayList<>(); for (Sku sku : skuList) { //2. 创建文档对象 Document document = new Document(); //创建域对象并且放入文档对象中 document.add(new TextField("id", sku.getId(), Field.Store.YES)); document.add(new TextField("name", sku.getName(), Field.Store.YES)); document.add(new TextField("price", String.valueOf(sku.getPrice()), Field.Store.YES)); document.add(new TextField("image", sku.getImage(), Field.Store.YES)); document.add(new TextField("categoryName", sku.getCategoryName(), Field.Store.YES)); document.add(new TextField("brandName", sku.getBrandName(), Field.Store.YES)); //将文档对象放入到文档集合中 docList.add(document); } //3. 创建分词器, StandardAnalyzer标准分词器, 对英文分词效果好, 对中文是单字分词, 也就是一个字就认为是一个词. Analyzer analyzer = new StandardAnalyzer(); //4. 创建Directory目录对象, 目录对象表示索引库的位置 Directory dir = FSDirectory.open(Paths.get("C:\software-c\lucene-dir")); //5. 创建IndexWriterConfig对象, 这个对象中指定切分词使用的分词器 IndexWriterConfig config = new IndexWriterConfig(analyzer); //6. 创建IndexWriter输出流对象, 指定输出的位置和使用的config初始化对象 IndexWriter indexWriter = new IndexWriter(dir, config); //7. 写入文档到索引库 for (Document doc : docList) { indexWriter.addDocument(doc); } //8. 释放资源 indexWriter.close(); }
生成的文件:
下载地址:https://github.com/DmitryKey/luke/releases
启动luke
打开刚刚生成的文件:
选中域对象点击show top terms可以查看分词后结果:
查看文档:
搜索文档中的数据:
对域进行筛选
如果上述查询operator选择的是or,就是分词查询,查询出包含华、为、手、机交集的数据,如果这里查看华为或者使用and,可以看到结果集少了很多
代码:
@Test public void testIndexSearch() throws Exception { //1. 创建分词器(对搜索的关键词进行分词使用) //注意: 分词器要和创建索引的时候使用的分词器一模一样 Analyzer analyzer = new StandardAnalyzer(); //2. 创建查询对象, //第一个参数: 默认查询域, 如果查询的关键字中带搜索的域名, 则从指定域中查询, 如果不带域名则从, 默认搜索域中查询 //第二个参数: 使用的分词器 QueryParser queryParser = new QueryParser("name", analyzer); //3. 设置搜索关键词 //华为手机 Query query = queryParser.parse("华为手机"); //4. 创建Directory目录对象, 指定索引库的位置 Directory dir = FSDirectory.open(Paths.get("C:\\software-c\\lucene-dir")); //5. 创建输入流对象 IndexReader indexReader = DirectoryReader.open(dir); //6. 创建搜索对象 IndexSearcher indexSearcher = new IndexSearcher(indexReader); //7. 搜索, 并返回结果 //第二个参数: 是返回多少条数据用于展示, 分页使用 TopDocs topDocs = indexSearcher.search(query, 10); //获取查询到的结果集的总数, 打印 System.out.println("=======count=======" + topDocs.totalHits); //8. 获取结果集 ScoreDoc[] scoreDocs = topDocs.scoreDocs; //9. 遍历结果集 if (scoreDocs != null) { for (ScoreDoc scoreDoc : scoreDocs) { //获取查询到的文档唯一标识, 文档id, 这个id是lucene在创建文档的时候自动分配的 int docID = scoreDoc.doc; //通过文档id, 读取文档 Document doc = indexSearcher.doc(docID); System.out.println("=================================================="); //通过域名, 从文档中获取域值 System.out.println("===id==" + doc.get("id")); System.out.println("===name==" + doc.get("name")); System.out.println("===price==" + doc.get("price")); System.out.println("===image==" + doc.get("image")); System.out.println("===brandName==" + doc.get("brandName")); System.out.println("===categoryName==" + doc.get("categoryName")); } } //10. 关闭流 }
结果:
=======count=======24147 ================================================== ===id==18182117877 ===name==华为(HUAWEI) 华为 mate10 手机 亮黑色 4G+64G ===price==76300 ===image==https://m.360buyimg.com/mobilecms/s720x720_jfs/t13615/278/1165446825/333206/55afe3aa/5a1bd4f4Nf02c806f.jpg!q70.jpg.webp ===brandName==华为 ===categoryName==手机 ================================================== ===id==18182117880 ===name==华为(HUAWEI) 华为 mate10 手机 亮黑色 6G+128G ===price==53100 ===image==https://m.360buyimg.com/mobilecms/s720x720_jfs/t13615/278/1165446825/333206/55afe3aa/5a1bd4f4Nf02c806f.jpg!q70.jpg.webp ===brandName==华为 ===categoryName==手机 ================================================== ===id==18182117882 ===name==华为(HUAWEI) 华为 mate10 手机 亮黑色 6G+128G ===price==87300 ===image==https://m.360buyimg.com/mobilecms/s720x720_jfs/t13615/278/1165446825/333206/55afe3aa/5a1bd4f4Nf02c806f.jpg!q70.jpg.webp ===brandName==华为 ===categoryName==手机 ================================================== ===id==18182117883 ===name==华为(HUAWEI) 华为 mate10 手机 亮黑色 6G+128G ===price==29100 ===image==https://m.360buyimg.com/mobilecms/s720x720_jfs/t13615/278/1165446825/333206/55afe3aa/5a1bd4f4Nf02c806f.jpg!q70.jpg.webp ===brandName==华为 ===categoryName==手机 ================================================== ===id==25387600556 ===name==华为(HUAWEI) 华为 mate10 手机 亮黑色 6G+128G ===price==32500 ===image==https://m.360buyimg.com/mobilecms/s720x720_jfs/t13615/278/1165446825/333206/55afe3aa/5a1bd4f4Nf02c806f.jpg!q70.jpg.webp ===brandName==华为 ===categoryName==手机 ================================================== ===id==39004962179 ===name==华为(HUAWEI) 华为nova3 手机 亮黑色 6G+128G ===price==88800 ===image==https://m.360buyimg.com/mobilecms/s720x720_jfs/t1/26940/35/866/202554/5c0dd453Eb6eeadcc/48ab841f324db426.jpg!q70.jpg.webp ===brandName==华为 ===categoryName==手机 ================================================== ===id==39004962180 ===name==华为(HUAWEI) 华为nova3 手机 亮黑色 6G+128G ===price==80400 ===image==https://m.360buyimg.com/mobilecms/s720x720_jfs/t1/26940/35/866/202554/5c0dd453Eb6eeadcc/48ab841f324db426.jpg!q70.jpg.webp ===brandName==华为 ===categoryName==手机 ================================================== ===id==39004962181 ===name==华为(HUAWEI) 华为nova3 手机 亮黑色 6G+128G ===price==67400 ===image==https://m.360buyimg.com/mobilecms/s720x720_jfs/t1/26940/35/866/202554/5c0dd453Eb6eeadcc/48ab841f324db426.jpg!q70.jpg.webp ===brandName==华为 ===categoryName==手机 ================================================== ===id==39004962182 ===name==华为(HUAWEI) 华为nova3 手机 亮黑色 6G+128G ===price==28900 ===image==https://m.360buyimg.com/mobilecms/s720x720_jfs/t1/26940/35/866/202554/5c0dd453Eb6eeadcc/48ab841f324db426.jpg!q70.jpg.webp ===brandName==华为 ===categoryName==手机 ================================================== ===id==39004962183 ===name==华为(HUAWEI) 华为nova3 手机 亮黑色 6G+128G ===price==1000 ===image==https://m.360buyimg.com/mobilecms/s720x720_jfs/t1/26940/35/866/202554/5c0dd453Eb6eeadcc/48ab841f324db426.jpg!q70.jpg.webp ===brandName==华为 ===categoryName==手机
如果将华为手机改成华为,结果:可以看到结果少了很多
=======count=======8029
==================================================
。。。。
Field是文档中的域,包括Field名和Field值两部分,一个文档可以包括多个Field,Document只是Field的一个承载体,Field值即为要索引的内容,也是要搜索的内容。
下边列出了开发中常用 的Filed类型,注意Field的属性,根据需求选择:
/** * 创建索引库 */ @Test public void createIndexTest() throws Exception { //1. 采集数据 SkuDao skuDao = new SkuDaoImpl(); List<Sku> skuList = skuDao.querySkuList(); //文档集合 List<Document> docList = new ArrayList<>(); for (Sku sku : skuList) { //2. 创建文档对象 Document document = new Document(); //创建域对象并且放入文档对象中 /** * 是否分词: 否, 因为主键分词后无意义 * 是否索引: 是, 如果根据id主键查询, 就必须索引 * 是否存储: 是, 因为主键id比较特殊, 可以确定唯一的一条数据, 在业务上一般有重要所用, 所以存储 * 存储后, 才可以获取到id具体的内容 */ document.add(new StringField("id", sku.getId(), Field.Store.YES)); /** * 是否分词: 是, 因为名称字段需要查询, 并且分词后有意义所以需要分词 * 是否索引: 是, 因为需要根据名称字段查询 * 是否存储: 是, 因为页面需要展示商品名称, 所以需要存储 */ document.add(new TextField("name", sku.getName(), Field.Store.YES)); /** * 是否分词: 是(因为lucene底层算法规定, 如果根据价格范围查询, 必须分词) * 是否索引: 是, 需要根据价格进行范围查询, 所以必须索引 * 是否存储: 是, 因为页面需要展示价格 */ document.add(new IntPoint("price", sku.getPrice())); document.add(new StoredField("price", sku.getPrice())); /** * 是否分词: 否, 因为不查询, 所以不索引, 因为不索引所以不分词 * 是否索引: 否, 因为不需要根据图片地址路径查询 * 是否存储: 是, 因为页面需要展示商品图片 */ document.add(new StoredField("image", sku.getImage())); /** * 是否分词: 否, 因为分类是专有名词, 是一个整体, 所以不分词 * 是否索引: 是, 因为需要根据分类查询 * 是否存储: 是, 因为页面需要展示分类 */ document.add(new StringField("categoryName", sku.getCategoryName(), Field.Store.YES)); /** * 是否分词: 否, 因为品牌是专有名词, 是一个整体, 所以不分词 * 是否索引: 是, 因为需要根据品牌进行查询 * 是否存储: 是, 因为页面需要展示品牌 */ document.add(new StringField("brandName", sku.getBrandName(), Field.Store.YES)); //将文档对象放入到文档集合中 docList.add(document); } //3. 创建分词器, StandardAnalyzer标准分词器, 对英文分词效果好, 对中文是单字分词, 也就是一个字就认为是一个词. Analyzer analyzer = new StandardAnalyzer(); //4. 创建Directory目录对象, 目录对象表示索引库的位置 Directory dir = FSDirectory.open(Paths.get("C:\\software-c\\lucene-dir")); //5. 创建IndexWriterConfig对象, 这个对象中指定切分词使用的分词器 IndexWriterConfig config = new IndexWriterConfig(analyzer); //6. 创建IndexWriter输出流对象, 指定输出的位置和使用的config初始化对象 IndexWriter indexWriter = new IndexWriter(dir, config); //7. 写入文档到索引库 for (Document doc : docList) { indexWriter.addDocument(doc); } //8. 释放资源 indexWriter.close(); }
管理人员通过电商系统更改图书信息,这时更新的是关系数据库,如果使用lucene搜索图书信息,需要在数据库表book信息变化时及时更新lucene索引库。
调用 indexWriter.addDocument(doc)添加索引。
参考入门程序的创建索引。
更新索引是先删除再添加,建议对更新需求采用此方法并且要保证对已存在的索引执行更新,可以先查询出来,确定更新记录存在执行更新操作。
如果更新索引的目标文档对象不存在,则执行添加。
代码:
/** * 索引库修改操作 * @throws Exception */ @Test public void updateIndexTest() throws Exception { //需要变更成的内容 Document document = new Document(); document.add(new StringField("id", "100000003145", Field.Store.YES)); document.add(new TextField("name", "xxxx", Field.Store.YES)); document.add(new IntPoint("price", 123)); document.add(new StoredField("price", 123)); document.add(new StoredField("image", "xxxx.jpg")); document.add(new StringField("categoryName", "手机", Field.Store.YES)); document.add(new StringField("brandName", "华为", Field.Store.YES)); //3. 创建分词器, StandardAnalyzer标准分词器, 对英文分词效果好, 对中文是单字分词, 也就是一个字就认为是一个词. Analyzer analyzer = new StandardAnalyzer(); //4. 创建Directory目录对象, 目录对象表示索引库的位置 Directory dir = FSDirectory.open(Paths.get("E:\\dir")); //5. 创建IndexWriterConfig对象, 这个对象中指定切分词使用的分词器 IndexWriterConfig config = new IndexWriterConfig(analyzer); //6. 创建IndexWriter输出流对象, 指定输出的位置和使用的config初始化对象 IndexWriter indexWriter = new IndexWriter(dir, config); //修改, 第一个参数: 修改条件, 第二个参数: 修改成的内容 indexWriter.updateDocument(new Term("id", "100000003145"), document); //8. 释放资源 indexWriter.close(); }
结果:可以通过id在luke中查询,不多演示
根据Term项删除索引,满足条件的将全部删除。
/** * 测试根据条件删除 * @throws Exception */ @Test public void deleteIndexTest() throws Exception { //3. 创建分词器, StandardAnalyzer标准分词器, 对英文分词效果好, 对中文是单字分词, 也就是一个字就认为是一个词. Analyzer analyzer = new StandardAnalyzer(); //4. 创建Directory目录对象, 目录对象表示索引库的位置 Directory dir = FSDirectory.open(Paths.get("E:\\dir")); //5. 创建IndexWriterConfig对象, 这个对象中指定切分词使用的分词器 IndexWriterConfig config = new IndexWriterConfig(analyzer); //6. 创建IndexWriter输出流对象, 指定输出的位置和使用的config初始化对象 IndexWriter indexWriter = new IndexWriter(dir, config); //测试根据条件删除 //indexWriter.deleteDocuments(new Term("id", "100000003145")); //测试删除所有内容 indexWriter.deleteAll(); //8. 释放资源 indexWriter.close(); }
不多演示。
在对Document中的内容进行索引之前,需要使用分词器进行分词 ,分词的目的是为了搜索。分词的主要过程就是先分词后过滤。
什么是停用词?停用词是为节省存储空间和提高搜索效率,搜索引擎在索引页面或处理搜索请求时会自动忽略某些字或词,这些字或词即被称为Stop Words(停用词)。比如语气助词、副词、介词、连接词等,通常自身并无明确的意义,只有将其放入一个完整的句子中才有一定作用,如常见的“的”、“在”、“是”、“啊”等。
对于分词来说,不同的语言,分词规则不同。Lucene作为一个工具包提供不同国家的分词器
输入关键字进行搜索,当需要让该关键字与文档域内容所包含的词进行匹配时需要对文档域内容进行分析,需要经过Analyzer分析器处理生成语汇单元(Token)。分析器分析的对象是文档中的Field域。当Field的属性tokenized(是否分词)为true时会对Field值进行分析,如下图:
对搜索关键字进行分析和索引分析一样,使用Analyzer对搜索关键字进行分析、分词处理,使用分析后每个词语进行搜索。比如:搜索关键字:spring web ,经过分析器进行分词,得出:spring和web拿词去索引词典表查找 ,找到索引链接到Document,解析Document内容。
对于匹配整体Field域的查询可以在搜索时不分析,比如根据订单号、身份证号查询等。
注意:搜索使用的分析器要和索引使用的分析器一致。
以下是Lucene中自带的分词器
特点 :
Lucene提供的标准分词器, 可以对用英文进行分词, 对中文是单字分词, 也就是一个字就认为是一个词。
如下是org.apache.lucene.analysis.standard.standardAnalyzer的部分源码:
protected TokenStreamComponents createComponents(String fieldName) {
final StandardTokenizer src = new StandardTokenizer();
src.setMaxTokenLength(this.maxTokenLength);
TokenStream tok = new LowerCaseFilter(src);
TokenStream tok = new StopFilter(tok, this.stopwords);
return new TokenStreamComponents(src, tok) {
protected void setReader(Reader reader) {
src.setMaxTokenLength(StandardAnalyzer.this.maxTokenLength);
super.setReader(reader);
}
};
}
Tokenizer就是分词器,负责将reader转换为语汇单元即进行分词处理,Lucene提供了很多的分词器,也可以使用第三方的分词,比如IKAnalyzer一个中文分词器。
TokenFilter是分词过滤器,负责对语汇单元进行过滤,TokenFilter可以是一个过滤器链儿,Lucene提供了很多的分词器过滤器,比如大小写转换、去除停用词等。
如下图是语汇单元的生成过程:
从一个Reader字符流开始,创建一个基于Reader的Tokenizer分词器,经过三个TokenFilter生成语汇单元Token。
比如下边的文档经过分析器分析如下:
原文档内容:Lucene is a java full-text search engine
分析后得到的多个语汇单元:lucene、java、full、text、search、engine
特点 :
仅仅是去掉了空格,没有其他任何操作,不支持中文。
测试代码:
略。
特点 :
将除了字母以外的符号全部去除,并且将所有字母变为小写,需要注意的是这个分词器同样把数字也去除了,同样不支持中文。
测试:略
特点 :
这个支持中日韩文字,前三个字母也就是这三个国家的缩写。对中文是二分法分词, 去掉空格, 去掉标点符号。个人感觉对中文支持依旧很烂
测试代码:
略。
特点 :
对中文支持也不是很好,扩展性差,扩展词库,禁用词库和同义词库等不好处理。
代码:略
学过英文的都知道,英文是以单词为单位的,单词与单词之间以空格或者逗号句号隔开。所以对于英文,我们可以简单以空格判断某个字符串是否为一个单词,比如I love China,love 和 China很容易被程序区分开来。
而中文则以字为单位,字又组成词,字和词再组成句子。中文“我爱中国”就不一样了,电脑不知道“中 国”是一个词语还是“爱中”是一个词语。
把中文的句子切分成有意义的词,就是中文分词,也称切词。我爱中国,分词的结果是:我、爱、中国。
IKAnalyzer继承Lucene的Analyzer抽象类,使用IKAnalyzer和Lucene自带的分析器方法一样,将
Analyzer测试代码改为IKAnalyzer测试中文分词效果。
如果使用中文分词器ik-analyzer,就需要在索引和搜索程序中使用一致的分词器:IK-analyzer。
从github上拉取ik-analyzer项目,并install到本地仓库
添加依赖,,pom.xml中加入依赖
<dependency>
<groupId>org.wltea.ik-analyzer</groupId>
<artifactId>ik-analyzer</artifactId>
<version>8.1.0</version>
</dependency>
/** * 使用第三方分词器(IK分词) * 特点: 支持中文语义分析, 提供停用词典, 提供扩展词典, 供程序员扩展使用 * @throws Exception */ @Test public void TestIKAnalyzer() throws Exception{ // 1. 创建分词器,分析文档,对文档进行分词 Analyzer analyzer = new IKAnalyzer(); // 2. 创建Directory对象,声明索引库的位置 Directory directory = FSDirectory.open(Paths.get("C:\\software-c\\lucene-dir")); // 3. 创建IndexWriteConfig对象,写入索引需要的配置 IndexWriterConfig config = new IndexWriterConfig(analyzer); // 4.创建IndexWriter写入对象 IndexWriter indexWriter = new IndexWriter(directory, config); // 5.写入到索引库,通过IndexWriter添加文档对象document Document doc = new Document(); doc.add(new TextField("name", "vivo X23 8GB+128GB 幻夜蓝,水滴屏全面屏,游戏手机.移动联通电信全网通4G手机", Field.Store.YES)); indexWriter.addDocument(doc); // 6.释放资源 indexWriter.close(); }
如果想配置扩展词和停用词,就创建扩展词的文件和停用词的文件。
从ikanalyzer包中拷贝配置文件
拷贝到资源文件夹中
IKAnalyzer.cfg.xml配置文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>IK Analyzer 扩展配置</comment>
<!--用户可以在这里配置自己的扩展字典 -->
<entry key="ext_dict">ext.dic;</entry>
<!--用户可以在这里配置自己的扩展停止词字典-->
<entry key="ext_stopwords">stopword.dic;</entry>
</properties>
QueryParser支持默认搜索域, 第一个参数为默认搜索域。
如果在执行parse方法的时候, 查询语法中包含域名则从指定的这个域名中搜索, 如果只有查询的关键字,则从默认搜索域中搜索结果。
需求描述 : 查询名称中包含华为手机关键字的结果.
测试代码:
@Test public void testIndexSearch() throws Exception { //1. 创建分词器(对搜索的关键词进行分词使用) //注意: 分词器要和创建索引的时候使用的分词器一模一样 Analyzer analyzer = new IKAnalyzer(); //2. 创建查询对象, //第一个参数: 默认查询域, 如果查询的关键字中带搜索的域名, 则从指定域中查询, 如果不带域名则从, 默认搜索域中查询 //第二个参数: 使用的分词器 QueryParser queryParser = new QueryParser("name", analyzer); //3. 设置搜索关键词 //华为手机(OR) // 华为 AND 手机,求交集 // 华为 OR 手机,求并集 Query query = queryParser.parse("华为 AND 手机"); //4. 创建Directory目录对象, 指定索引库的位置 Directory dir = FSDirectory.open(Paths.get("C:\\software-c\\lucene-dir")); //5. 创建输入流对象 IndexReader indexReader = DirectoryReader.open(dir); //6. 创建搜索对象 IndexSearcher indexSearcher = new IndexSearcher(indexReader); //7. 搜索, 并返回结果 //第二个参数: 是返回多少条数据用于展示, 分页使用 TopDocs topDocs = indexSearcher.search(query, 10); //获取查询到的结果集的总数, 打印 System.out.println("=======count=======" + topDocs.totalHits); //8. 获取结果集 ScoreDoc[] scoreDocs = topDocs.scoreDocs; //9. 遍历结果集 if (scoreDocs != null) { for (ScoreDoc scoreDoc : scoreDocs) { //获取查询到的文档唯一标识, 文档id, 这个id是lucene在创建文档的时候自动分配的 int docID = scoreDoc.doc; //通过文档id, 读取文档 Document doc = indexSearcher.doc(docID); System.out.println("=================================================="); //通过域名, 从文档中获取域值 System.out.println("===id==" + doc.get("id")); System.out.println("===name==" + doc.get("name")); System.out.println("===price==" + doc.get("price")); System.out.println("===image==" + doc.get("image")); System.out.println("===brandName==" + doc.get("brandName")); System.out.println("===categoryName==" + doc.get("categoryName")); } } //10. 关闭流 }
需求描述 : 查询价格大于等于100, 小于等于1000的商品
测试代码:
/** * 数值范围查询 * @throws Exception */ @Test public void testRangeQuery() throws Exception { //1. 创建分词器(对搜索的关键词进行分词使用) //注意: 分词器要和创建索引的时候使用的分词器一模一样 Analyzer analyzer = new IKAnalyzer(); //2. 创建查询对象, Query query = IntPoint.newRangeQuery("price", 100, 1000); //4. 创建Directory目录对象, 指定索引库的位置 Directory dir = FSDirectory.open(Paths.get("C:\\software-c\\lucene-dir")); //5. 创建输入流对象 IndexReader indexReader = DirectoryReader.open(dir); //6. 创建搜索对象 IndexSearcher indexSearcher = new IndexSearcher(indexReader); //7. 搜索, 并返回结果 //第二个参数: 是返回多少条数据用于展示, 分页使用 TopDocs topDocs = indexSearcher.search(query, 10); //获取查询到的结果集的总数, 打印 System.out.println("=======count=======" + topDocs.totalHits); //8. 获取结果集 ScoreDoc[] scoreDocs = topDocs.scoreDocs; //9. 遍历结果集 if (scoreDocs != null) { for (ScoreDoc scoreDoc : scoreDocs) { //获取查询到的文档唯一标识, 文档id, 这个id是lucene在创建文档的时候自动分配的 int docID = scoreDoc.doc; //通过文档id, 读取文档 Document doc = indexSearcher.doc(docID); System.out.println("=================================================="); //通过域名, 从文档中获取域值 System.out.println("===id==" + doc.get("id")); System.out.println("===name==" + doc.get("name")); System.out.println("===price==" + doc.get("price")); System.out.println("===image==" + doc.get("image")); System.out.println("===brandName==" + doc.get("brandName")); System.out.println("===categoryName==" + doc.get("categoryName")); } } //10. 关闭流 }
需求描述 : 查询价格大于等于100, 小于等于1000, 并且名称中不包含华为手机关键字的商品
注意 : 如果逻辑条件中, 只有MUST_NOT, 或者多个逻辑条件都是MUST_NOT, 无效, 查询不出任何数据
测试代码:
/** * 组合查询 * @throws Exception */ @Test public void testBooleanQuery() throws Exception { //1. 创建分词器(对搜索的关键词进行分词使用) //注意: 分词器要和创建索引的时候使用的分词器一模一样 Analyzer analyzer = new IKAnalyzer(); //2. 创建查询对象, Query query1 = IntPoint.newRangeQuery("price", 100, 1000); QueryParser queryParser = new QueryParser("name", analyzer); //3. 设置搜索关键词 //华 OR 为 手 机 Query query2 = queryParser.parse("华为手机"); //创建布尔查询对象(组合查询对象) /** * BooleanClause.Occur.MUST 必须相当于and, 也就是并且的关系 * BooleanClause.Occur.SHOULD 应该相当于or, 也就是或者的关系 * BooleanClause.Occur.MUST_NOT 不必须, 相当于not, 非 * 注意: 如果查询条件都是MUST_NOT, 或者只有一个查询条件, 然后这一个查询条件是MUST_NOT则 * 查询不出任何数据. */ BooleanQuery.Builder query = new BooleanQuery.Builder(); query.add(query1, BooleanClause.Occur.MUST); query.add(query2, BooleanClause.Occur.MUST); //4. 创建Directory目录对象, 指定索引库的位置 Directory dir = FSDirectory.open(Paths.get("C:\\software-c\\lucene-dir")); //5. 创建输入流对象 IndexReader indexReader = DirectoryReader.open(dir); //6. 创建搜索对象 IndexSearcher indexSearcher = new IndexSearcher(indexReader); //7. 搜索, 并返回结果 //第二个参数: 是返回多少条数据用于展示, 分页使用 TopDocs topDocs = indexSearcher.search(query.build(), 10); //获取查询到的结果集的总数, 打印 System.out.println("=======count=======" + topDocs.totalHits); //8. 获取结果集 ScoreDoc[] scoreDocs = topDocs.scoreDocs; //9. 遍历结果集 if (scoreDocs != null) { for (ScoreDoc scoreDoc : scoreDocs) { //获取查询到的文档唯一标识, 文档id, 这个id是lucene在创建文档的时候自动分配的 int docID = scoreDoc.doc; //通过文档id, 读取文档 Document doc = indexSearcher.doc(docID); System.out.println("=================================================="); //通过域名, 从文档中获取域值 System.out.println("===id==" + doc.get("id")); System.out.println("===name==" + doc.get("name")); System.out.println("===price==" + doc.get("price")); System.out.println("===image==" + doc.get("image")); System.out.println("===brandName==" + doc.get("brandName")); System.out.println("===categoryName==" + doc.get("categoryName")); } } //10. 关闭流 }
@Override public ResultModel query(String queryString, String price, Integer page) throws Exception { long startTime = System.currentTimeMillis(); //1. 需要使用的对象封装 ResultModel resultModel = new ResultModel(); //从第几条开始查询 int start = (page - 1) * PAGE_SIZE; //查询到多少条为止 Integer end = page * PAGE_SIZE; //创建分词器 Analyzer analyzer = new StandardAnalyzer(); //创建组合查询对象 BooleanQuery.Builder builder = new BooleanQuery.Builder(); //2. 根据查询关键字封装查询对象 QueryParser queryParser = new QueryParser("name", analyzer); Query query1 = null; //判断传入的查询关键字是否为空, 如果为空查询所有, 如果不为空, 则根据关键字查询 if (StringUtils.isEmpty(queryString)) { query1 = queryParser.parse("*:*"); } else { query1 = queryParser.parse(queryString); } //将关键字查询对象, 封装到组合查询对象中 builder.add(query1, BooleanClause.Occur.MUST); //3. 根据价格范围封装查询对象 if (!StringUtils.isEmpty(price)) { String[] split = price.split("-"); Query query2 = IntPoint.newRangeQuery("price", Integer.parseInt(split[0]), Integer.parseInt(split[1])); //将价格查询对象, 封装到组合查询对象中 builder.add(query2, BooleanClause.Occur.MUST); } //4. 创建Directory目录对象, 指定索引库的位置 /** * 使用MMapDirectory消耗的查询时间 * ====消耗时间为=========324ms * ====消耗时间为=========18ms */ Directory directory = FSDirectory.open(Paths.get("C:\\software-c\\lucene-dir")); //5. 创建输入流对象 IndexReader reader = DirectoryReader.open(directory); //6. 创建搜索对象 IndexSearcher indexSearcher = new IndexSearcher(reader); //7. 搜索并获取搜索结果 TopDocs topDocs = indexSearcher.search(builder.build(), end); //8. 获取查询到的总条数 resultModel.setRecordCount(topDocs.totalHits); //9. 获取查询到的结果集 ScoreDoc[] scoreDocs = topDocs.scoreDocs; long endTime = System.currentTimeMillis(); System.out.println("====消耗时间为=========" + (endTime - startTime) + "ms"); //10. 遍历结果集封装返回的数据 List<Sku> skuList = new ArrayList<>(); if (scoreDocs != null) { for (int i = start; i < end; i ++) { //通过查询到的文档编号, 找到对应的文档对象 Document document = reader.document(scoreDocs[i].doc); //封装Sku对象 Sku sku = new Sku(); sku.setId(document.get("id")); sku.setPrice(Integer.parseInt(document.get("price"))); sku.setImage(document.get("image")); sku.setName(document.get("name")); sku.setBrandName(document.get("brandName")); sku.setCategoryName(document.get("categoryName")); skuList.add(sku); } } //封装查询到的结果集 resultModel.setSkuList(skuList); //封装当前页 resultModel.setCurPage(page); //总页数 Long pageCount = topDocs.totalHits % PAGE_SIZE > 0 ? (topDocs.totalHits/PAGE_SIZE) + 1 : topDocs.totalHits/PAGE_SIZE; resultModel.setPageCount(pageCount); return resultModel; }
为何Lucene大数据量搜索快, 要分两部分来看 :
倒排索引中的词典位于内存,其结构尤为重要,有很多种词典结构,各有各的优缺点,最简单如排序数组,通过二分查找来检索数据,更快的有哈希表,磁盘查找有B树、B+树,但一个能支持TB级数据的倒排索引结构需要在时间和空间上有个平衡,下图列了一些常见词典的优缺点:
Lucene3.0之前使用的也是跳跃表结构,后换成了FST,但跳跃表在Lucene其他地方还有应用如倒排表合并和文档号索引。
Lucene3.0版本之前使用的跳跃表结构后换成了FST结构
单链表中查询一个元素即使是有序的,我们也不能通过二分查找法的方式缩减查询时间。
通俗的讲也就是按照链表顺序一个一个找.
举例: 查找85这个节点, 需要查找7次
举例: 查询85这个节点, 一共需要查询6次
Lucene现在采用的数据结构为FST,它的特点就是: 优点:内存占用率低,压缩率一般在3倍~20倍之间、模糊查询支持好、查询快 缺点:结构复杂、输入要求有序、更新不易
已知FST要求输入有序,所以Lucene会将解析出来的文档单词预先排序,然后构建FST,我们假设输入为abd,abe,acf,acg,那么整个构建过程如下:
例:
输入数据:
输入的数据如下:
存储的结果如下:
创建索引代码优化测试:
/** * 测试创建索引速度优化 * @throws Exception */ @Test public void createIndexTest2() throws Exception { //1. 采集数据 SkuDao skuDao = new SkuDaoImpl(); List<Sku> skuList = skuDao.querySkuList(); //文档集合 List<Document> docList = new ArrayList<>(); for (Sku sku : skuList) { //2. 创建文档对象 Document document = new Document(); document.add(new StringField("id", sku.getId(), Field.Store.YES)); document.add(new TextField("name", sku.getName(), Field.Store.YES)); document.add(new IntPoint("price", sku.getPrice())); document.add(new StoredField("price", sku.getPrice())); document.add(new StoredField("image", sku.getImage())); document.add(new StringField("categoryName", sku.getCategoryName(), Field.Store.YES)); document.add(new StringField("brandName", sku.getBrandName(), Field.Store.YES)); //将文档对象放入到文档集合中 docList.add(document); } long start = System.currentTimeMillis(); //3. 创建分词器, StandardAnalyzer标准分词器, 对英文分词效果好, 对中文是单字分词, 也就是一个字就认为是一个词. Analyzer analyzer = new StandardAnalyzer(); //4. 创建Directory目录对象, 目录对象表示索引库的位置 Directory dir = FSDirectory.open(Paths.get("C:\\software-c\\lucene-dir")); //5. 创建IndexWriterConfig对象, 这个对象中指定切分词使用的分词器 /** * 没有优化 小100万条数据, 创建索引需要7725ms */ IndexWriterConfig config = new IndexWriterConfig(analyzer); //设置在内存中多少个文档向磁盘中批量写入一次数据 //如果设置的数字过大, 会过多消耗内存, 但是会提升写入磁盘的速度 // config.setMaxBufferedDocs(500000); //6. 创建IndexWriter输出流对象, 指定输出的位置和使用的config初始化对象 IndexWriter indexWriter = new IndexWriter(dir, config); //设置多少给文档合并成一个段文件,数值越大索引速度越快, 搜索速度越慢; 值越小索引速度越慢, 搜索速度越快 indexWriter.forceMerge(1000000); //7. 写入文档到索引库 for (Document doc : docList) { indexWriter.addDocument(doc); } //8. 释放资源 indexWriter.close(); long end = System.currentTimeMillis(); System.out.println("=====消耗的时间为:==========" + (end - start) + "ms"); }
不同的分词器分词效果不同, 所用时间也不同
虽然StandardAnalyzer切分词速度快过IKAnalyzer, 但是由于StandardAnalyzer对中文支持不好, 所以为了追求好的分词效果, 为了追求查询时的准确率, 也只能用IKAnalyzer分词器, IKAnalyzer支持停用词典和扩展词典, 可以通过调整两个词典中的内容, 来提升查询匹配的精度
Lucene对查询关键字和索引文档的相关度进行打分,得分高的就排在前边。
Lucene是在用户进行检索时实时根据搜索的关键字计算出来的,分两步:
明确索引的最小单位是一个Term(索引词典中的一个词),搜索也是要从Term中搜索,再根据Term找到文档,Term对文档的重要性称为权重,影响Term权重有两个因素:
boost是一个加权值(默认加权值为1.0f),它可以影响权重的计算。
设置boost是给域(field)或者Document设置的。
查询的时候, 通过设置查询域的权重, 可以人为影响查询结果.
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。