[给JavaScript一个机会] 10min从0写一个简单搜索引擎

自从写完Android运行NodeJS程序的app NodeBase,感觉还蛮实用的,无论是在手机上编辑小文件,还是互相共享文件,都方便许多。于是开始深邃的JavaScript之旅,让世界JavaScript化吧。自从看了一则node_modules比黑洞还能扭曲空间的笑话,还是尽量减少使用dependecy比较好,看来后面还得写个分析器合并JS小文件…

因为想有个自己的搜索引擎,一直没有时间去玩。前阵子闲了一些,开始设计搜索引擎,准备把自己遇见的东西都塞进去成为知识库,然后手机打开app就能搜索,多开心呀。

眼下最常用的开源搜索引擎几乎是被Lucene统治着。虽然编译Java的OpenJDK也就30min的事情,把java运行在Android上也不会耗太长时间,但是JVM这个高耗内存的虚拟机个人是不太喜欢,在移动设备上的资源消耗不敢想象。Python社区有大神已经参照Lucene实现了whoosh,要么我们也来个JavaScript版本吧。先来个prototype 10min热个手。

TL;DR 10min-Written Search Engine Source Code in JavaScript

2. 实用的测试

在10min内实现了简单的搜索引擎,我们来进行一个简单的测试。github: LAZAC 是一个代码静态分析工具。初衷是实现输入一段log,然后输出每行log可能来源于哪些代码文件,加速找bug。本来是想用elastic search,但是java太笨重,不想使用,尤其是要切换到手机上。

https://github.com/dna2github/lazac/blob/master/lazac/lazac.js 中,我们只要找到文件夹里的所有代码文件,然后解析出string,将这些string存入引擎:

      i_string_index.addDocument(engine, {
         index: token_index,      // parsed token index
         filename: repo_filename, // string in file
         line_no: line_no,        // string location in file
         value: token.token       // string value
      }, i_string_index.tokenize(token.token));

在搜索的时候

   // token化query的string
   let tokens = i_string_index.tokenize(line);
   // 靠倒排索引找到string
   let doc_set = i_string_index.search(engine, tokens);
   // 为所有找到的string打分并排序,取topN
   score(engine, tokens, doc_set);
   doc_set = Object.keys(doc_set).map((doc_id) => {
      return {
         id: doc_id,
         score: doc_set[doc_id],
         meta: engine.document[doc_id].meta
      };
   }).sort((x, y) => y.score - x.score).slice(0, topN);

   // 对topN的string再打分,取top 3
   doc_set.forEach((doc) => {
      // if string not stored in doc meta
      // get_string(doc, token_filename_map);
      let rate = {value: 0, dense: 0};
      if (line && doc.meta.value) {
         rate = lcs(doc.meta.value, line);
      }
      doc.dense = rate.dense;
      doc.score *= rate.value / (line.length + doc.meta.value.length);
   });
   doc_set = doc_set.sort((x, y) => y.score - x.score).slice(0, 3);

目前的搜索引擎运转还不错,缺点是慢,加上抗噪能力比较差。所以一般log还需要grok预处理。至此,搜索引擎的局部篇初步完成,后面我们再说搜索引擎如何把握全局,将文档更好得送上top 1。

1. 设计与实现

其实简单的搜索引擎并不复杂,然后把简单慢慢优化,就慢慢完善了。二话不说,先模仿Lucene core的源代码,建几个文件夹 analysis index search score codec storage。storage负责底层文件操作,codec负责读写引擎,analysis用来分词,index用来往引擎里添加文档,search用来查询匹配的文档,score将查询出来的文档进行打分,整条线路还是蛮清晰的。

Lucene大量使用了特殊设计的数据结构来提高性能并减少存储。所以我们暂时抛开这些,就直接用JSON文件存数据好了。设置三个文件dictionary.json document.json reverse_index.json。看文件名就知道,dictionary.json存储单词,顺便再存储个单词在所有出现该单词文档的频率(DF)吧;document.json用来存储文档的meta和词频(TF)统计,reverse_index.json就是经典的倒排索引啦,单词id对应一列文档id。

分词我们暂时只支持英文吧,比如hello world => [hello, world],省去找词根。到index里,就是把这些词先加入dictionary,然后计算词频和meta一起加入document集合,最后将词和文档建立倒排索引。search很简单,找到单词倒排索引指向的文档,合并集合。之后用score给文档们打分就好了,关于最简单的TF.IDF公式,网上一大堆讲解,引用下Wikipedia的吧 TF.IDFtest.js展示了如何在内存中建立一个搜索引擎。可以用i_codec.writeEngine(engine, '/path')保存内存里的引擎,然后i_codec.readEngine('/path')复用。

来看看片段代码吧,都在注释里了:

function addDocument(engine, meta, tokens) {
   // 一个新的doc对象
   let docobj = {
      // 得到一个doc id
      id: engine.doc_auto_id ++,
      // doc的meta信息,可以存任何想存的东西,比如对应的文件名
      meta: meta,
      // 词频统计
      tf_vector: {}
   };
   tokens.forEach((term) => {
      let termobj = engine.dictionary[engine.term_id_map[term]];
      if (!termobj) {
         // 一个新的词对象
         termobj = {
            id: engine.term_auto_id ++,
            term: term,
            df: 0
         };
         engine.dictionary[termobj.id] = termobj;
         // 词转化为词id的cache 
         engine.term_id_map[term] = termobj.id;
      }
      doc里该词的词频增加
      if (!docobj.tf_vector[termobj.id]) docobj.tf_vector[termobj.id] = 0;
      docobj.tf_vector[termobj.id] ++;
   });
   Object.keys(docobj.tf_vector).forEach((term_id) => {
      // 对于doc里出现的词,词的文档出现频率增加
      engine.dictionary[term_id].df ++;
      // 倒索引
      if (!engine.reverse_index[term_id]) engine.reverse_index[term_id] = [];
      engine.reverse_index[term_id].push(docobj.id);
   });
   engine.document[docobj.id] = docobj;
   return docobj;
}

接下去就是构建graph,PageRank也好,知识图也罢,玩乐去 …

J.Y.Liu
2018.03.09

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值