簡體   English   中英

在SQL中實現子字符串搜索的最佳方法是什么?

[英]What is the best way to implement a substring search in SQL?

我們這里有一個簡單的SQL問題。 在varchar列中,我們希望在字段中的任何位置搜索字符串。 實現此性能的最佳方法是什么? 顯然,一個指數在這里沒有任何幫助,其他任何技巧?

我們使用MySQL並擁有大約300萬條記錄。 我們需要每秒執行許多這些查詢,因此我們真正嘗試以最佳性能實現這些查詢。

到目前為止,最簡單的方法是:

Select * from table where column like '%search%'

我應該進一步指定該列實際上是一個長字符串,如“sadfasdfwerwe”,我必須在此列中搜索“asdf”。 所以他們不是句子而是試圖匹配他們中的一個詞 全文搜索仍然有用嗎?

查看我的演示文稿MySQL中的實用全文搜索

我比較了:

今天我將使用的是Apache Solr ,它將Lucene置於一項帶有一系列額外功能和工具的服務中。


你的評論:啊哈,好吧,不。 我提到的全文搜索功能都沒有幫助,因為它們都假設某種詞邊界

有效地找到任意子串的另一種方法是N-gram方法。 基本上,創建N個字母的所有可能序列的索引,並指向每個相應序列出現的字符串。 通常,這是通過N = 3或三元組來完成的 ,因為它是匹配較長子串並將索引保​​持在可管理大小之間的折衷點。

我不知道任何透明地支持N-gram索引的SQL數據庫,但您可以使用倒排索引自己設置它:

create table trigrams (
  trigram char(3) primary key
);

create table trigram_matches (
  trigram char(3),
  document_id int,
  primary key (trigram, document_id),
  foreign key (trigram) references trigrams(trigram),
  foreign key (document_id) references mytable(document_id)
);

現在用艱難的方式填充它:

insert into trigram_matches
  select t.trigram, d.document_id
  from trigrams t join mytable d
    on d.textcolumn like concat('%', t.trigram, '%');

當然這需要一段時間! 但是一旦完成,你可以更快地搜索:

select d.*
from mytable d join trigram_matches t
  on t.document_id = d.document_id
where t.trigram = 'abc'

當然你可以搜索超過三個字符的模式,但倒排索引仍然有助於縮小你的搜索范圍:

select d.*
from mytable d join trigram_matches t
  on t.document_id = d.document_id
