用于250K +字符串的通配符搜索的快速(呃)方法

时间:2012-04-11 21:59:09

标签: sql ruby database wildcard

我在MySQL数据库中有一个英文词典,只有超过250K条目,而我正在使用一个简单的ruby前端在字符串的开头用通配符搜索它。到目前为止,我一直在这样做:

SELECT * FROM words WHERE word LIKE '_e__o'

甚至

SELECT * FROM words WHERE word LIKE '____s'

我总是知道单词的确切长度,但除了单个字符之外的所有字符都可能是未知的。

这比糖蜜慢,比没有前导通配符的类似查询慢15倍,因为无法使用该列的索引。

我尝试了一些方法来缩小搜索范围。例如,我添加了26个附加列,其中包含每个单词的单个字母计数,并使用这些列首先缩小搜索范围。我也试过缩小字长。由于领先通配符搜索固有的低效率,这些方法几乎没有差别。我已经尝试了REGEXP语句,这个语句甚至更慢。

SQLite和PostgreSQL与MySQL一样有限,尽管我对NoSQL系统的经验有限,但我的研究给我的印象是他们在可扩展性方面表现优异,而不是我所需的那种性能。

我的问题是,我应该在哪里寻找解决方案?我应该继续尝试找到一种方法来优化我的查询或添加可以缩小我的潜在记录集的补充列吗?是否有专门设计用于实现快速通配符搜索的系统?

8 个答案:

答案 0 :(得分:5)

使用PostgreSQL 9.1和pg_trgm扩展,您可以创建可用于您所描述的类似条件的索引。

有关示例,请参阅此处:http://www.depesz.com/2011/02/19/waiting-for-9-1-faster-likeilike/

我在使用LIKE '____1'的300k行的表上验证了它,它确实使用了这样的索引。计算该表中的行数(在旧笔记本电脑上)需要大约120ms。有趣的是,表达式LIKE 'd___1'并不快,它的速度大致相同。

它还取决于搜索词中的字符数,它的长度,我可以说的速度越慢。

如果性能可以接受,您需要检查数据。

答案 1 :(得分:1)

我假设最初用于插入单词和设置索引的时间是无关紧要的。此外,您不会经常对单词列表进行更新,因此它基本上是静态数据。

您可以尝试这样的方法: -

  • 由于您始终知道单词长度,因此请创建一个包含长度为1的单词,另一个单词长度为2的表格等的表格。
  • 执行查询时,请根据字长选择适当的表格。它仍然需要对该表进行全面扫描。

如果您允许RDBMS使用它,那么使用单个表和分区的字长会更好。

如果仍然不够快,你可以按长度和已知字母进一步拆分。例如,您可以有一个表格,列出包含“Z”的所有8个字母单词。

当您查询时,您知道您有一个包含“E”和“Z”的8个字母的单词。首先查询数据字典,以查看8个字母单词中最稀有的字母,然后扫描该表。通过查询数据字典,我的意思是确定表words_8E或表words_8z是否具有最少的记录数。

关于正常形式和良好实践

这不是我在建模数据时通常会建议的那种。在您的特定情况下,将整个单词存储在单个字符列中实际上并不在1st normal form中。这是因为你关心单词中的个别元素。根据您的用例,单词是一个字母列表而不是单个单词。一如既往,如何建模取决于你关心的事情。

您的查询给您带来麻烦,因为它不是第一次正常形式。

此问题的完全规范化模型将有两个表:word(WordId PK)和WordLetter(WordId PK,Position PK,Letter)。然后,您将在适当的位置查询具有多个WHERE EXISTS字母的所有单词。

虽然根据数据库理论是正确的,但我认为这不会很好。

答案 2 :(得分:1)

这一切都归结为索引。

您可以创建如下表格:

create table letter_index (
    id integer not null primary key,
    letter varchar(1),
    position integer
)

create unique index letter_index_i1 (letter, position)

create table letter_index_words (
    letter_index_id integer,
    word_id integer
)

然后索引你的所有单词。

如果你想要一个在第二个位置带有'e'的所有单词的列表:

select words.* from words, letter_index_word liw, letter_index li
where li.letter = 'e' and li.position = 2
and liw.letter_index_id = li.id
and words.id = liw.word_id

如果你想要所有单词的'e'在第二个位置,而's'在第五个位置:

