天天看點

MYSQ LFULLTEXT索引實作全文搜尋使用FULLTEXT索引MySQL中文全文檢索解決方案

使用FULLTEXT索引

MySQL5.6.10作為GA版本釋出,内置了對Innodb的全文索引支援。5.6.4之前版本的MySQL隻有MyISAM支援全文搜尋。

MySQL具備全文搜尋的能力。全文搜尋引擎可以在不使用模闆比對操作的情況下查找單詞或短語。全文搜尋分為3種模式,如下所示。

自然語言模式。把搜尋字元串解釋為一系列單詞并查找包含這些單詞的資料行。

布爾模式。把搜尋字元串解釋為一系列單詞,但允許使用一些操作符字元來"修飾"這些單詞以表明特定的要求,如某給定單詞必須出現(或不出現)在比對資料行裡,某個資料行必須包含一個精确的短語,等等。

查詢擴充模式。這種搜尋分兩階段進行。第一階段是自然語言搜尋,第二階段使用原來的搜尋字元串加上在第一次搜尋中找到的相關度最高的比對資料行再進行一次搜尋。這擴大了搜尋範圍,它可以把與原來的搜尋字元串相關、但用原來的搜尋字元串比對不到的資料行也找出來。

要想對某個資料表進行全文搜尋,必須事先為它建立一個FULLTEXT索引,這種索引具有以下特點。

全文搜尋的基礎是FULLTEXT索引,這種索引隻能在MyISAM資料表裡建立。FULLTEXT索引隻能由CHAR、VARCHAR和TEXT這幾種類型的資料列構成。

全文搜尋将忽略"常見的"單詞,而"常見"在這裡的含義是"至少在一半的資料行裡出現過"。千萬不要忘記這個特點,尤其是在你準備對資料表進行全文搜尋測試時。你至少要在測試資料表裡插入3個資料行。如果那個資料表隻有一個或兩個資料行,它裡面的每個單詞将至少有50%的出現幾率,是以對它進行全文搜尋将不會有任何結果。

全文搜尋還将忽略一些常用單詞,如"the"、"after"和"other"等,這些單詞被稱為"休止單詞",MySQL在進行全文搜尋時總是會忽略它們。

太短的單詞也将被忽略。在預設的情況下,"太短"指少于4個字元。但你可以通過重新配置伺服器的辦法把這個最小長度設定為其他值。

全文搜尋對"單詞"的定義是:由字母、數字、撇号(如"it's"中的"'")和下劃線字元構成的字元序列。這意味着字元串"full-blood"将被解釋為包含"full"和"blood"兩個單詞。全文搜尋比對整個單詞,而不是單詞的一部分。隻要在一個資料行裡找到了搜尋字元串裡的任何單詞,FULLTEXT引擎就會認為這個資料行與搜尋字元串是比對的。在此基礎上,布爾式全文搜尋還允許你加上一些額外的要求,比如說,所有的單詞都必須出現(不論順序)才認為是比對,(在搜尋一條短語時)單詞順序必須與在搜尋字元串裡列出的一緻,等等。布爾搜尋還可以用來比對不包含特定單詞的資料行,或者通過添加一個通配符來比對以一個給定字首開頭的所有單詞。

FULLTEXT索引可以為一個或多個資料列而建立。如果它涉及多個資料列,基于該索引的搜尋将在所有資料列上同時進行。反過來說,在進行全文搜尋時,你給出的資料列清單必須和某個FULLTEXT索引所比對的那些資料列精确比對。比如說,如果你需要分别搜尋col1、col2以及"col1和col2",你将需要建立3個索引:col1和col2各有一個,"col1和col2"有一個。

接下來的例子示範了全文搜尋的使用方法。我們将先建立幾個FULLTEXT索引,然後用MATCH操作符對它們進行一些查詢。用來建立資料表并把一些樣闆資料加載到其中的腳本可以在sampdb資料庫的fulltext子目錄裡找到。

FULLTEXT索引的具體建立過程與其他索引大同小異。你可以在建立一個新資料表的同時在CREATE TABLE語句裡定義它們,也可以在資料表被建立出來以後再用ALTER TABLE或CREATE INDEX語句添加它們。因為FULLTEXT索引要求你必須使用MyISAM資料表,是以如果你正在建立一個需要使用全文搜尋的MyISAM資料表,不妨利用一下MyISAM存儲引擎的這個特點來加快點兒速度。在加載資料時,先填充資料表、再添加索引的辦法要比先建立索引再加載資料的辦法快得多。假設你有一個名為apothegm.txt的資料檔案,其内容是一些名人名言:

  1. Aeschylus              Time as he grows old teaches many lessons  
  2.  Alexander Graham Bell  Mr. Watson, come here. I want you!  
  3.  Benjamin Franklin      It is hard for an empty bag to stand upright   
  4. Benjamin Franklin      Little strokes fell great oaks  
  5.  Benjamin Franklin      Remember that time is money  
  6.  Miguel de Cervantes    Bell, book, and candle  
  7.  Proverbs 15:1          A soft answer turneth away wrath  
  8.  Theodore Roosevelt     Speak softly and carry a big stick  
  9.  William Shakespeare    But, soft! what light through yonder window breaks?  
  10.  Robert Burton          I light my candle from their torches.  

如果按"名人"、"名言"和"名人加名言"進行搜尋,你需要建立3個FULLTEXT索引:兩個資料列各有一個,它們加起來有一個。下面這些語句将建立、填充一個名為apothegm的資料表,并為它編制索引:

  1. CREATE TABLE apothegm (attribution VARCHAR(40), phrase TEXT) ENGINE = MyISAM;  
  2.  LOAD DATA LOCAL INFILE 'apothegm.txt' INTO TABLE apothegm;  
  3.  ALTER TABLE apothegm  
  4.    ADD FULLTEXT (phrase),  
  5.    ADD FULLTEXT (attribution),  
  6.    ADD FULLTEXT (phrase, attribution);  

MySQL中文全文檢索解決方案

在PHP+MySQL構架的網站中,大資料量的全文檢索一般都會用到MySQL的FULLTEXT全文索引,通過SELECT...MATCH...AGAINST語句來進行查找。

迄今為止,MySQL對中文全文索引無法正确支援,MySQL是不會識别中文詞語的。參照MySQL識别英文單詞機制,要建立中文全文索引,暫時的解決方案隻有手動将中文分詞(以空格的形式将中文詞語分開),來将中文轉換成MySQL認識的語言。

如今網上對于中文分詞的解決方案有很多,有基于MySQL插件的,有談論算法思想的。基于插件(如海量科技的MySQL--LinuxX86-Chinese+,hightman開發的mysql--ft-hightman)的方式主要通過對MySQL資料庫安裝一個别人提供好的插件,在建FULLTEXT索引的字段時後面加上WITH PARSER ×××(大多都是這樣)的形式。而基于算法思想的則大部分工作都要自己完成,但他們的大體思想都差不多:

1. 對插入的要建全文索引的中文資料進行分詞;

2. 将原始資料和分詞後的資料都存入資料庫中,并以某種方式建立聯系;

3. 在存儲分詞資料的字段上建立FULLTEXT索引;

4. 查詢時以SELECT...MATCH...AGAINST的方式在分詞字段上搜尋,将搜到的行通過前面建立的聯系找到原始資料行并傳回。

而我們在讨論解決方案時,考慮到使用開源插件的話可控性比價差,而且插件會對MySQL做一些改變,我們決定将分詞存儲的工作自己寫代碼完成,這樣雖然工作量加大,但以後的維護成本卻降低了很多。下面我們來看下大體實作。

一、首先,先建立資料庫

要注意的是隻有MyISAM表類型才能支援FULLTEXT,MyISAM與InnoDB各有優劣,我們決定将原始資料與索引分表存儲,原始資料存入InnoDB表,同時建立MyISAM表存入作為檢索的字段和用于關聯的字段id。這裡我建立了兩張表questions和questions_idx;其中,title和detail是要建立全文索引的字段,而id則是建立兩張表的聯系。值得注意的是,MyISAM是不支援事務和外鍵的,是以對于兩張表資料的同步還要靠額外代碼邏輯來實作。

還有就是索引字段有可能需要比原始資料更大的空間,這裡我配置設定了2倍(這個是我随意想的,有可能需要更多)。

二、接着,讨論中文分詞