where t.trigram = 'abc'
  and d.textcolumn like '%abcdef%';
  1. 如果您的語言不是英語,mysql全文搜索的質量(為此目的)很差

  2. trigram搜索為此任務提供了非常好的結果

  3. postgreSQL有三元組索引 ,很容易使用:)

  4. 但如果您需要在mysql中執行此操作,請嘗試使用Bill Karwin的改進版本的答案:

    -each trigram只存儲一次

    - 一個簡單的php類使用數據

     <?php /* # mysql table structure CREATE TABLE `trigram2content` ( `trigram_id` int NOT NULL REFERENCES trigrams(id), `content_type_id` int(11) NOT NULL, `record_id` int(11) NOT NULL, PRIMARY KEY (`content_type_id`,`trigram_id`,`record_id`) ); #each trigram is stored only once CREATE TABLE `trigrams` ( `id` int not null auto_increment, `token` varchar(3) NOT NULL, PRIMARY KEY (id), UNIQUE token(token) ) DEFAULT CHARSET=utf8 COLLATE=utf8_bin; SELECT count(*), record_id FROM trigrams t inner join trigram2content c ON t.id=c.trigram_id WHERE ( t.token IN ('loc','ock','ck ','blo',' bl', ' bu', 'bur', 'urn') AND c.content_type_id = 0 ) GROUP by record_id ORDER BY count(*) DESC limit 20; */ class trigram { private $dbLink; var $types = array( array(0, 'name'), array(1, 'city')); function trigram() { //connect to db $this->dbLink = mysql_connect("localhost", "username", "password"); if ($this->dbLink) mysql_select_db("dbname"); else mysql_error(); mysql_query("SET NAMES utf8;", $this->dbLink); } function get_type_value($type_name){ for($i=0; $i<count($this->types); $i++){ if($this->types[$i][1] == $type_name) return $this->types[$i][0]; } return ""; } function getNgrams($word, $n = 3) { $ngrams = array(); $len = mb_strlen($word, 'utf-8'); for($i = 0; $i < $len-($n-1); $i++) { $ngrams[] = mysql_real_escape_string(mb_substr($word, $i, $n, 'utf-8'), $this->dbLink); } return $ngrams; } /** input: array('hel', 'ell', 'llo', 'lo ', 'o B', ' Be', 'Bel', 'ell', 'llo', 'lo ', 'o ') output: array(1, 2, 3, 4, 5, 6, 7, 2, 3, 4, 8) */ private function getTrigramIds(&$t){ $u = array_unique($t); $q = "SELECT * FROM trigrams WHERE token IN ('" . implode("', '", $u) . "')"; $query = mysql_query($q, $this->dbLink); $n = mysql_num_rows($query); $ids = array(); //these trigrams are already in db, they have id $ok = array(); for ($i=0; $i<$n; $i++) { $row = mysql_fetch_array($query, MYSQL_ASSOC); $ok []= $row['token']; $ids[ $row['token'] ] = $row['id']; } $diff = array_diff($u, $ok); //these trigrams are not yet in the db foreach($diff as $n){ mysql_query("INSERT INTO trigrams (token) VALUES('$n')", $this->dbLink); $ids[$n]= mysql_insert_id(); } //so many ids than items (if a trigram occurs more times in input, then it will occur more times in output as well) $result = array(); foreach($t as $n){ $result[]= $ids[$n]; } return $result; } function insertData($id, $data, $type){ $t = $this->getNgrams($data); $id = intval($id); $type = $this->get_type_value($type); $tIds = $this->getTrigramIds($t); $q = "INSERT INTO trigram2content (trigram_id, content_type_id, record_id) VALUES "; $rows = array(); foreach($tIds as $n => $tid){ $rows[]= "($tid, $type, $id)"; } $q .= implode(", ", $rows); mysql_query($q, $this->dbLink); } function updateData($id, $data, $type){ mysql_query("DELETE FROM trigram2content WHERE record_id=".intval($id)." AND content_type_id=".$this->get_type_value($type), $this->dbLink); $this->insertData($id, $data, $type); } function search($str, $type){ $tri = $this->getNgrams($str); $max = count($tri); $q = "SELECT count(*), count(*)/$max as score, record_id FROM trigrams t inner join trigram2content c ON t.id=c.trigram_id WHERE ( t.token IN ('" . implode("', '", $tri) . "') AND c.content_type_id = ".$this->get_type_value($type)." ) GROUP by record_id HAVING score >= 0.6 ORDER BY count(*) DESC limit 20;"; $query = mysql_query($q, $this->dbLink); $n = mysql_num_rows($query); $result = array(); for ($i=0; $i<$n; $i++) { $row = mysql_fetch_array($query, MYSQL_ASSOC); $result[] = $row; } return $result; } }; 

和用法:

 $t = new trigram();

 $t->insertData(1, "hello bello", "name");
 $t->insertData(2, "hellllo Mammmma mia", "name");

  print_r($t->search("helo", "name"));

我想匹配整個單詞,查看FULLTEXT索引和MATCH() AGAINST() 當然,請加載數據庫服務器:根據您的特定需求緩存結果一段適當的時間。

首先,這可能是一個設計糟糕的表的問題,該表將分隔的字符串存儲在一個字段中,而不是正確設計以創建相關的表。 如果是這種情況,您應該修改您的設計。

如果您的字段包含長描述性文本(例如注釋字段)並且搜索始終是整個單詞,則可以進行全文搜索。

考慮一下,如果它是像Last_name這樣的普通字段,您是否可以要求您的用戶至少為您提供他們正在搜索的內容的第一個字符。

考慮首先進行完全匹配搜索,如果沒有返回結果,則僅執行通配符匹配。 如果您有可以提供完全匹配的用戶,這將有效。 我們用機場名稱搜索做了一次,如果他們輸入確切的名字,它會很快恢復,如果他們沒有,則會慢一些。

如果您只想搜索不是文本中某些字詞的字符串,那么您幾乎會遇到性能不佳的問題。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM