[給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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章