system: [SYSTEM]

user: 3. 系统设计与实现 3.1. 系统总体架构 搜索的基础是数据。整个系统先利用Python从相关网站爬取数据，保存到数据库中；由于Lucene是Java语言实现的工具包，接下来使用Java连接到数据库；在获取到数据库连接后，利用Lucene开始创建索引并保存到本地磁盘或服务器中；最后用户输入关键字进行检索，并按照一定的顺序将检索结果输出。整体流程如图1所示。 图1. 流程图 3.2. 爬虫的设计与实现 因为Lucene并没有提供数据的收集功能，在不借用solr这类二次封装的框架或者scrapy爬虫框架的情况下，需要使用Python编写的爬虫来从互联网获取想要的数据。 1) 确定数据来源 本文使用万方数据库、知网数据库等作为数据源，通过检索关键词石墨烯查询到全部文献信息并爬取。经过统计，十万以上数量级的数据可以作为搜索引擎检索和数据库检索速度比较的数据源。 2) 网站分析 以万方数据库为例，分析万方数据库的网络请求过程：新版网站在搜索框输入“石墨烯”并搜索后，就会展示相关检索结果，通过点击按钮实现数据翻页。进入浏览器F12开发者模式，分析网络请求，发现请求是XHR类GET请求，也就是异步GET请求。所以接下来只要分析异步请求的参数，并利用模拟器发送请求并获取服务器响应即可。如图2所示。 图2. 新版万方数据库异步请求分析 经过实际运行发现，新版网站暴露的接口最多只能爬取100 * 50条即5000数据，数量级太低，没有办法作为搜索引擎数据集，也没有办法作为数据库和搜索引擎比较的样本。旧版网站的搜索接口比较简单，只需要构建http://s.wanfangdata.com.cn/Paper.aspx?q=石墨烯&f=top&p=2这样的URL就可以获取到全部数据。如图3所示。 图3. 旧版万方数据库请求分析 3) 参数分析并编写代码 通过分析GET请求URL可以分析出关键参数，通过动态赋值既可以实现翻页等功能。利用selenium自动化测试工具调动Chrome浏览器访问构建好的URL并获取到服务器响应结果，利用BeautifulSoup开源库进行解析，并获取到需要的相关数据。再利用pymysql连接到本地MySQL数据库并保存。关键Python代码如下。 browser.get(get_url) wait = WebDriverWait(browser， 5) soup = BeautifulSoup(browser.page_source， lxml) 4) 数据处理 虽然网络提供了大量数据，但是不免其中有大量的重复数据和无关数据。在爬虫的实际运行中，会发现通过在网站上检索出的数据会出现不间断的重复、以及文献摘要部分的缺失。因此，从两个方面对数据进行处理。一方面是在爬虫运行过程中，如果解析出的数据格式不正确或者为空，则跳过这条数据的写入；另一个方面，是创建数据库时，设立唯一索引从源头保证不写入重复数据，同时在写入数据库时，通过唯一索引保证重复数据替换而不是新增。数据表创建语句如下： CREATE TABLE `spiders`.`wanfang_spiders`( `doc_id` int(0) NOT NULL AUTO_INCREMENT， `doc_title` varchar(255) NOT NULL， `doc_summary` varchar(2000) NULL， `doc_date` datetime(0) NOT NULL， `doc_papers` varchar(255) NOT NULL， `doc_readtime` bigint(0) NOT NULL， `doc_clicktime` bigint(0) NOT NULL， PRIMARY KEY (`doc_id`)， UNIQUE INDEX(`doc_title`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8; 在插入数据时，利用唯一索引保证数据不重复，插入的SQL语句如下：INSERT INTO wanfang_spiders (***) VALUES (***) ON DUPLICATE KEY UPDATE doc_title=doc_title，这样就会保证当数据重复时，新插入的数据替代旧数据的内容，同时自增id不会变化。 3.3. 搜索引擎设计与实现 Lucene工具包提供了创建索引、保存索引、检索索引等功能。 3.3.1. 索引种类 在搜索引擎中，目前主要使用的是正排索引(forward index)和倒排索引(inverted index)这两种索引 [ 2 ] 。 正排索引：以本文研究为例，使用文档名作为key，将文档中出现的词作为对应的value保存下来。如此一来，如果两篇不同的文章中某一个词都出现多次，就会造成数据的冗余。优点是易于维护，缺点是搜索时间过长。 倒排索引：与正排索引相反，存在一个词典和倒排列表。所有出现的词保存在词典中，倒排列表中保存的是每一个词所在的文档和位置。与正排索引相反，优点是搜索时间短，但是不易维护。 综上所述，针对需要大量数据的搜索引擎设计，需要使用倒排索引。另一个原因是，索引的更新需要配合文献库的更新，而且提供增量更新功能，维护问题并不明显 [ 3 ] 。 3.3.2. 数据导入 利用Java连接MySQL并读取数据，并没有采用业界流行的hibernate或者mybatis等框架，而是使用原生方法。一是考虑到开源框架更新较快，某些API可能会经常变动导致旧系统崩溃；二是由于网络政策，连接到远程仓库可能会出现问题导致jar包无法引用从而导致旧系统崩溃；三是bug修复更新发布时，如果将jar包保存在服务器上，则需要更新服务端资源；如果使用maven构建，则会遇到第二点涉及的问题。因此选用Java内嵌提供的sql包实现。关键代码如下所示。 conn = DriverManager.getConnection(DATABASE_NAME， username， password); stat = conn.createStatement(); set = stat.executeQuery(sql); 3.3.3. 分词器设计 如前文所述，使用IKAnalyzer，但由于版本不再维护，针对Lucen7.5.0版本，需要重写分词器。主要涉及到Analyzer类(分析器)和Tokenizer类(分词器)。分析器关键代码如下。 @Override protected TokenStreamComponents createComponents(String s) { IKTokenizer4lucene750 ik = new IKTokenizer4lucene750(this.useSmart); return new TokenStreamComponents(ik); } 重写IKAnalyzer的关键在于分词器，其基于IKSegmenter类(具体分词实现类)，其内部会加载自定义词典和停止词典，并据此进行分词功能。关键代码如下。 Lexeme l = segmenter.next(); if (l != null){ xxxTermAttribute.xxx(); return true; } 3.3.4. 创建索引 主要涉及到Lucene的IndexWriter用于写入索引和Document用于保存索引这两个类。Document可以类比于MySQL中数据表的行，TextField可以类比于数据表的字段。最后再将索引文件保存到本地磁盘或者服务器上 [ 4 ] 。关键代码如下。 luceneDoc.add(titleFiled); docList.add(luceneDoc); IndexWriter writer = new IndexWriter(directory， iwConfig); writer.addDocument(docList.get(i)); 3.3.5. 创建查询 索引保存完毕之后，根据用户输入字符串进行检索。主要涉及到QueryParser和Query类，用于解析和查询功能 [ 5 ] [ 6 ] 。关键代码如下。 Query query = new QueryParser(title， analyzer).parse(queryWord); IndexSearcher s = new IndexSearcher(DirectoryReader.open(directory)); TopDocs topDocs = s.search(query， 10);//至多搜索10条 至此，所有检索出来的数据都保存到数组中。操作数据就可以获取到相应数据。 3.3.6. 评分机制 Lucene自身内置的评分算法是TF-IDF算法，也就是词频算法，TF指词频(Term Frequency)，某一个词在所有分词中出现的频率；IDF指逆文本频率指数(Inverse Document Frequency)，即所有文本数除以包含特定词的文本数的以10为底的对数。最后针对每个文档，将TF-IDF计算的结果相加，就是该文档最后的评分。 针对文献的评分，利用默认的评分机制效果并不理想。因为一是发表在不同等级期刊上的文献无法区别，所以需要期刊种类相关；二是一般新发表的文献的需求更大，所以需要发表时间相关。所以需要自定义实现Lucene的评分机制。先了解一下Lucene自身的评分机制，如式-1所示 [ 7 ] [ 8 ] 。 s c o r e ( q ， d ) = c o o r d ( q ， d ) * q u e r y N o r m ( q ) * ∑ t i n q ( t f ( t i n d ) * i d f ( t ) 2 * t . g e t B o o s t ( ) * n o r m ( t ， d ) ) (式-1) c o o r d ( q ， d ) 评分因子，用于计算检索命中的比例。诸如检索“石墨烯的氧化还原法”，经过分词器之后变成“石墨烯”和“氧化还原法”。另有一个文档“在石墨烯制备中的化学气相沉积法的改进”，那么就只有“石墨烯”命中，而检索条件一共有2个元素，所以评分因子为1/2，即0.5； q u e r y N o r m ( q ) 查询的标准查询，对所有的文档都是一致的，可以认为是一个常数值； f ( t i n d ) 指项t在文档d中出现的次数。就如a的例子中描述，石墨烯在文档中出现的次数为1，那么最后计算结果就是 1 ； i d f ( t ) 反转文档频率，最后的结果为 t lg ( a l l _ doc i n d e x _ doc ) + 1 ，其中all_doc为所有文档，index_doc为检索命中的文档数； t . g e t B o o s t 某一个查询项的加权系数，针对某一类文献，可以把这一类文献关键词的系数设置得更大一点； n o r m ( t ， d ) 长度相关的加权因子，目的是为了将匹配结果相等的文档中，保证更短的在前面，这样更符合“完全匹配”原则 [ 9 ] 。 接下来是将额外的时间、期刊等参数添加到分数计算之中。因为时间、期刊等元素并不在搜索关键词中出现，所以并不能通过更改上述公式来达到调参的目的，需要重写Lucene的Custom Score Query和Custom Score Provider两类来将非检索参数转化为分数添加进最终计算分数中。针对期刊的系数调整，可以简单地根据SCI/EI/中文核心/CSCD等条件进行常数操作，根据Lucene计算出的原始分数动态调整；针对时间的系数调整，距离搜索日期越近，相应的系数影响越大，即基于时间衰减。同时又需要考虑到时间与文献质量并不具有强关联性，尤其是一些经典文献发表时间久远。因而基于时间的系数调整需要具备如下特征：既能保证最新的相关文献排在推荐列表的前部，又需要保证经典文献不会因为时间因素而排在推荐列表的后部。董立岩等人的研究中 [ 10 ] ，通过融合艾宾浩斯曲线的方式，将时间因素添加到协同过滤推荐算法中。本研究借用这种思路，将艾宾浩斯曲线的计算结果作为系数，还需要获取到每个文献的阅读次数和停留时间以筛选出经典文献。最终的计算公式如式-2所示。 f i n a l _ s c o r e = l u c e n e _ s c o r e * p a p e r s _ s c o r e * d a t e _ s c o r e p a p e r s _ s c o r e = e p a p e r s _ i n d e x p a p e r s _ b a s e * ( l u c e n e _ s c o r e + 1 ) d a t e _ s c o r e = ln ( E b b i n g h a u s ( p a p e r s _ d a t e ， n o w _ d a t e ) * p a p e r s _ h o t ) (式-2)