網上流傳的分詞方法有很多,主要有基于算法的(比如二進制分詞算法,位元組交叉切分算法)和基于詞庫的。基于算法是不必要維護詞庫的,而詞庫法則必須維護詞庫,有可能跟不上詞彙的發展。實際上現在很多著名的搜尋引擎都使用了多種分詞的辦法,比如“正向最大比對”+“逆向最大比對”,基于統計學的新詞識别,自動維護詞庫等技術。

我們采用的是基于詞庫的,并且使用了hightman的scws的php擴充子產品方式。參考http://www.ftphp.com/scws/ 。這個開源分詞系統這裡不多說,總之利用的是詞庫來分詞,而且最新版的是支援自定義詞庫的,這對于我們的内部網站來說,詞庫的維護問題變得簡單了,因為新增詞彙不會像外部網站那麼大,也不需要維護太多。

下面定義了類CWS,方法get_idx将輸入中文資料,

輸出分詞并編碼後的資料:

class CWS {
  //對輸入字元串使用scws進行分詞,去重複項,進行urlencode編碼
  public static function get_idx($input) {
    //--------分詞-----------
    $so = scws_new();
    $so->set_charset('utf8');
    $so->set_ignore(true);
    $output = '';
    $so->send_text($input);
    while ($tmp = $so->get_result()) {
      foreach ($tmp as $item) {
        $output .= $item['word'] . ' ';
      }
    }
    $so->close();
    //--------編碼-----------
    $data = array_filter(explode(" ",$output)); //删除數組空項
    $data = array_flip(array_flip($data));      //删除重複項
    //對分詞結果進行urlcode編碼
    foreach ($data as $ss) {
      if (strlen($ss) > 1) {
        $data_code .= str_replace('%','',urlencode($ss)) . ' ';
      }
    }
    return $data_code;
  }
}
           

對于scws的那段代碼請參照scws使用手冊。

而對于為什麼要進行編碼,網上大都解釋是:MySQL系統自變量規定了全文檢索被編入索引單詞的最小長度和最大長度(ft_min_word_len和ft_max_word_len),預設的最小值為4個字元,預設的最大值取決于使用的 MySQL 版本。 參考http://dev.mysql.com/doc/refman/4.1/en/server-system-variables.html#sysvar_ft_min_word_len 

為了不改變這個預設值同時也是兼考慮這個值對于英文的意義,則需要通過編碼将中文詞變長。

而對于編碼,網上流傳的方式也有很多,比如base64編碼、urlencode編碼等,甚至還有漢字轉拼音。這裡我嘗試了urlencode編碼,需要注意的是urlencode會産生很多’%’,這在MySQL中是通配符,要去掉。

三、插入資料

插入資料的時候我們就要調用以上的函數了:

public function add($title,$detail, $askerid) {
  $date= NOW;
  $sql= 'INSERT INTO questions '.
      '(title, detail, askerid, date) '.
      'values '.
      "('$title', '$detail', $askerid, '$date')";
  $result= $this->db->query($sql);
  $id= $this->db->lastId();
  //scws分詞存儲
  $title_idx= CWS::get_idx($title);
  //使用strip_tags函數過濾掉富文本編輯器産生的标簽
  $detail_idx= CWS::get_idx(strip_tags($detail));
  $sql= 'INSERT INTO questions_idx '.
      '(id, title, detail) '.
      'values '.
      "($id, '$title_idx', '$detail_idx')";
  $this->db->query($sql);
  return$result;
}
           

 四、搜尋資料

搜尋的時候,從輸入框擷取問題,當然如果你輸入的是關鍵詞,那就直接搜就是了,但我們是要使用者輸入的一個完整的問題,是以也要分詞,否則MySQL還是檢索不到:

public function search($word,$limit) {
    $word= CWS::get_idx($word);
    $sql= "SELECT A.title, A.detail, askerid, date ".
           "FROM questions as A, questions_idx as B ".
           "WHERE A.id = B.id ".
           "AND MATCH (B.title, B.detail) AGAINST ('$word')";
    $result= $this->db->getAll($sql,$limit);
    return$result;
}
           

相關資料:

SCWS:http://www.ftphp.com/scws/download.php

繼續閱讀