select words.* from words, letter_index_word liw, letter_index li
where li.letter = 'e' and li.position = 2
and liw.letter_index_id = li.id
and words.id = liw.word_id
and words.id in (
    select liw.word_id from letter_index_word liw, letter_index li
    where li.letter = 's' and li.position = 5
    and liw.letter_index_id = li.id
)

或者你可以运行两个简单的查询并自己合并结果。

当然,简单地缓存和迭代内存中的列表可能比其中任何一个更快。但速度不够快,不值得每次从DB加载250K列表。

答案 3 :(得分:1)

您可以完全索引此查询,而无需扫描任何超过最佳结果集的大小。

像这样创建一个查找表:

Table:  lookup
pattern     word_id
_o_s_       1
_ous_       1
...

哪个引用你的单词表:

Table:  word
word_id     word
1           mouse

在模式上放置索引并执行如下所示的选择:

select w.word
from lookup l, word w
where l.pattern = '_ous_' and
l.word_id = w.word_id;

当然,你需要一个小的ruby脚本来创建这个查找表,其中pattern是字典中每个单词的每个可能的模式。换句话说,鼠标的模式是:

m____
mo___
mou__
mous_
mouse
_o___
_ou__
...

为特定单词生成所有模式的ruby可能如下所示:

def generate_patterns word
  return [word, '_'] if word.size == 1
  generate_patterns(word[1..-1]).map do |sub_word|
    [word[0] + sub_word, '_' + sub_word]
  end.flatten
end

例如:

> generate_patterns 'mouse'
mouse
_ouse
m_use
__use
mo_se
_o_se
m__se
___se
mou_e
_ou_e
m_u_e
__u_e
mo__e
_o__e
m___e
____e
mous_
_ous_
m_us_
__us_
mo_s_
_o_s_
m__s_
___s_
mou__
_ou__
m_u__
__u__
mo___
_o___
m____
_____

答案 4 :(得分:1)

快速降低10倍左右的方法是为字符串长度创建一个列,在其上放置一个索引,并在where子句中使用它。

答案 5 :(得分:0)

您可以尝试使用全文搜索引擎Apache Lucene。这是为了回答这样的问题,所以你可能会有更多的运气。

Wildcard searching with lucene

答案 6 :(得分:0)

创建内存查找表解决方案:您可以为每个长度分配一个表。

然后匹配,说你知道第4和第8个字母,循环检查每个第4个字母。它们的长度都相同,因此速度很快。只有字母匹配才能查看第8个字母。

这是蛮力,但会很快。让我们说最坏的情况你有5万个8个字母的单词。这是50,000比较。假设ruby运行时性能问题它应该仍然是< 1秒。

所需内存为250k x 10.所以2.5兆。

答案 7 :(得分:0)

这更像是一种练习,而不是现实生活中的解决方案。想法是将单词分成字符。

让我们先设计所需的表格。我假设您的words表格中包含word_id, word, size列:

CREATE TABLE letter_search
( word_id INT NOT NULL
, position UNSIGNED TINYINT NOT NULL
, letter CHAR(1) NOT NULL
, PRIMARY KEY (word_id, position)
, FOREIGN KEY (word_id)
    REFERENCES words (word_id)
      ON DELETE CASCADE 
      ON UPDATE CASCADE
, INDEX position_letter_idx (position, letter)
, INDEX letter_idx (letter)
) ENGINE = InnoDB ;

我们需要一个辅助“数字”表:

CREATE TABLE num
( i UNSIGNED TINYINT NOT NULL
, PRIMARY KEY (i)
) ;

INSERT INTO num (i)               --- I suppose you don't have
VALUES                            --- words with 100 letters
  (1), (2), ..., (100) ;

填充我们的letter_search表:

INSERT INTO letter_search
  ( word_id, position, letter )
SELECT
    w.word_id
  , num.i
  , SUBSTRING( w.word, num.i, 1 ) 
FROM 
    words AS w
  JOIN
    num
       ON num.i <= w.size

此搜索表的大小约为10 * 250K行(其中10,表示单词的平均大小)。


最后,查询:

SELECT * FROM words WHERE word LIKE '_e__o'

将写成:

SELECT w.* 
FROM 
    words AS w
  JOIN
    letter_search AS s2
        ON (s2.position, s2.letter, s2.word_id) = (2, 'e', w.word_id)
  JOIN
    letter_search AS s5
        ON (s5.position, s5.letter, s5.word_id) = (5, 'o', w.word_id)
WHERE
    w.size = 5