Lucene
全文检索技术
原来的方式实现搜索功能,我们的搜索流程如下图:
正在上传…重新上传取消
正在上传…重新上传取消
上图就是原始搜索引擎技术,如果用户比较少而且数据库的数据量比较小,那么这种方式实现搜索功能在企业中是比较常见的。
但是数据量过多时,数据库的压力就会变得很大,查询速度会变得非常慢。我们需要使用更好的解决方案来分担数据库的压力。
现在的方案(使用Lucene),如下图
正在上传…重新上传取消
正在上传…重新上传取消
为了解决数据库压力和速度的问题,我们的数据库就变成了索引库,我们使用Lucene的API的来操作服务器上的索引库。这样完全和数据库进行了隔离。
所谓顺序扫描,例如要找内容包含一个字符串的文件,就是一个文档一个文档的看,对于每一个文档,从头看到尾,如果此文档包含此字符串,则此文档为我们要找的文件,接着看下一个文件,直到扫描完所有的文件。
这种方法是顺序扫描方法,数据量大就搜索慢。
先举一个栗子:
例如我们使用新华字典查询汉字,新华字典有偏旁部首的目录(索引),我们查字首先查这个目录,找到这个目录中对应的偏旁部首,就可以通过这个目录中的偏旁部首找到这个字所在的位置(文档)。
现在有两篇文档:
Doc1: When in Rome, do as the Romans do.
Doc2: When do you come back from Rome?
Lucene会对以上两篇文档建立倒排索引
索引结构如下图:
正在上传…重新上传取消
应用场景 :
1、 单机软件的搜索(word中的搜索)
2、 站内搜索 (baidu贴吧、论坛、 京东、 taobao)
3、 垂直领域的搜索 (818工作网) 智联招聘
4、 专业搜索引擎公司 (google、baidu)
计算机索引程序通过扫描文章中的每一个词,对每一个词建立一个索引,指明该词在文章中出现的次数和位置,当用户查询时,检索程序就根据事先建立的索引进行查找,并将查找的结果反馈给用户的检索方式
Lucene是apache软件基金会4 jakarta项目组的一个子项目,是一个开放源代码的全文检索引擎工具包,但它不是一个完整的全文检索引擎,而是一个全文检索引擎的架构,提供了完整的查询引擎和索引引擎,部分文本分析引擎(英文与德文两种西方语言)。
Lucene的目的是为软件开发人员提供一个简单易用的工具包,以方便的在目标系统中实现全文检索的功能,或者是以此为基础建立起完整的全文检索引擎。
目前已经有很多应用程序的搜索功能是基于 Lucene 的,比如 Eclipse 的帮助系统的搜索功能。Lucene 能够为文本类型的数据建立索引,所以你只要能把你要索引的数据格式转化的文本的,Lucene 就能对你的文档进行索引和搜索。比如你要对一些 HTML 文档,PDF 文档进行索引的话你就首先需要把 HTML 文档和 PDF 文档转化成文本格式的,然后将转化后的内容交给 Lucene 进行索引,然后把创建好的索引文件保存到磁盘或者内存中,最后根据用户输入的查询条件在索引文件上进行查询。不指定要索引的文档的格式也使 Lucene 能够几乎适用于所有的搜索应用程序。
全文检索系统是按照全文检索理论建立起来的用于提供全文检索服务的软件系统,包括建立索引、处理查询返回结果集、增加索引、优化索引结构等功能。例如:百度搜索、eclipse帮助搜索、淘宝网商品搜索等。
搜索引擎是全文检索技术最主要的一个应用,例如百度。搜索引擎起源于传统的信息全文检索理论,即计算机程序通过扫描每一篇文章中的每一个词,建立以词为单位的倒排文件,检索程序根据检索词在每一篇文章中出现的频率和每一个检索词在一篇文章中出现的概率,对包含这些检索词的文章进行排序,最后输出排序的结果。全文检索技术是搜索引擎的核心支撑技术。
Lucene和搜索引擎不同,Lucene是一套用java或其它语言写的全文检索的工具包,为应用程序提供了很多个api接口去调用,可以简单理解为是一套实现全文检索的类库,搜索引擎是一个全文检索系统,它是一个单独运行的软件系统
官网: http://lucene.apache.org/
正在上传…重新上传取消
正在上传…重新上传取消
正在上传…重新上传取消
正在上传…重新上传取消
1、绿色表示索引过程,对要搜索的原始内容进行索引构建一个索引库,索引过程包括:
确定原始内容即要搜索的内容à获得文档à创建文档à分析文档à索引文档
2、红色表示搜索过程,从索引库中搜索内容,搜索过程包括:
用户通过搜索界面à创建查询à执行搜索,从索引库搜索à渲染搜索结果
对文档索引的过程,将用户要搜索的文档内容进行索引,索引存储在索引库(index)中。
原始内容是指要索引和搜索的内容。
原始内容包括互联网上的网页、数据库中的数据、磁盘上的文件等。
从互联网上、数据库、文件系统中等获取需要搜索的原始信息,这个过程就是信息采集,采集数据的目的是为了对原始内容进行索引。
采集数据分类:
1、对于互联网上网页,可以使用工具将网页抓取到本地生成html文件。
2、数据库中的数据,可以直接连接数据库读取表中的数据。
3、文件系统中的某个文件,可以通过I/O操作读取文件的内容。
在Internet上采集信息的软件通常称为爬虫或蜘蛛,也称为网络机器人,爬虫访问互联网上的每一个网页,将获取到的网页内容存储起来。
Lucene不提供信息采集的类库,需要自己编写一个爬虫程序实现信息采集,也可以通过一些开源软件实现信息采集,如下:
Solr(Welcome to Apache Solr - Apache Solr) ,solr是apache的一个子项目,支持从关系数据库、xml文档中提取原始数据。
Nutch(http://lucene.apache.org/nutch), Nutch是apache的一个子项目,包括大规模爬虫工具,能够抓取和分辨web网站数据。
jsoup(http://jsoup.org/ ),jsoup 是一款Java 的HTML解析器,可直接解析某个URL地址、HTML文本内容。它提供了一套非常省力的API,可通过DOM,CSS以及类似于jQuery的操作方法来取出和操作数据。
获取原始内容的目的是为了索引,在索引前需要将原始内容创建成文档(Document),文档中包括一个一个的域(Field),域中存储内容。
这里我们可以将磁盘上的一个文件当成一个document,Document中包括一些Field,如下图:
Document(文档)
|
Field(域) Name:id(图书id) Value:1 |
Field(域) Name:name(图书名称) Value:lucene |
Field(域) Name:price(图书价格) Value:66 |
Field(域) Name:desc(图书描述) Value:lucene是apache的开源项目,是java开发的检索的工具
|
其它Field.。。。。。。。。。。。。 |
注意:每个Document可以有多个Field,不同的Document可以有不同的Field,同一个Document可以有相同的Field(域名和域值都相同)
将原始内容创建为包含域(Field)的文档(document),需要再对域中的内容进行分析,分析成为一个一个的单词。
比如下边的文档经过分析如下:
原文档内容:
Lucene is a Java full-text search engine. Lucene is not a complete
application, but rather a code library and API that can easily be used
to add search capabilities to applications.
分析后得到的词:
lucene、java、full、search、engine。。。。
对所有文档分析得出的语汇单元进行索引,索引的目的是为了搜索,最终要实现只搜索被索引的语汇单元从而找到Document(文档)。
创建索引是对语汇单元索引,通过词语找文档,这种索引的结构叫倒排索引结构。
倒排索引结构是根据内容(词汇)找文档,如下图:
正在上传…重新上传取消
倒排索引结构也叫反向索引结构,包括索引和文档两部分,索引即词汇表,它的规模较小,而文档集合较大。
搜索就是用户输入关键字,从索引中进行搜索的过程。根据关键字搜索索引,根据索引找到对应的文档,从而找到要搜索的内容。
就是使用搜索的角色,用户可以是自然人,也可以是远程调用的程序。
全文检索系统提供用户搜索的界面供用户提交搜索的关键字,搜索完成展示搜索结果。如下图:
正在上传…重新上传取消
Lucene不提供制作用户搜索界面的功能,需要根据自己的需求开发搜索界面。
用户输入查询关键字执行搜索之前需要先构建一个查询对象,查询对象中可以指定查询要查询关键字、要搜索的Field文档域等,查询对象会生成具体的查询语法,比如:
name:lucene表示要搜索name这个Field域中,内容为“lucene”的文档。
desc:lucene AND desc:java 表示要搜索即包括关键字“lucene”也包括“java”的文档。
搜索索引过程:
例如搜索语法为“desc:lucene AND desc:java”表示搜索出的文档中既要包括lucene也要包括java。
词lucene |
文档lucene |
文档solr |
..... |
词java |
文档lucene |
文档java编程思想 |
..... |
3、获取文档中的Field域数据。
以一个友好的界面将查询结果展示给用户,用户根据搜索结果找自己想要的信息,为了帮助用户很快找到自己的结果,提供了很多展示的效果,比如搜索结果中将关键字高亮显示,百度提供的快照等。
正在上传…重新上传取消
Lucene可以在官网上下载。课程已经准备好了Lucene的文件,我们使用的是4.10.3版本,文件位置如下图:
正在上传…重新上传取消
解压后的效果:
正在上传…重新上传取消
使用这三个文件的jar包,就可以实现lucene功能
本教程使用的数据是MySQL数据库的数据,所以还需要MySQL的连接包
学员编写的时候,也可以直接复制准备好的jar包,位置如下图:
正在上传…重新上传取消
JDK: 1.7 (Lucene4.8以上,必须使用JDK1.7及以上版本)
IDE: eclipse Mars2
数据库: MySQL
数据库脚本位置如下图:
正在上传…重新上传取消
导入到MySQL效果如下图:
正在上传…重新上传取消
创建java工程测试即可,效果如下:
正在上传…重新上传取消
在电商网站中,全文检索的数据源在数据库中,需要通过jdbc访问数据库中book表的内容。
public class Book {
// 图书ID
private Integer id;
// 图书名称
private String name;
// 图书价格
private Float price;
// 图书图片
private String pic;
// 图书描述
private String desc;
get/set。。。
}
public interface BookDao {
/**
* 查询所有的book数据
*
* @return
*/
List<Book> queryBookList();
}
使用jdbc实现
public class BookDaoImpl implements BookDao {
@Override
public List<Book> queryBookList() {
// 数据库链接
Connection connection = null;
// 预编译statement
PreparedStatement preparedStatement = null;
// 结果集
ResultSet resultSet = null;
// 图书列表
List<Book> list = new ArrayList<Book>();
try {
// 加载数据库驱动
Class.forName("com.mysql.jdbc.Driver");
// 连接数据库
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/solr", "root", "root");
// SQL语句
String sql = "SELECT * FROM book";
// 创建preparedStatement
preparedStatement = connection.prepareStatement(sql);
// 获取结果集
resultSet = preparedStatement.executeQuery();
// 结果集解析
while (resultSet.next()) {
Book book = new Book();
book.setId(resultSet.getInt("id"));
book.setName(resultSet.getString("name"));
book.setPrice(resultSet.getFloat("price"));
book.setPic(resultSet.getString("pic"));
book.setDesc(resultSet.getString("desc"));
list.add(book);
}
} catch (Exception e) {
e.printStackTrace();
}
return list;
}
}
public class CreateIndexTest {
@Test
public void testCreateIndex() throws Exception {
// 1. 采集数据
BookDao bookDao = new BookDaoImpl();
List<Book> bookList = bookDao.queryBookList();
// 2. 创建Document文档对象
List<Document> documents = new ArrayList<>();
for (Book book : bookList) {
Document document = new Document();
// Document文档中添加Field域
// 图书Id
// Store.YES:表示存储到文档域中
document.add(new TextField("id", book.getId().toString(), Store.YES));
// 图书名称
document.add(new TextField("name", book.getName().toString(), Store.YES));
// 图书价格
document.add(new TextField("price", book.getPrice().toString(), Store.YES));
// 图书图片地址
document.add(new TextField("pic", book.getPic().toString(), Store.YES));
// 图书描述
document.add(new TextField("desc", book.getDesc().toString(), Store.YES));
// 把Document放到list中
documents.add(document);
}
// 3. 创建Analyzer分词器,分析文档,对文档进行分词
Analyzer analyzer = new StandardAnalyzer();
// 4. 创建Directory对象,声明索引库的位置
Directory directory = FSDirectory.open(new File("C:/itcast/lucene/index"));
// 5. 创建IndexWriteConfig对象,写入索引需要的配置
IndexWriterConfig config = new IndexWriterConfig(Version.LUCENE_4_10_3, analyzer);
// 6.创建IndexWriter写入对象
IndexWriter indexWriter = new IndexWriter(directory, config);
// 7.写入到索引库,通过IndexWriter添加文档对象document
for (Document doc : documents) {
indexWriter.addDocument(doc);
}
// 8.释放资源
indexWriter.close();
}
}
执行效果:
在文件夹中出现了以下文件,表示创建索引成功
正在上传…重新上传取消
Luke作为Lucene工具包中的一个工具(http://www.getopt.org/luke/),可以通过界面来进行索引文件的查询、修改
luke所在位置如下图:
正在上传…重新上传取消
打开Luke方法:打开cmd命令行运行命令:java -jar lukeall-4.10.3.jar
打开后,使用如下图:
正在上传…重新上传取消
下图是索引域的展示效果:
正在上传…重新上传取消
下图是文档域展示效果
正在上传…重新上传取消
Lucene可以通过query对象输入查询语句。同数据库的sql一样,lucene也有固定的查询语法:
最基本的有比如:AND, OR, NOT 等(必须大写)
举个栗子:
用户想找一个desc中包括java关键字和lucene关键字的文档。
它对应的查询语句:desc:java AND desc:lucene
如下图是使用luke搜索的例子:
正在上传…重新上传取消
和索引过程的分词一样,这里要对用户输入的关键字进行分词,一般情况索引和搜索使用的分词器一致。
比如:输入搜索关键字“java学习”,分词后为java和学习两个词,与java和学习有关的内容都搜索出来了,如下:
正在上传…重新上传取消
1. 创建Query搜索对象
2. 创建Directory流对象,声明索引库位置
3. 创建索引读取对象IndexReader
4. 创建索引搜索对象IndexSearcher
5. 使用索引搜索对象,执行搜索,返回结果集TopDocs
6. 解析结果集
7. 释放资源
IndexSearcher搜索方法如下:
方法 |
说明 |
indexSearcher.search(query, n) |
根据Query搜索,返回评分最高的n条记录 |
indexSearcher.search(query, filter, n) |
根据Query搜索,添加过滤策略,返回评分最高的n条记录 |
indexSearcher.search(query, n, sort) |
根据Query搜索,添加排序策略,返回评分最高的n条记录 |
indexSearcher.search(booleanQuery, filter, n, sort) |
根据Query搜索,添加过滤策略,添加排序策略,返回评分最高的n条记录 |
代码实现
public class SearchIndexTest {
@Test
public void testSearchIndex() throws Exception {
// 1. 创建Query搜索对象
// 创建分词器
Analyzer analyzer = new StandardAnalyzer();
// 创建搜索解析器,第一个参数:默认Field域,第二个参数:分词器
QueryParser queryParser = new QueryParser("desc", analyzer);
// 创建搜索对象
Query query = queryParser.parse("desc:java AND lucene");
// 2. 创建Directory流对象,声明索引库位置
Directory directory = FSDirectory.open(new File("C:/itcast/lucene/index"));
// 3. 创建索引读取对象IndexReader
IndexReader reader = DirectoryReader.open(directory);
// 4. 创建索引搜索对象
IndexSearcher searcher = new IndexSearcher(reader);
// 5. 使用索引搜索对象,执行搜索,返回结果集TopDocs
// 第一个参数:搜索对象,第二个参数:返回的数据条数,指定查询结果最顶部的n条数据返回
TopDocs topDocs = searcher.search(query, 10);
System.out.println("查询到的数据总条数是:" + topDocs.totalHits);
// 获取查询结果集
ScoreDoc[] docs = topDocs.scoreDocs;
// 6. 解析结果集
for (ScoreDoc scoreDoc : docs) {
// 获取文档
int docID = scoreDoc.doc;
Document doc = searcher.doc(docID);
System.out.println("=============================");
System.out.println("docID:" + docID);
System.out.println("bookId:" + doc.get("id"));
System.out.println("name:" + doc.get("name"));
System.out.println("price:" + doc.get("price"));
System.out.println("pic:" + doc.get("pic"));
// System.out.println("desc:" + doc.get("desc"));
}
// 7. 释放资源
reader.close();
}
}
在对Docuemnt中的内容进行索引之前,需要使用分词器进行分词 ,分词的目的是为了搜索。分词的主要过程就是先分词后过滤。
什么是停用词?停用词是为节省存储空间和提高搜索效率,搜索引擎在索引页面或处理搜索请求时会自动忽略某些字或词,这些字或词即被称为Stop Words(停用词)。比如语气助词、副词、介词、连接词等,通常自身并无明确的意义,只有将其放入一个完整的句子中才有一定作用,如常见的“的”、“在”、“是”、“啊”等。
对于分词来说,不同的语言,分词规则不同。Lucene作为一个工具包提供不同国家的分词器,本例子使用StandardAnalyzer,它可以对用英文进行分词。
如下是org.apache.lucene.analysis.standard.standardAnalyzer的部分源码:
@Override
protected TokenStreamComponents createComponents(final String fieldName, final Reader reader) {
final StandardTokenizer src = new StandardTokenizer(getVersion(), reader);
src.setMaxTokenLength(maxTokenLength);
TokenStream tok = new StandardFilter(getVersion(), src);
tok = new LowerCaseFilter(getVersion(), tok);
tok = new StopFilter(getVersion(), tok, stopwords);
return new TokenStreamComponents(src, tok) {
@Override
protected void setReader(final Reader reader) throws IOException {
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 |
输入关键字进行搜索,当需要让该关键字与文档域内容所包含的词进行匹配时需要对文档域内容进行分析,需要经过Analyzer分析器处理生成语汇单元(Token)。分析器分析的对象是文档中的Field域。当Field的属性tokenized(是否分词)为true时会对Field值进行分析,如下图:
正在上传…重新上传取消
对于一些Field可以不用分析:
1、不作为查询条件的内容,比如文件路径
2、不是匹配内容中的词而匹配Field的整体内容,比如订单号、身份证号等。
对搜索关键字进行分析和索引分析一样,使用Analyzer对搜索关键字进行分析、分词处理,使用分析后每个词语进行搜索。比如:搜索关键字:spring web ,经过分析器进行分词,得出:spring web拿词去索引词典表查找 ,找到索引链接到Document,解析Document内容。
对于匹配整体Field域的查询可以在搜索时不分析,比如根据订单号、身份证号查询等。
注意:搜索使用的分析器要和索引使用的分析器一致。
学过英文的都知道,英文是以单词为单位的,单词与单词之间以空格或者逗号句号隔开。所以对于英文,我们可以简单以空格判断某个字符串是否为一个单词,比如I love China,love 和 China很容易被程序区分开来。
而中文则以字为单位,字又组成词,字和词再组成句子。中文“我爱中国”就不一样了,电脑不知道“中国”是一个词语还是“爱中”是一个词语。
把中文的句子切分成有意义的词,就是中文分词,也称切词。我爱中国,分词的结果是:我、爱、中国。
单字分词:就是按照中文一个字一个字地进行分词。如:“我爱中国”,
效果:“我”、“爱”、“中”、“国”。
二分法分词:按两个字进行切分。如:“我是中国人”,效果:“我是”、“是中”、“中国”“国人”。
上边两个分词器无法满足需求。
对中文支持较好,但扩展性差,扩展词库,禁用词库和同义词库等不好处理
IKAnalyzer继承Lucene的Analyzer抽象类,使用IKAnalyzer和Lucene自带的分析器方法一样,将Analyzer测试代码改为IKAnalyzer测试中文分词效果。
如果使用中文分词器ik-analyzer,就需要在索引和搜索程序中使用一致的分词器:IK-analyzer。
我是中国人
‘我是’
中国
国人
中国人
正在上传…重新上传取消
@Test
public void testCreateIndex() throws Exception {
// 1. 采集数据
BookDao bookDao = new BookDaoImpl();
List<Book> bookList = bookDao.queryBookList();
// 2. 创建Document文档对象
List<Document> documents = new ArrayList<>();
for (Book book : bookList) {
Document document = new Document();
// Document文档中添加Field域
// 图书Id
// Store.YES:表示存储到文档域中
document.add(new TextField("id", book.getId().toString(), Store.YES));
// 图书名称
document.add(new TextField("name", book.getName().toString(), Store.YES));
// 图书价格
document.add(new TextField("price", book.getPrice().toString(), Store.YES));
// 图书图片地址
document.add(new TextField("pic", book.getPic().toString(), Store.YES));
// 图书描述
document.add(new TextField("desc", book.getDesc().toString(), Store.YES));
// 把Document放到list中
documents.add(document);
}
// 3. 创建Analyzer分词器,分析文档,对文档进行分词
// Analyzer analyzer = new StandardAnalyzer();
Analyzer analyzer = new IKAnalyzer();
// 4. 创建Directory对象,声明索引库的位置
Directory directory = FSDirectory.open(new File("C:/itcast/lucene/index"));
// 5. 创建IndexWriteConfig对象,写入索引需要的配置
IndexWriterConfig config = new IndexWriterConfig(Version.LUCENE_4_10_3, analyzer);
// 6.创建IndexWriter写入对象
IndexWriter indexWriter = new IndexWriter(directory, config);
// 7.写入到索引库,通过IndexWriter添加文档对象document
for (Document doc : documents) {
indexWriter.addDocument(doc);
}
// 8.释放资源
indexWriter.close();
}
如果想配置扩展词和停用词,就创建扩展词的文件和停用词的文件。
注意:不要用window自带的记事本保存扩展词文件和停用词文件,那样的话,格式中是含有bom的。
正在上传…重新上传取消
从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>
中文词库,添加新词的地方
正在上传…重新上传取消
stopword.dic是存放停用词的地方
正在上传…重新上传取消
最终分词效果
正在上传…重新上传取消
Field是文档中的域,包括Field名和Field值两部分,一个文档可以包括多个Field,Document只是Field的一个承载体,Field值即为要索引的内容,也是要搜索的内容。
是:作分词处理,即将Field值进行分词,分词的目的是为了索引。
比如:商品名称、商品描述等,这些内容用户要输入关键字搜索,由于搜索的内容格式大、内容多需要分词后将语汇单元建立索引
否:不作分词处理
比如:商品id、订单号、身份证号等
是:进行索引。将Field分词后的词或整个Field值进行索引,存储到索引域,索引的目的是为了搜索。
比如:商品名称、商品描述分析后进行索引,订单号、身份证号不用分词但也要索引,这些将来都要作为查询条件。
否:不索引。
比如:图片路径、文件路径等,不用作为查询条件的不用索引。
是:将Field值存储在文档域中,存储在文档域中的Field才可以从Document中获取。
比如:商品名称、订单号,凡是将来要从Document中获取的Field都要存储。
否:不存储Field值
比如:商品描述,内容较大不用存储。如果要向用户展示商品描述可以从系统的关系数据库中获取。
下边列出了开发中常用 的Filed类型,注意Field的属性,根据需求选择:
Field类 |
数据类型 |
Analyzed 是否分词 |
Indexed 是否索引 |
Stored 是否存储 |
说明 |
StringField(FieldName, FieldValue,Store.YES)) |
字符串 |
N |
Y |
Y或N |
这个Field用来构建一个字符串Field,但是不会进行分词,会将整个串存储在索引中,比如(订单号,身份证号等) 是否存储在文档中用Store.YES或Store.NO决定 |
LongField(FieldName, FieldValue,Store.YES) FloatField |
Long型 |
Y |
Y |
Y或N |
这个Field用来构建一个Long数字型Field,进行分词和索引,比如(价格) 是否存储在文档中用Store.YES或Store.NO决定 |
StoredField(FieldName, FieldValue) |
重载方法,支持多种类型 |
N |
N |
Y |
这个Field用来构建不同类型Field 不分析,不索引,但要Field存储在文档中 |
TextField(FieldName, FieldValue, Store.NO) 或 TextField(FieldName, reader) |
字符串 或 流 |
Y |
Y |
Y或N |
如果是一个Reader, lucene猜测内容比较多,会采用Unstored的策略. |
图书id:
是否分词:不用分词,因为不会根据商品id来搜索商品
是否索引:不索引,因为不需要根据图书ID进行搜索
是否存储:要存储,因为查询结果页面需要使用id这个值。
图书名称:
是否分词:要分词,因为要根据图书名称的关键词搜索。
是否索引:要索引。
是否存储:要存储。
图书价格:
是否分词:要分词,lucene对数字型的值只要有搜索需求的都要分词和索引,因 为lucene对数字型的内容要特殊分词处理,需要分词和索引。
是否索引:要索引
是否存储:要存储
图书图片地址:
是否分词:不分词
是否索引:不索引
是否存储:要存储
图书描述:
是否分词:要分词
是否索引:要索引
是否存储:因为图书描述内容量大,不在查询结果页面直接显示,不存储。
不存储是不在lucene的索引域中记录,节省lucene的索引文件空间。
如果要在详情页面显示描述,解决方案:
从lucene中取出图书的id,根据图书的id查询关系数据库(MySQL)中book表得到描述信息。
对之前编写的testCreateIndex()方法进行修改。
代码片段
// Document文档中添加域
// 图书Id
// Store.YES:表示存储到文档域中
// 不分词,不索引,储存
document.add(new StoredField("id", book.getId().toString()));
// 图书名称
// 分词,索引,储存
document.add(new TextField("name", book.getName().toString(), Store.YES));
// 图书价格
// 分词,索引,储存
document.add(new FloatField("price", book.getPrice(), Store.YES));
// 图书图片地址
// 不分词,不索引,储存
document.add(new StoredField("pic", book.getPic().toString()));
// 图书描述
// 分词,索引,不储存
document.add(new TextField("desc", book.getDesc().toString(), Store.NO));
管理人员通过电商系统更改图书信息,这时更新的是关系数据库,如果使用lucene搜索图书信息,需要在数据库表book信息变化时及时更新lucene索引库。
调用 indexWriter.addDocument(doc)添加索引。
参考入门程序的创建索引。
根据Term项删除索引,满足条件的将全部删除。
@Test
public void testIndexDelete() throws Exception {
// 创建Directory流对象
Directory directory = FSDirectory.open(new File("C:/itcast/lucene/index"));
IndexWriterConfig config = new IndexWriterConfig(Version.LUCENE_4_10_3, null);
// 创建写入对象
IndexWriter indexWriter = new IndexWriter(directory, config);
// 根据Term删除索引库,name:java
indexWriter.deleteDocuments(new Term("name", "java"));
// 释放资源
indexWriter.close();
}
效果如下图:索引域没有变化
正在上传…重新上传取消
文档域数据被删除掉
正在上传…重新上传取消
正在上传…重新上传取消
将索引目录的索引信息全部删除,直接彻底删除,无法恢复。
建议参照关系数据库基于主键删除方式,所以在创建索引时需要创建一个主键Field,删除时根据此主键Field删除。
索引删除后将放在Lucene的回收站中,Lucene3.X版本可以恢复删除的文档,3.X之后无法恢复。
代码:
@Test
public void testIndexDelete() throws Exception {
// 创建Directory流对象
Directory directory = FSDirectory.open(new File("D:/itcast/lucene/index"));
IndexWriterConfig config = new IndexWriterConfig(Version.LUCENE_4_10_3, null);
// 创建写入对象
IndexWriter indexWriter = new IndexWriter(directory, config);
// 根据Term删除索引库,name:java
// indexWriter.deleteDocuments(new Term("name", "java"));
// 全部删除
indexWriter.deleteAll();
// 释放资源
indexWriter.close();
}
索引域数据清空
正在上传…重新上传取消
文档域数据也清空
正在上传…重新上传取消
更新索引是先删除再添加,建议对更新需求采用此方法并且要保证对已存在的索引执行更新,可以先查询出来,确定更新记录存在执行更新操作。
如果更新索引的目标文档对象不存在,则执行添加。
代码
@Test
public void testIndexUpdate() throws Exception {
// 创建分词器
Analyzer analyzer = new IKAnalyzer();
// 创建Directory流对象
Directory directory = FSDirectory.open(new File("C:/itcast/lucene/index"));
IndexWriterConfig config = new IndexWriterConfig(Version.LUCENE_4_10_3, analyzer);
// 创建写入对象
IndexWriter indexWriter = new IndexWriter(directory, config);
// 创建Document
Document document = new Document();
document.add(new TextField("id", "1002", Store.YES));
document.add(new TextField("name", "lucene测试test 002", Store.YES));
// 执行更新,会把所有符合条件的Document删除,再新增。
indexWriter.updateDocument(new Term("name", "test"), document);
// 释放资源
indexWriter.close();
}
对要搜索的信息创建Query查询对象,Lucene会根据Query查询对象生成最终的查询语法。类似关系数据库Sql语法一样,Lucene也有自己的查询语法,比如:“name:lucene”表示查询名字为name的Field域中的“lucene”的文档信息。
可通过两种方法创建查询对象:
1)使用Lucene提供Query子类
Query是一个抽象类,lucene提供了很多查询对象,比如TermQuery项精确查询,NumericRangeQuery数字范围查询等。
如下代码:
Query query = new TermQuery(new Term("name", "lucene"));
2)使用QueryParse解析查询表达式
QueryParser会将用户输入的查询表达式解析成Query对象实例。
如下代码:
QueryParser queryParser = new QueryParser("name", new IKAnalyzer());
Query query = queryParser.parse("name:lucene");
TermQuery词项查询,TermQuery不使用分析器,搜索关键词进行精确匹配Field域中的词,比如订单号、分类ID号等。
搜索对象创建:
@Test
public void testSearchTermQuery() throws Exception {
// 创建TermQuery搜索对象
Query query = new TermQuery(new Term("name", "lucene"));
doSearch(query);
}
抽取搜索逻辑:
private void doSearch(Query query) throws IOException {
// 2. 执行搜索,返回结果集
// 创建Directory流对象
Directory directory = FSDirectory.open(new File("D:/itcast/lucene/index"));
// 创建索引读取对象IndexReader
IndexReader reader = DirectoryReader.open(directory);
// 创建索引搜索对象
IndexSearcher searcher = new IndexSearcher(reader);
// 使用索引搜索对象,执行搜索,返回结果集TopDocs
// 第一个参数:搜索对象,第二个参数:返回的数据条数,指定查询结果最顶部的n条数据返回
TopDocs topDocs = searcher.search(query, 10);
System.out.println("查询到的数据总条数是:" + topDocs.totalHits);
// 获取查询结果集
ScoreDoc[] docs = topDocs.scoreDocs;
// 解析结果集
for (ScoreDoc scoreDoc : docs) {
// 获取文档id
int docID = scoreDoc.doc;
Document doc = searcher.doc(docID);
System.out.println("======================================");
System.out.println("docID:" + docID);
System.out.println("bookId:" + doc.get("id"));
System.out.println("name:" + doc.get("name"));
System.out.println("price:" + doc.get("price"));
System.out.println("pic:" + doc.get("pic"));
// System.out.println("desc:" + doc.get("desc"));
}
// 3. 释放资源
reader.close();
}
NumericRangeQuery,指定数字范围查询.
@Test
public void testSearchNumericRangeQuery() throws Exception {
// 创建NumericRangeQuery搜索对象,数字范围查询.
// 五个参数分别是:域名、最小值、最大值、是否包含最小值,是否包含最大值
Query query = NumericRangeQuery.newFloatRange("price", 54f, 56f, false, true);
doSearch(query);
}
BooleanQuery,布尔查询,实现组合条件查询。
@Test
public void testSearchBooleanQuery() throws Exception {
// 创建TermQuery搜索对象
Query query1 = new TermQuery(new Term("name", "lucene"));
// 创建NumericRangeQuery搜索对象,数字范围查询.
// 四个参数分别是:域名、最小值、最大值、是否包含最小值,是否包含最大值
Query query2 = NumericRangeQuery.newFloatRange("price", 54f, 66f, false, true);
// 创建BooleanQuery搜索对象,组合查询条件
BooleanQuery boolQuery = new BooleanQuery();
// 组合条件,
// 第一个参数,查询条件,第二个参数,组合方式
boolQuery.add(query1, Occur.MUST_NOT);
boolQuery.add(query2, Occur.MUST);
doSearch(boolQuery);
}
组合关系代表的意思如下:
1、MUST和MUST表示“与”的关系,即“交集”。
2、MUST和MUST_NOT前者包含后者不包含。
3、MUST_NOT和MUST_NOT没意义
4、SHOULD与MUST表示MUST,SHOULD失去意义;
5、SHOULD与MUST_NOT相当于MUST与MUST_NOT。
6、SHOULD与SHOULD表示“或”的关系,即“并集”。
通过QueryParser也可以创建Query,QueryParser提供一个Parse方法,此方法可以直接根据查询语法来查询。可以通过打印Query对象的方式,查看生成的查询语句。
1、基础的查询语法,关键词查询:
域名+“:”+搜索的关键字
例如:name:java
域名+“:”+[最小值 TO 最大值]
例如:size:[1 TO 1000]
注意:QueryParser不支持对数字范围的搜索,它支持字符串范围。数字范围搜索建议使用NumericRangeQuery。
Occur.MUST 查询条件必须满足,相当于AND |
+(加号) |
Occur.SHOULD 查询条件可选,相当于OR
|
空(不用符号) |
Occur.MUST_NOT 查询条件不能满足,相当于NOT非 |
-(减号) |
@Test
public void testSearchIndex() throws Exception {
// 创建分词器
Analyzer analyzer = new StandardAnalyzer();
// 1. 创建Query搜索对象
// 创建搜索解析器,第一个参数:默认Field域,第二个参数:分词器
QueryParser queryParser = new QueryParser("desc", analyzer);
// 创建搜索对象
// Query query = queryParser.parse("desc:java学习");
Query query = queryParser.parse("desc:java AND lucene");
// 打印生成的搜索语句
System.out.println(query);
// 执行搜索
doSearch(query);
}
通过MultiFieldQueryParse对多个域查询。
@Test
public void testSearchMultiFieldQueryParser() throws Exception {
// 创建分词器
Analyzer analyzer = new IKAnalyzer();
// 1. 创建MultiFieldQueryParser搜索对象
String[] fields = { "name", "desc" };
MultiFieldQueryParser multiFieldQueryParser = new MultiFieldQueryParser(fields, analyzer);
// 创建搜索对象
Query query = multiFieldQueryParser.parse("lucene");
// 打印生成的搜索语句
System.out.println(query);
// 执行搜索
doSearch(query);
}
生成的查询语句:
name:lucene desc:lucene
Lucene搜索结果可通过TopDocs遍历,TopDocs类提供了少量的属性,如下:
方法或属性 |
说明 |
totalHits |
匹配搜索条件的总记录数 |
scoreDocs |
顶部匹配记录 |
注意:
Search方法需要指定匹配记录数量n:indexSearcher.search(query, n)
TopDocs.totalHits:是匹配索引库中所有记录的数量
TopDocs.scoreDocs:匹配相关度高的前边记录数组,scoreDocs的长度小于等于search方法指定的参数n
相关度排序是查询结果按照与查询关键字的相关性进行排序,越相关的越靠前。比如搜索“Lucene”关键字,与该关键字最相关的文章应该排在前边。
SEO 优化
设置Boost属性值
如果两个属性值一样那么后面写入的权重高
Lucene对查询关键字和索引文档的相关度进行打分,得分高的就排在前边。如何打分呢?Lucene是在用户进行检索时实时根据搜索的关键字计算出来的,分两步:
1)计算出词(Term)的权重
2)根据词的权重值,计算文档相关度得分。
什么是词的权重?
通过索引部分的学习,明确索引的最小单位是一个Term(索引词典中的一个词)。搜索也是从索引域中查询Term,再根据Term找到文档。Term对文档的重要性称为权重,影响Term权重有两个因素:
指此Term在此文档中出现了多少次。tf 越大说明越重要。
词(Term)在文档中出现的次数越多,说明此词(Term)对该文档越重要,如“Lucene”这个词,在文档中出现的次数很多,说明该文档主要就是讲Lucene技术的。
指有多少文档包含此Term。df 越大说明越不重要。
比如,在一篇英语文档中,this出现的次数更多,就说明越重要吗?不是的,有越多的文档包含此词(Term), 说明此词(Term)太普通,不足以区分这些文档,因而重要性越低。
boost是一个加权值(默认加权值为1.0f),它可以影响权重的计算。在索引时对某个文档中的field设置加权值,设置越高,在搜索时匹配到这个文档就可能排在前边。
未设置权重:
希望把name为spring的排名提高
正在上传…重新上传取消
先清空索引库,然后修改创建索引的代码,添加设置加权值的逻辑
修改创建索引代码:
@Test
public void testCreateIndex() throws Exception {
// 1. 采集数据
BookDao bookDao = new BookDaoImpl();
List<Book> bookList = bookDao.queryBookList();
// 2. 创建Document文档对象
List<Document> documents = new ArrayList<>();
for (Book book : bookList) {
Document document = new Document();
// Document文档中添加域
// 图书Id
// Store.YES:表示存储到文档域中
// 不分词,不索引,储存
document.add(new StoredField("id", book.getId().toString()));
// 图书名称
// 分词,索引,储存
document.add(new TextField("name", book.getName().toString(), Store.YES));
// 图书价格
// 分词,索引,储存
document.add(new FloatField("price", book.getPrice(), Store.YES));
// 图书图片地址
// 不分词,不索引,储存
document.add(new StoredField("pic", book.getPic().toString()));
// 图书描述
// 分词,索引,不储存
TextField descField = new TextField("desc", book.getDesc().toString(), Store.NO);
// 给id为4的文档设置加权值
if (4 == book.getId()) {
descField.setBoost(100f);
}
document.add(descField);
// 把Document放到list中
documents.add(document);
}
// 3. 创建Analyzer分词器,分析文档,对文档进行分词
Analyzer analyzer = new StandardAnalyzer();
// 4. 创建IndexWrite,需要directory流对象
// 创建流对象
Directory directory = FSDirectory.open(new File("D:/itcast/lucene/index"));
// 创建IndexWriteConfig对象
IndexWriterConfig config = new IndexWriterConfig(Version.LUCENE_4_10_3, analyzer);
// 创建IndexWriter写入对象
IndexWriter indexWriter = new IndexWriter(directory, config);
// 通过IndexWriter添加文档对象document
for (Document doc : documents) {
indexWriter.addDocument(doc);
}
// 释放资源
indexWriter.close();
}
执行创建索引的逻辑,使用luke重载新生成的索引库,再次查询spring在第一
查询结果:
正在上传…重新上传取消
Solr 是Apache下的一个顶级开源项目,采用Java开发,它是基于Lucene的全文搜索服务。Solr可以独立运行在Jetty、Tomcat等这些Servlet容器中。
使用Solr 进行创建索引和搜索索引的实现方法很简单,如下:
Solr是一个可以独立运行的搜索服务器,使用solr进行全文检索服务的话,只需要通过http请求访问该服务器即可。
Lucene是一个开放源代码的全文检索引擎工具包,它不是一个完整的全文检索应用。Lucene仅提供了完整的查询引擎和索引引擎,目的是为软件开发人员提供一个简单易用的工具包,以方便的在目标系统中实现全文检索的功能,或者以Lucene为基础构建全文检索应用。
Solr的目标是打造一款企业级的搜索引擎系统,它是基于Lucene一个搜索引擎服务,可以独立运行,通过Solr可以非常快速的构建企业的搜索引擎,通过Solr也可以高效的完成站内搜索功能。
正在上传…重新上传取消
Solr和lucene的版本是同步更新的,本课程使用的版本:4.10.3
下载地址:http://archive.apache.org/dist/lucene/solr/
Linux下需要solr-4.10.3.tgz,windows下需要solr-4.10.3.zip。
正在上传…重新上传取消
解压solr-4.10.3.zip:
正在上传…重新上传取消
bin:solr的运行脚本
contrib:solr的一些扩展jar包,用于增强solr的功能。
dist:该目录包含build过程中产生的war和jar文件,以及相关的依赖文件。
docs:solr的API文档
example:solr工程的例子目录:
该目录是一个标准的SolrHome,它包含一个默认的SolrCore
该目录包含了在Solr的multicore中设置的多个Core目录。
该目录中包括一个solr.war,该war可作为solr的运行实例工程。
licenses:solr相关的一些许可信息
solr 需要运行在一个Servlet容器中,Solr4.10.3要求jdk使用1.7以上,Solr默认提供Jetty(java写的Servlet容器),
使用jetty启动:
使用cmd命令行,进入example文件夹启动
启动命令java -jar start.jar
正在上传…重新上传取消
启动后访问地址:http://127.0.0.1:8983/solr
但是企业中一般使用Tomcat作为服务器,本课程也是一样,
相关环境如下:
SolrHome是Solr服务运行的主目录,该目录中包括了多个SolrCore目录。SolrCore目录中包含了运行Solr实例所有的配置文件和数据文件,Solr实例就是SolrCore。
每个SolrCore提供单独的搜索和索引服务。
SolrHome目录:
正在上传…重新上传取消
SolrCore目录:
正在上传…重新上传取消
创建SolrCore先要创建SolrHome。在solr解压包下solr-4.10.3\example\solr文件夹就是一个标准的SolrHome,只需要将它复制到指定的目录下即可。
拷贝solr解压包下solr-4.10.3\example\solr文件夹。
正在上传…重新上传取消
复制该文件夹到本地的一个目录,把文件名称改为solrhome。
改名不是必须的,只是为了便于理解
正在上传…重新上传取消
打开solrhome目录确认solrcore
正在上传…重新上传取消
其实就是配置SolrCore目录下的conf/solrconfig.xml。
正在上传…重新上传取消
这个文件是来配置SolrCore实例的相关信息。如果使用默认配置可以不用做任何修改。它里面包含了不少标签,但是我们经常使用的标签为:lib标签、datadir标签、requestHandler标签。
在solrconfig.xml中可以加扩展载一些的jar,如果需要使用,则首先要把这些jar复制到指定的目录,我们复制到SolrHome同级目录
复制之前解压的文件夹中的contrib和dist文件夹
正在上传…重新上传取消
粘贴到SolrHome同级目录下。
正在上传…重新上传取消
修改solrconfig.xml配置文件加载扩展的jar。
configsolr.install.dir表示${SolrCore}的目录位置,需要如下修改:
./ 表示当前目录 ../表示上一级目录
正在上传…重新上传取消
配置SolrCore的data目录。
data目录用来存放SolrCore的索引文件和tlog日志文件
solr.data.dir表示${SolrCore}/data的目录位置
正在上传…重新上传取消
如果不想使用默认的目录也可以通过solrconfig.xml更改索引目录 ,
例如:
正在上传…重新上传取消
(建议不修改,否则配置多个SolrCore会报错)
requestHandler请求处理器,定义了索引和搜索的访问方式。
通过/update维护索引,可以完成索引的添加、修改、删除操作。
正在上传…重新上传取消
通过/select搜索索引。
正在上传…重新上传取消
设置搜索参数完成搜索,搜索参数也可以设置一些默认值,如下:
<requestHandler name="/select" class="solr.SearchHandler">
<!-- 设置默认的参数值,可以在请求地址中修改这些参数-->
<lst name="defaults">
<str name="echoParams">explicit</str>
<int name="rows">10</int><!--显示数量-->
<str name="wt">json</str><!--显示格式-->
<str name="df">text</str><!--默认搜索字段-->
</lst>
</requestHandler>
由于在项目中用到的web服务器大多数是用的Tomcat,所以就进行solr和Tomcat的整合。
复制自己的Tomcat7到这里
正在上传…重新上传取消
删除不用的应用(可以不删)
正在上传…重新上传取消
修改server.xml配置文件里面的端口号(否则后面eclipse使用Tomcat会冲突)
正在上传…重新上传取消
修改以下三个端口号
正在上传…重新上传取消
正在上传…重新上传取消
正在上传…重新上传取消
复制solr.war
正在上传…重新上传取消
粘贴到自己Tomcat的webapps里
正在上传…重新上传取消
在Tomcat的webapps里,把war解压到当前路径,并删除solr.war
正在上传…重新上传取消
效果:
正在上传…重新上传取消
正在上传…重新上传取消
把solr解压包下solr-4.10.3\example\lib\ext目录下的所有jar包拷贝到Tomcat部署的solr的WEB-INF/lib文件夹
复制扩展jar包
正在上传…重新上传取消
粘贴到Tomcat的webapps的solr工程的WEB-INF\lib目录
正在上传…重新上传取消
需要修改web.xml,让Tomcat使用JNDI的方式告诉solr服务器SolrHome在哪。
正在上传…重新上传取消
修改内容:
第42行的Solr/home名称必须是固定的,修改第43行,如下图
正在上传…重新上传取消
访问
http://localhost:8081/solr/
出现以下界面则说明solr安装成功!!!
仪表盘,显示了该Solr实例开始启动运行的时间、版本、系统资源、jvm等信息。
Solr运行日志信息
Cloud即SolrCloud,即Solr云(集群),当使用Solr Cloud模式运行时会显示此菜单,该部分功能在第二个项目,即电商项目会演示。
Solr Core的管理界面。在这里可以添加SolrCore实例(有bug,不推荐使用浏览器界面添加SolrCore)。
Solr在JVM 运行环境中的属性信息,包括类路径、文件编码、jvm内存设置等信息。
显示Solr Server中当前活跃线程信息,同时也可以跟踪线程运行栈信息。
选择一个SolrCore进行详细操作,如下:
正在上传…重新上传取消
通过此界面可以测试索引分析器和搜索分析器的执行情况
正在上传…重新上传取消
可以定义数据导入处理器,从关系数据库将数据导入到Solr索引库中。
默认没有配置,需要手工配置。
通过/update表示更新索引,solr默认根据id(唯一约束)域来更新Document的内容,如果根据id值搜索不到id域则会执行添加操作,如果找到则更新。
通过此菜单可以创建索引、更新索引、删除索引等操作,界面如下:
正在上传…重新上传取消
通过/select执行搜索索引,必须指定“q”查询条件方可搜索。
正在上传…重新上传取消
solrj是访问Solr服务的java客户端,提供索引和搜索的请求方法,如下图:
Index索引库 |
javaEE应用程序
SolrJ程序客户端
|
Solr服务
Tomcat |
通过SolrJ请求Solr服务 |
最终Solr完在索引和搜索 |
Solrj和图形界面操作的区别就类似于数据库中使用jdbc和mysql客户端的区别一样。
使用solrj调用solr服务实现对索引库的增删改查操作。
正在上传…重新上传取消
Solrj的包,\solr-4.10.3\dist\目录下
正在上传…重新上传取消
solrj依赖包,\solr-4.10.3\dist\solrj-lib
正在上传…重新上传取消
Solr服务的依赖包,\solr\example\lib\ext
正在上传…重新上传取消
说明:根据id(唯一约束)域来更新Document的内容,如果根据id值搜索不到id域则会执行添加操作,如果找到则更新。
@Test
public void testCreateAndUpdateIndex() throws Exception {
// 1. 创建HttpSolrServer对象
// 设置solr服务接口,浏览器客户端地址http://127.0.0.1:8081/solr/#/
String baseURL = "http://127.0.0.1:8081/solr";
HttpSolrServer httpSolrServer = new HttpSolrServer(baseURL);
// 2. 创建SolrInputDocument对象
SolrInputDocument document = new SolrInputDocument();
document.addField("id", "c1001");
document.addField("content ", "Hello world!");
// 3. 把SolrInputDocument对象添加到索引库中
httpSolrServer.add(document);
// 4. 提交
httpSolrServer.commit();
}
抽取HttpSolrServer 的创建代码
private HttpSolrServer httpSolrServer;
// 提取HttpSolrServer创建
@Before
public void init() {
// 1. 创建HttpSolrServer对象
// 设置solr服务接口,浏览器客户端地址http://127.0.0.1:8081/solr/#/
String baseURL = "http://127.0.0.1:8081/solr/";
this.httpSolrServer = new HttpSolrServer(baseURL);
}
删除索引逻辑,两种:
根据id删除
根据条件删除,根据条件删除
可以使用*:*作为条件,就是删除所有数据(慎用)
@Test
public void testDeleteIndex() throws Exception {
// 根据id删除索引数据
// this.httpSolrServer.deleteById("c1001");
// 根据条件删除(如果是*:*就表示全部删除,慎用)
this.httpSolrServer.deleteByQuery("*:*");
// 提交
this.httpSolrServer.commit();
}
/**
* 简单搜索
*
* @throws Exception
*/
@Test
public void testSearchIndex1() throws Exception {
// 创建搜索对象
SolrQuery query = new SolrQuery();
// 设置搜索条件
query.setQuery("*:*");
// 发起搜索请求
QueryResponse response = this.httpSolrServer.query(query);
// 处理搜索结果
SolrDocumentList results = response.getResults();
System.out.println("搜索到的结果总数:" + results.getNumFound());
// 遍历搜索结果
for (SolrDocument solrDocument : results) {
System.out.println("----------------------------------------------------");
System.out.println("id:" + solrDocument.get("id"));
System.out.println("content" + solrDocument.get("content"));
}
schema.xml文件在SolrCore的conf目录下,在此配置文件中定义了域以及域的类型等一些配置。在solr中域必须先定义后使用。
正在上传…重新上传取消
<field name="id" type="string" indexed="true" stored="true" required="true" multiValued="false" />
<dynamicField name="*_s" type="string" indexed="true" stored="true" />
Name:动态域的名称,是一个表达式,*匹配任意字符,只要域的名称和表达式的规则能够匹配就可以使用。
例如:搜索时查询条件[product_i:钻石]就可以匹配这个动态域,可以直接使用,不用单独再定义一个product_i域。
<uniqueKey>id</uniqueKey>
相当于主键,每个文档中必须有一个id域。
<copyField source="cat" dest="text"/>
可以将多个Field复制到一个Field中,以便进行统一的检索。当创建索引时,solr服务器会自动的将源域的内容复制到目标域中。
定义目标域:
<field name="text" type="text_general" indexed="true" stored="false" multiValued="true"/>
目标域必须要使用:multiValued="true"
<fieldType name="text_general" class="solr.TextField" positionIncrementGap="100">
<analyzer type="index">
<tokenizer class="solr.StandardTokenizerFactory"/>
<filter class="solr.StopFilterFactory" ignoreCase="true" words="stopwords.txt" />
<filter class="solr.LowerCaseFilterFactory"/>
</analyzer>
<analyzer type="query">
<tokenizer class="solr.StandardTokenizerFactory"/>
<filter class="solr.StopFilterFactory" ignoreCase="true" words="stopwords.txt" />
<filter class="solr.SynonymFilterFactory" synonyms="synonyms.txt" ignoreCase="true" expand="true"/>
<filter class="solr.LowerCaseFilterFactory"/>
</analyzer>
</fieldType>
使用IKAnalyzer中文分析器
正在上传…重新上传取消
第一步:把IKAnalyzer2012FF_u1.jar添加到solr/WEB-INF/lib目录下。
正在上传…重新上传取消
第二步:复制IKAnalyzer的配置文件和自定义词典和停用词词典到solr的solr/WEB-INF/classes目录下。
复制IK分词器配置文件、自定义词典、停用词词典
正在上传…重新上传取消
粘贴到Tomcat的solr的/WEB-INF/classes目录下
正在上传…重新上传取消
第三步:在schema.xml中添加一个自定义的fieldType,使用中文分析器。
<!-- IKAnalyzer-->
<fieldType name="text_ik" class="solr.TextField">
<analyzer class="org.wltea.analyzer.lucene.IKAnalyzer"/>
</fieldType>
第四步:在schema.xml中添加field,指定field的type属性为text_ik
<!--IKAnalyzer Field-->
<field name="content_ik" type="text_ik" indexed="true" stored="true" />
第五步:重启tomcat
效果:
正在上传…重新上传取消
要使用solr实现网站中商品搜索,需要 将mysql数据库中数据在solr中创建索引。
在数据库中运行solr.sql脚本
正在上传…重新上传取消
先确定定义的商品document的Field域有哪些?
可以根据mysql数据库中商品表的字段来确定:
products商品表:
正在上传…重新上传取消
Schema.xml中配置业务域
<!--product-->
<field name="product_name" type="text_ik" indexed="true" stored="true"/>
<field name="product_price" type="float" indexed="true" stored="true"/>
<field name="product_description" type="text_ik" indexed="true" stored="false" />
<field name="product_picture" type="string" indexed="false" stored="true" />
<field name="product_catalog_name" type="string" indexed="true" stored="true" />
<field name="product_keywords" type="text_ik" indexed="true" stored="false" multiValued="true"/>
<copyField source="product_name" dest="product_keywords"/>
<copyField source="product_description" dest="product_keywords"/>
使用dataimport插件批量导入数据。
第一步:把dataimport插件依赖的jar包添加到solrcore(collection1\lib)中, 还需要mysql的数据库驱动。
正在上传…重新上传取消
第二步:配置solrconfig.mxl文件,添加一个requestHandler。
<requestHandler name="/dataimport" class="org.apache.solr.handler.dataimport.DataImportHandler"> <lst name="defaults"> <str name="config">data-config.xml</str> </lst> </requestHandler> |
第三步:创建一个data-config.xml,保存到collection1\conf\目录下
<?xml version="1.0" encoding="UTF-8" ?> <dataConfig> <dataSource type="JdbcDataSource" driver="com.mysql.jdbc.Driver" url="jdbc:mysql://localhost:3306/lucene" user="root" password="root"/> <document> <entity name="product" query="SELECT pid,name,catalog_name,price,description,picture FROM products "> <field column="pid" name="id"/> <field column="name" name="product_name"/> <field column="catalog_name" name="product_catalog_name"/> <field column="price" name="product_price"/> <field column="description" name="product_description"/> <field column="picture" name="product_picture"/> </entity> </document>
</dataConfig> |
第四步:重启tomcat
正在上传…重新上传取消
第五步:点击“execute”按钮导入数据
注意:导入数据前会先清空索引库,然后再导入。
请求的q是字符串,如果查询所有使用*:*
正在上传…重新上传取消
作用:在q查询符合结果中同时是fq查询符合的
请求fq是一个数组(多个值)
过滤查询价格从1到20的记录。
正在上传…重新上传取消
也可以使用“*”表示无限,例如:
20以上:product_price:[20 TO *]
20以下:product_price:[* TO 20]
也可以在“q”查询条件中使用product_price:[1 TO 20],
如下效果和上面一样:
正在上传…重新上传取消
按照价格升序排
正在上传…重新上传取消
rows: 指定返回结果最多有多少条记录,配合start来实现分页。
正在上传…重新上传取消
正在上传…重新上传取消
显示商品id、商品名称、商品分类名称
正在上传…重新上传取消
正在上传…重新上传取消
正在上传…重新上传取消
页面的查询条件,复杂查询条件和页面的查询条件一致
正在上传…重新上传取消
代码实现:
//复杂查询索引 @Test public void queryIndex2() throws Exception { //创建连接 SolrServer solrServer = new HttpSolrServer("http://localhost:8080/solr"); //创建一个query对象 SolrQuery query = new SolrQuery(); //设置查询条件 query.setQuery("钻石"); //过滤条件 query.setFilterQueries("product_catalog_name:幽默杂货"); //排序条件 query.setSort("product_price", ORDER.asc); //分页处理 query.setStart(0); query.setRows(10); //结果中域的列表 query.setFields("id","product_name","product_price","product_catalog_name","product_picture"); //设置默认搜索域 query.set("df", "product_keywords"); //高亮显示 query.setHighlight(true); //高亮显示的域 query.addHighlightField("product_name"); //高亮显示的前缀 query.setHighlightSimplePre("<em>"); //高亮显示的后缀 query.setHighlightSimplePost("</em>"); //执行查询 QueryResponse queryResponse = solrServer.query(query); //取查询结果 SolrDocumentList solrDocumentList = queryResponse.getResults(); //共查询到商品数量 System.out.println("共查询到商品数量:" + solrDocumentList.getNumFound()); //遍历查询的结果 for (SolrDocument solrDocument : solrDocumentList) { System.out.println(solrDocument.get("id")); //取高亮显示 String productName = ""; Map<String, Map<String, List<String>>> highlighting = queryResponse.getHighlighting(); List<String> list = highlighting.get(solrDocument.get("id")).get("product_name"); //判断是否有高亮内容 if (null != list) { productName = list.get(0); } else { productName = (String) solrDocument.get("product_name"); }
System.out.println(productName); System.out.println(solrDocument.get("product_price")); System.out.println(solrDocument.get("product_catalog_name")); System.out.println(solrDocument.get("product_picture"));
} } |
使用Solr实现电商网站中商品信息搜索功能,可以根据关键字搜索商品信息,根据商品分类、价格过滤搜索结果,也可以根据价格进行排序,实现分页。
界面如下:
正在上传…重新上传取消
spring容器 |
表现层 springmvc |
Service层 |
Dao层 |
Solr索引库 |
mysql数据库 |
Tomcat
|
Solr服务 |
索引、搜索请求 |
架构分为:
自己开发的应用
获取搜索条件,并响应搜索结果到前台页面。
使用solrj来调用solr的服务进行索引和搜索
Service调用dao进行商品数据的维护时,要同步更新索引库(不实现)
对商品数据进行维护和查询
创建一个web工程导入jar包
正在上传…重新上传取消
功能:接收service层传递过来的参数,根据参数查询索引库,返回查询结果。
参数:SolrQuery对象
返回值:一个商品列表List<ProductModel>,还需要返回查询结果的总数量。
返回:ResultModel
方法定义:ResultModel queryProduct(SolrQuery query) throws Exception;
商品对象模型:
public class ProductModel {
// 商品编号
private String pid;
// 商品名称
private String name;
// 商品分类名称
private String catalog_name;
// 价格
private float price;
// 商品描述
private String description;
// 图片名称
private String picture;
}
返回值对象模型
public class ResultModel {
// 商品列表
private List<ProductModel> productList;
// 商品总数
private Long recordCount;
// 总页数
private int pageCount;
// 当前页
private int curPage;
}
@Repository public class ProductDaoImpl implements ProductDao {
@Autowired private SolrServer solrServer;
@Override public ResultModel queryProduct(SolrQuery query) throws Exception {
ResultModel resultModel = new ResultModel(); //根据query对象查询商品列表 QueryResponse queryResponse = solrServer.query(query); SolrDocumentList solrDocumentList = queryResponse.getResults(); //取查询结果的总数量 resultModel.setRecordCount(solrDocumentList.getNumFound()); List<ProductModel> productList = new ArrayList<>(); //遍历查询结果 for (SolrDocument solrDocument : solrDocumentList) { //取商品信息 ProductModel productModel = new ProductModel(); productModel.setPid((String) solrDocument.get("id")); //取高亮显示 String productName = ""; Map<String, Map<String, List<String>>> highlighting = queryResponse.getHighlighting(); List<String> list = highlighting.get(solrDocument.get("id")).get("product_name"); if (null != list) { productName = list.get(0); } else { productName = (String) solrDocument.get("product_name"); } productModel.setName(productName); productModel.setPrice((float) solrDocument.get("product_price")); productModel.setCatalog_name((String) solrDocument.get("product_catalog_name")); productModel.setPicture((String) solrDocument.get("product_picture")); //添加到商品列表 productList.add(productModel); } //商品列表添加到resultmodel中 resultModel.setProductList(productList); return resultModel; }
} |
功能:接收action传递过来的参数,根据参数拼装一个查询条件,调用dao层方法,查询商品列表。接收返回的商品列表和商品的总数量,根据每页显示的商品数量计算总页数。
参数:
1、查询条件:字符串
2、商品分类的过滤条件:商品的分类名称,字符串
3、商品价格区间:传递一个字符串,满足格式:“0-100、101-200、201-*”
4、排序条件:页面传递过来一个升序或者降序就可以,默认是价格排序。0:升序1:降序
5、分页信息:每页显示的记录条数创建一个常量60条。传递一个当前页码就可以了。
返回值:ResultModel
方法定义:ResultModel queryProduct(String queryString, String caltalog_name, String price, String sort, Integer page) throws Exception;
@Service public class ProductServiceImpl implements ProductService {
@Autowired private ProductDao productDao;
@Override public ResultModel queryProduct(String queryString, String caltalog_name, String price, String sort, Integer page) throws Exception { //拼装查询条件 SolrQuery query = new SolrQuery(); //查询条件 if (null != queryString && !"".equals(queryString)) { query.setQuery(queryString); } else { query.setQuery("*:*"); } //商品分类名称过滤 if (null != caltalog_name && !"".equals(caltalog_name)) { query.addFilterQuery("product_catalog_name:" + caltalog_name); } //价格区间过滤 if (null != price && !"".equals(price)) { String[] strings = price.split("-"); query.addFilterQuery("product_price:["+strings[0]+" TO "+strings[1]+"]"); } //排序条件 if ("1".equals(sort)) { query.setSort("product_price", ORDER.desc); } else { query.setSort("product_price", ORDER.asc); } //分页处理 if (null == page) { page = 1; } //start int start = (page-1) * Commons.PAGE_SIZE; query.setStart(start); query.setRows(Commons.PAGE_SIZE); //设置默认搜索域 query.set("df", "product_keywords"); //高亮设置 query.setHighlight(true); query.addHighlightField("product_name"); query.setHighlightSimplePre("<span style=\"color:red\">"); query.setHighlightSimplePost("</span>");
//查询商品列表 ResultModel resultModel = productDao.queryProduct(query); //计算总页数 long recordCount = resultModel.getRecordCount(); int pages = (int) (recordCount/Commons.PAGE_SIZE); if (recordCount % Commons.PAGE_SIZE > 0) { pages ++; } resultModel.setPageCount(pages); resultModel.setCurPage(page);
return resultModel; }
} |
功能:接收页面传递过来的参数调用service查询商品列表。将查询结果返回给jsp页面,还需要查询参数的回显。
参数:
1、查询条件:字符串
2、商品分类的过滤条件:商品的分类名称,字符串
3、商品价格区间:传递一个字符串,满足格式:“0-100、101-200、201-*”
4、排序条件:页面传递过来一个升序或者降序就可以,默认是价格排序。0:升序1:降序
5、分页信息:每页显示的记录条数创建一个常量60条。传递一个当前页码就可以了。
6、Model:相当于request。
返回结果:String类型,就是一个jsp的名称。
String queryProduct(String queryString, String caltalog_name, String price, String sort, Integer page, Model model) throws Exception;
@Controller public class ProductController {
@Autowired private ProductService productService;
@RequestMapping("/list") public String queryProduct(String queryString, String catalog_name, String price, String sort, Integer page, Model model) throws Exception { //查询商品列表 ResultModel resultModel = productService.queryProduct(queryString, catalog_name, price, sort, page); //列表传递给jsp model.addAttribute("result", resultModel); //参数回显 model.addAttribute("queryString", queryString); model.addAttribute("caltalog_name", catalog_name); model.addAttribute("price", price); model.addAttribute("sort", sort); model.addAttribute("page", page);
return "product_list"; } } |
参考资料。
文章浏览阅读502次。原标题:用Python帮小姐姐选口红,人人都是李佳琦 对于李佳琦,想必知道他的女生要远远多于男生,李佳琦最早由于直播向广大的网友们推荐口红,逐渐走红网络,被大家称作“口红一哥”。不可否认的是,李佳琦的直播能力确实很强,他能够抓住绝大多数人的心理,让大家喜欢看他的直播,看他直播推荐的口红适不适合自己,色号适合什么样子的妆容。为了提升效率,让自己的家人或者女友能够快速的挑选出合适自己妆容的口红色号,今..._获取口红品牌 及色号,色值api
文章浏览阅读3.6k次。简介awk命令的名称是取自三位创始人Alfred Aho 、Peter Weinberger 和 Brian Kernighan姓名的首字母,awk有自己的程序设计语言,设计简短的程序,读入文件,数据排序,处理数据,生成报表等功能。awk 通常用于文本处理和报表生成,最基本功能是在文件或者字符串中基于指定规则浏览和抽取信息,awk抽取信息后,才能进行其他文本操作。awk 通常以文件的一行为处理单位..._linux awk nr
文章浏览阅读1.3w次,点赞5次,收藏2次。在网上找了一个小时,一直没有头绪,因为上个星期还是好好的,最后看到一个大神的解答,只需要将防火墙关闭就好了.原本向测试功能的,却卡在了登录上.以此记录.另外好像还有种错误是电脑与手机连接的WiFi不同,也可以看看...._failed to connect to 192.168.88.218:80
文章浏览阅读1.9k次。利用MATLAB仿真多种多径衰落信道摘要:移动信道的多径传播引起的瑞利衰落,时延扩展以及伴随接收过程的多普勒频移使接受信号受到严重的衰落,阴影效应会是接受的的信号过弱而造成通信的中断:在信道中存在噪声和干扰,也会是接收信号失真而造成误码,所以通过仿真找到衰落的原因并采取一些信号处理技术来改善信号接收质量显得很重要,这里利用MATLAB对多径衰落信道的波形做一比较。一,多径衰落信道的特点关于多径衰落..._matlab多径衰落工具箱
文章浏览阅读1w次,点赞2次,收藏17次。Json简介:Json,全名 JavaScript Object Notation,是一种轻量级的数据交换格式。它基于 ECMAScript (w3c制定的js规范)的一个子集,采用完全独立于编程语言的文本格式来存储和表示数据。简洁和清晰的层次结构使得 JSON 成为理想的数据交换语言。 易于人阅读和编写,同时也易于机器解析和生成,并有效地提升网络传输效率。(来自百度百科)python关于json文_import json灰色
文章浏览阅读1.1k次,点赞6次,收藏3次。一、工作原理MHA工作原理总结为以下几条:(1) 从宕机崩溃的 master 保存二进制日志事件(binlog events);(2) 识别含有最新更新的 slave ;(3) 应用差异的中继日志(relay log) 到其他 slave ;(4) 应用从 master 保存的二进制日志事件(binlog events);(5) 通过Manager控制器提升一个 slave 为新 m..._mysql mha超详细教程
文章浏览阅读194次。一 java环境安装:1 安装JDK 参考链接地址:https://blog.csdn.net/qq_42815754/article/details/82968464注:有网情况下直接 yum 一键安装:yum -y list java(1)首先执行以下命令查看可安装的jdk版本(2)选择自己需要的jdk版本进行安装,比如这里安装1.8,执行以下命令:yum install -y java-1.8.0-openjdk-devel.x86_64(3)安装完之后,查看安装的jdk 版本,输入以下指令_linux的java主从策略是什么
文章浏览阅读104次。定义int 类型,由while实现A,B的连续输入,输出A+B的值按Ctrl Z结束循环。#include&lt;iostream&gt;using namespace std;int main(){ int A,B; while(cin&gt;&gt;A&gt;&gt;B) { cout&lt;&lt;A+B&lt;&_acm竞赛题 i 'm from mars
文章浏览阅读5.2k次。在需要给TextView的某句话添加点击事件的时候,我们一般会使用ClickableSpan来进行富文本编辑。与此同时我们还需要配合 textView.setMovementMethod(LinkMovementMethod.getInstance());方法才能使点击处理生效。但与此同时还会有一个问题:如果我们给父布局添加一个点击事件,需要在点击非链接的时候触发(例如RectclerV..._linkmovementmethod
文章浏览阅读1.1w次,点赞6次,收藏31次。JAVA实现压缩解压文件_java 解压zip
文章浏览阅读1.3w次,点赞7次,收藏21次。在Java 8 中使用Stream 例子对一个 Map 进行按照keys或者values排序.1. 快速入门 在java 8中按照此步骤对map进行排序.将 Map 转换为 Stream 对其进行排序 Collect and return a new LinkedHashMap (保持顺序)Map result = map.entrySet().stream() .sort..._java comparingbykey
文章浏览阅读497次。第一次参加GDKOI,考完感觉还可以,结果发现还是不行,有一些地方细节打错,有些失分严重,总结出以下几点:1.大模拟一定要注意,细节打挂就是没分,像T1就是一道大模拟题,马上切了,后面就没想着检查以下,导致有些地方挂掉了,用民间数据一测,才85分。2.十年OI一场空,不开longlonglong longlonglong见祖宗。今天的T2本来想用暴力水点分的,结果没想到longlong→intlong long\to intlonglong→int,40→040\to040→0。3.代码实现能力太差,_gdkoi