博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
一个朴素的搜索引擎实现
阅读量:2439 次
发布时间:2019-05-10

本文共 6070 字,大约阅读时间需要 20 分钟。

今天我们要使用 Lucene 来实现一个简单的搜索引擎,我们要使用上一节爬取的果壳网语料库来构建索引,然后在索引的基础上进行关键词查询。

上一节果壳网的语料库放在了 Redis 中如下,有一个有效的文章 ID 集合,对于每一篇文章都会有一个 hash 结构存储了它的标题和 HTML 内容。

valid_article_ids => set(article_id)article_${id} => hash(title=>${title}, html=>${html})

种不同功用的文件。构建索引的目标就是生成倒排索引,在本例中,会建立一个 title 标题的倒排索引和一个 html 内容的倒排索引,这是两个不同的倒排索引。

倒排索引就是分词词汇和文档 ID 列表的映射。如果是英文,那么就是一个个的英文单词,如果是中文,就需要中文分词词库来切分标题和内容来得到一个个的中文词语。这里的「文档 ID」 不是指文章 ID,而是 Lucene 内部的 Document 对象的唯一 ID。我们通过调用 Lucene 的 addDocument 方法添加进去的每一篇文章在 Lucene 内部都会有一个 Document 对象。

class InvertedIndex {  Map
 mappings; //  word => docIds}class Documents {  Map
 docs;  // docId => document}

在 Lucene 中,文档 ID 是一个 32bit 的「有符号整数」,按顺序添加进来的文档其 ID 也是连续递增的。因为它是 32bit,这也意味着 Lucene 的单个索引最多能存储 1<<31 -1 篇文档。文档 ID 之间可能会有空隙,因为 Lucene 的文档是支持删除操作的。

Elasticsearch 为了支持海量的文档存储,它内部对索引进行了分片存储(Sharding)。在内部实现中,它会使用到多个 Lucene 的索引来聚合处理。

好,下面我们来看看文档索引的构建是如何使用代码来完成的。首先导入 Lucene 依赖库

    
org.apache.lucene
    
lucene-core
    
8.2.0

    
com.hankcs.nlp
    
hanlp-lucene-plugin
    
1.1.6

// 中文分词分析器var analyser = new HanLPAnalyzer();// 指定索引的存储目录var directory = FSDirectory.open(Path.of("./guokr"));// 构造配置对象var config = new IndexWriterConfig(analyser);// 构造 IndexWriter 对象var indexWriter = new IndexWriter(directory, config);

var redis = new JedisPool();// 首先拿到所有的文章IDvar db = redis.getResource();var articleIds = db.smembers("valid_article_ids");db.close();// 挨个添加for(var id : articleIds) {    // 取文章的标题和内容    db = redis.getResource();    var key = String.format("article_%s", id);    var title = db.hget(key, "title");    var html = db.hget(key, "html");    var url = String.format("https://www.guokr.com/article/%s/", id);    db.close();    if(title != null && html != null) {        // 干掉内容中所有的 HTML 标签只剩下纯文本        var content = Jsoup.parse(html).text();        // 构造文档        var doc = new Document();        doc.add(new TextField("title", title, Field.Store.YES));        doc.add(new TextField("content", content, Field.Store.YES));        doc.add(new StoredField("url", url));        // 这里添加文档        indexWriter.addDocument(doc);    }}

indexWriter.close();directory.close();

上面的代码最关键的地方在于 Document 对象的构造

var doc = new Document();doc.add(new TextField("title", title, Field.Store.YES));doc.add(new TextField("content", content, Field.Store.YES));doc.add(new StoredField("url", url));

注意到 TextField 对象的最后一个参数指明是否存储字段的内容,如果这个字段设置为 Field.Store.NO,那么 Lucene 就不存储这个字段的值,但是还是会将这个值的文本进行切词后放入倒排索引中。在关键词查询阶段,我们可以根据关键词搜索到文档 ID,进一步得到这个文档的具体内容,但是文档的内容会缺失这个字段,因为 Lucene 没有存它。简单的说这个字段是隐身的,它在搜索时会起到作用,但是最终的搜索结果里却看不见它。之所以提供这个选项,很明显这是为了可以节约存储空间。

同时我们还注意到 url 字段使用了 StoreField,这是啥意思?它的意思和 Field.Store.NO 正好相反。它只存储字段的值,不参与检索,相当于文档的附加字段。通俗点讲它就是个「搭便车」字段 —— 老司机带带我。

现在让我们跑一跑这个程序,跑完之后打开 ./guokr 目录,看看里面都有些啥。

bash> ls -ltotal 13824-rw-r--r--  1 qianwenpin  staff   299B  9  4 14:22 _0.cfe-rw-r--r--  1 qianwenpin  staff   6.7M  9  4 14:22 _0.cfs-rw-r--r--  1 qianwenpin  staff   383B  9  4 14:22 _0.si-rw-r--r--  1 qianwenpin  staff   137B  9  4 14:22 segments_1-rw-r--r--  1 qianwenpin  staff     0B  9  4 14:22 write.lock

640?wx_fmt=png
图片

Lucene 虽然不允许多进程同时写,但是可以单进程写多进程读,也就是单写多读。好接下来我们开始尝试 Lucene 的读操作 —— 关键词查询。

查询操作需要构造一个关键对象 IndexSearcher,它的构造方式比 IndexWriter 简单很多。

var directory = FSDirectory.open(Path.of("./guokr"));var reader = DirectoryReader.open(directory);var searcher = new IndexSearcher(reader);

var query = new TermQuery(new Term("title", "动物"));

var hits = searcher.search(query, 10).scoreDocs;for(var hit : hits) {    var doc = searcher.doc(hit.doc);    System.out.printf("%s=>%s\n", doc.get("url"), doc.get("title"));}

reader.close();directory.close();

640?wx_fmt=png
图片

所有的文章标题里确实都有「动物」这个词。下面我们改变一下查询的输入,改为从内容查询,并且必须同时包含「动物」和 「世界」两个词汇。这是一个复合查询,复合查询需要使用到一个关键的类 BooleanQuery,它可以对多个子 Query 进行逻辑组合来融合查询结果。

var query1 = new TermQuery(new Term("content", "动物"));var query2 = new TermQuery(new Term("content", "世界"));var query = new BooleanQuery.Builder()        .add(query1, BooleanClause.Occur.MUST)        .add(query2, BooleanClause.Occur.MUST)        .build();

640?wx_fmt=png
图片

我们可以点开链接看看文章的内容进行验证一下。

640?wx_fmt=png
图片

下面我们继续改变查询条件,还是从内容查询,但是条件变为包含「动物」但是不得有「世界」这个词汇,估计满足这样条件的文章会非常多。

var query1 = new TermQuery(new Term("content", "动物"));var query2 = new TermQuery(new Term("content", "世界"));var query = new BooleanQuery.Builder()        .add(query1, BooleanClause.Occur.MUST)        .add(query2, BooleanClause.Occur.MUST_NOT)        .build();

var query1 = new TermQuery(new Term("content", "动物"));var query2 = new TermQuery(new Term("content", "经济"));var query = new BooleanQuery.Builder()        .add(query1, BooleanClause.Occur.SHOULD)        .add(query2, BooleanClause.Occur.SHOULD)        .build();

var query1 = new TermQuery(new Term("content", "动物"));var query2 = new TermQuery(new Term("content", "经济"));var query = new BooleanQuery.Builder()        .add(query1, BooleanClause.Occur.MUST)        .add(query2, BooleanClause.Occur.SHOULD)        .build();

前面提到 MUST 表示必须包含,MUST_NOT 表示必须不包含。但是如果将两个 MUST_NOT 组合你得到的将会是空查询。为什么会这样呢?

var query1 = new TermQuery(new Term("content", "动物"));var query2 = new TermQuery(new Term("content", "经济"));var query = new BooleanQuery.Builder()        .add(query1, BooleanClause.Occur.MUST_NOT)        .add(query2, BooleanClause.Occur.MUST_NOT)        .build();

var query1 = new TermQuery(new Term("content", "动物"));var query = new BooleanQuery.Builder()        .add(query1, BooleanClause.Occur.MUST_NOT)        .build();

最后我们再看一下 FILTER 选项的作用,它和 SHOULD 正好相反。SHOULD 不影响查询结果,但是会影响排序,而 FILTER 会影响查询结果但是不影响排序,它只起到过滤的作用,就好比数据库查询里的 Where 条件。

var query1 = new TermQuery(new Term("content", "动物"));var query2 = new TermQuery(new Term("content", "经济"));var query = new BooleanQuery.Builder()        .add(query1, BooleanClause.Occur.FILTER)        .add(query2, BooleanClause.Occur.FILTER)        .build();

关于 Lucene 查询语句的更多奥秘,在后面的文章中我们会继续深入探讨。

下面是有钱(有老钱)的字节跳动内推入口,找出自己心意的职位后,请勇于投递你的个人简历,内部系统的数据安全性做得非常到位,我是看不到你们的简历内容的,所以不必太担心个人隐私问题。北京、上海、深圳、杭州、成都、广州等城市的职位都有,Python、Java、Golang、算法、数据、分布式计算和存储的岗位也都有,各人发挥自己的搜商自行搜寻。请认真投递自己的简历,否则可能连面试的机会都没有。

640?wx_fmt=png

转载地址:http://rvbqb.baihongyu.com/

你可能感兴趣的文章
Fedora 显示设备配置工具介绍(转)
查看>>
FREEBSD 升级及优化全攻略(转)
查看>>
系统移民须知:Linux操作系统安装要点(转)
查看>>
在redhat系统中使用LVM(转)
查看>>
Gentoo 2005.1 完整的USE参数清单中文详解(转)
查看>>
如何在嵌入式Linux产品中做立体、覆盖产品生命期的调试 (5)
查看>>
手机最新触控技术
查看>>
Kubuntu 项目遭遇困难(转)
查看>>
kubuntu使用日记之 eva的配置使用(转)
查看>>
unix下几个有用的小shell脚本(转)
查看>>
QQ病毒的系列处理办法(转)
查看>>
source命令的一个妙用(转)
查看>>
亚洲开源航母呼之欲出 目标瞄向Novell与红帽(转)
查看>>
正版化:水到渠成?预装Windows对Linux无打压(转)
查看>>
Red Hat并购JBoss 谁将受创?(转)
查看>>
基于IBM大型主机,Linux开辟意大利旅游新天地(转)
查看>>
一些Linux试题(经典!!)(转)
查看>>
优化MySQL数据库性能的八大“妙手”(转)
查看>>
小心:谁动了你的注册表(转)
查看>>
Unix/BSD/Linux的口令机制初探(转)
查看>>