实现查找/查找下一个算法

时间:2015-10-23 19:00:21

标签: sql database postgresql

我有一个数据库表(mysql / pgsql),格式如下:

id|text
 1| the cat is black
 2| a cat is a cat
 3| a dog

我需要选择包含单词第n个匹配项的行:

例如:"选择单词cat的第3个匹配项,即2号条目。" 结果:结果的第二行,其中第三个单词是cat

我能找到的唯一解决方案是搜索所有具有文本cat的条目,将它们加载到内存中并通过计算它们来查找匹配项。但是对于大量的比赛(> 100万)来说这并不高效。

你会如何以有效的方式处理这个问题?你有什么可以直接在数据库中做的吗?也许使用像lucene这样的其他技术?

更新:内存中有100万个字符串可能不是一个大问题,但应用程序的期望是有1k到50k的活跃用户可能会同时执行此操作。

14 个答案:

答案 0 :(得分:11)

考虑使用以下结构创建另一个表

Table : index_table
columns : 
         index_id , word, occurrence, id(foreign key to your original table)

进行一次索引编制过程如下:

对原始表中的每个条目进行迭代将文本拆分为单词,并且对于新表中的每个单词查找是否存在(如果不存在)插入一个出现设置为1的新条目。如果存在,则插入具有occurrence =现有的新条目发生+1

完成此操作后,您的选择变得非常简单。 例如,对于第三场比赛的猫将是

SELECT * 
FROM original_table o, index_table idx
WHERE idx.word = 'cat' 
  AND idx.occurrence = 3 
  AND o.id = idx.id

答案 1 :(得分:4)

你不需要Lucene来完成这项工作。此外,如果您有大量的正匹配,那么从数据库中抽取所有必需数据的努力将远远超过计算成本。

这是一个简单的解决方案:

索引:我们需要两个属性:

  1. 有效地访问每个id的单词
  2. 以升序有效地访问所有ID
  3. 如下:

    create index i_words on example_data (id, string_to_array(txt, ' '));
    

    查询:使用以下查询找到与n匹配项相关联的ID:

    select id
    from (
        select id, unnest(string_to_array(txt, ' ')) as word
        from example_data
    ) words
    where word = :w     -- :w = 'cat'
    offset :n - 1       -- :n = 3
    limit 1;
    

    在100万行上执行 2ms

    这是完整的PostgreSQL设置,如果您宁愿亲自尝试而不是接受我的话:

    drop table if exists example_data;
    create table example_data (
        id integer primary key,
        txt text not null
    );
    
    insert into example_data
    (select generate_series(1, 1000000, 3) as id, 'the cat is black' as txt
    union all
    select generate_series(2, 1000000, 3), 'a cat is a cat'
    union all
    select generate_series(3, 1000000, 3), 'a dog'
    order by id);
    
    commit;
    
    drop index if exists i_words;
    create index i_words on example_data (id, string_to_array(txt, ' '));
    
    select id
    from (
        select id, unnest(string_to_array(txt, ' ')) as word
        from example_data
    ) words
    where word = 'cat'
    offset 3 - 1
    limit 1;
    
    select 
        id, word
    from (
        select id, unnest(string_to_array(txt, ' ')) as word
        from example_data
    ) words
    where word = 'cat'
    offset 3 - 1
    limit 1;
    

答案 2 :(得分:2)

请注意,我仍然不确定究竟是什么" 为单词cat选择第3个匹配,即2号条目"应该是的意思。

可能的含义:

  1. 第3个单词为cat
  2. 的结果的第2行
  3. 第3行是第3行" cat"
  4. 来自所有行" cat"出现至少3次,取第二行
  5. 来自所有行" cat"出现至少2次,取第三行
  6. 如果它是1或2,我认为这可以通过使用trigram索引来减少可能的匹配行数来以可接受的速度完成。三元组索引(由module pg_trgm提供)将允许Postgres在执行例如索引时使用索引。 like '%cat%'

    假设只有少量行满足该条件,则可以将生成的行拆分为数组并检查第n个字。

    这样的事情:

    with matching_rows as (
      select id, line, 
             row_number() over (order by id) as rn
      from the_table
      where line like '%cat%' -- this hopefully reduces the result to only very few rows
    )
    select *
    from matching_rows 
    where rn = 3 --<< "the third match for the word cat"
      and (string_to_array(line, ' '))[2] = 'cat' -- "the second word is "cat"
    

    请注意,三元组索引 也有缺点。维持这样的索引要比维护常规的b树索引贵得多(=慢)。因此,如果您的表经过大量更新,这可能不是一个好的解决方案 - 但您需要自己测试一下。

    如果条件“喜欢&#39;%cat%&#39;并没有真正减少行数,这可能也不会很好。

    有关三元组索引的更多信息:

    另一种选择是过滤掉相关的&#34;使用Postgres的行#39;全文搜索而不是简单的LIKE条件。

答案 3 :(得分:2)

无论你为数据库提出什么样的算法,这种数据都可能很慢。你需要一个有效的基于文本的搜索,像solr或elasticsearch这样的基于lucene的解决方案在这里做得很好。这将是最好的选择,虽然找到一个字符串中的第三个标记匹配不是我知道如何构建没有进一步谷歌搜索。

您还可以在数据库中编写一个作业,以便您构建反向映射,字符串 - &gt; id。像这样:

rownum, id, text            
1       1   the cat is black
2       3   nice cat

key,    rownum, id
1_the   1       1
2_cat   1       1
3_is    1       1
4_black 1       1
1_nice  2       3
2_cat   2       3   

如果您可以按ID订购,则不需要rownum。您还应该将该列称为其他内容而不是rownum,为了清楚起见,我将其保留为

现在,您可以通过搜索

搜索第一个ID,其中单词cat是第二个单词
SELECT ID WHERE ROWNUM=1 AND key='3_CAT'

如果您创建了(idkey)或(keyid)索引,那么您的搜索应该非常快。

如果您可以将所有数据放入内存,那么您可以使用简单的Map<MyKey, Long>进行搜索。 MyKey或多或少Pair<Long,String>使用正确的equals和hashCode(和/或Comparable,如果使用TreeMap)实现。

答案 4 :(得分:2)

(感谢Daniel Grosskopf指出我最初误解了这个问题。)

此查询将为您提供只需SQL的内容。它获取文本中单词出现次数(例如&#39; cat&#39;)的运行总计,然后返回达到所需阈值的第一行(例如3)。

SELECT id, text
  FROM (SELECT entries.*,
               SUM((SELECT COUNT(*)
                      FROM regexp_split_to_table(text, E'\\s+') AS words(word)
                     WHERE word = 'cat')) OVER (ORDER BY id) AS running_count
          FROM entries) AS entries_with_running_count
 WHERE running_count >= 3
 LIMIT 1

SQL Fiddle

中查看此操作

答案 5 :(得分:1)

  

你会如何以有效的方式处理这个问题?你有什么把戏吗?   可以直接在数据库中做?

您没有具体说明您可能拥有的其他限制/要求或您对

的定义
  

大量比赛。

作为一般答案,我会说在数据库中进行字符串操作是一种有效的方法。
它太慢并且在您的数据库上施加了很多工作,这通常是共享资源 IMO你应该以编程方式做到这一点 执行此操作的方法可以是将元数据保存在另一个表中,即包含文本cat的行的索引以及句子中的位置 您可以查询此元表,以便从主表中查找要查询的行 这个额外的表比搜索定义的表更有效,因为带有LIKE后缀的查询不能使用索引,最终会导致串行扫描,这会导致性能非常低

答案 6 :(得分:1)

Postgres数据库的解决方案:

在表格中添加新列:

alter table my_table add text_as_array text[];

此列将包含拼接成单词的句子:

"the cat is black" -> ["the","cat","is","black"]

使用当前记录中的值填充此列:

update my_table set text_as_array = string_to_array(text,' ');

(并且在插入新记录时不要忘记将其值设置为string_to_array(text,' ')

在其上创建gin索引:

create index my_table_text_as_array_index on text_as_array gin(text_as_array);
analyze my_table;

然后你只需要运行一个简单的快速查询:

select *
from   my_table
where  text_as_array @> ARRAY['cat'] 
and    text_as_array[3] = 'cat' -- third word in sentence
order  by id
limit  1 
offset 2  -- second occurrence

在我的机器上进行的测试中搜索超过2,400,000条记录花了11毫秒。

说明:

Limit  (cost=11252.08..11252.08 rows=1 width=104)
  ->  Sort  (cost=11252.07..11252.12 rows=19 width=104)
        Sort Key: id
        ->  Bitmap Heap Scan on my_table  (cost=48.21..11251.83 rows=19 width=104)
              Recheck Cond: (text_as_array @> '{cat}'::text[])
              Filter: (text_as_array[3] = 'cat'::text)
              ->  Bitmap Index Scan on my_table_text_as_array_index  (cost=0.00..48.20 rows=3761 width=0)
                    Index Cond: (text_as_array @> '{cat}'::text[])

enter image description here

答案 7 :(得分:0)

从效率的角度来看,“直接在数据库”解决方案中似乎是优选的,因为大多数类型的抽象层或其他地方的加载/处理可能会产生额外的开销。

如果可以按摩源文本,只有空格分隔单词as mentioned in the comments - 可能通过预处理来适当地替换所有非字母字符?),以下(我的SQL解决方案将起作用:

#############################################################
SET @searchWord = 'cat', # Search word: Must be lower case  #
    @n = 1,              # n where nth match is to be found #
#############################################################
    @matches = 0;        # Initialise local variable

SELECT s.*
FROM sentence s
WHERE id = 
(SELECT subq.id
 FROM
 (SELECT *,
         @matches AS prevMatches,
         (@matches := @matches + LENGTH(`text`) - LENGTH(
                      REPLACE(LOWER(`text`),
                              CONCAT(' ', @searchWord, ' '),
                              CONCAT(@searchWord, ' ')))
          + CASE WHEN LEFT(LOWER(`text`), 4) = CONCAT(@searchWord, ' ') THEN 1 ELSE 0 END
          + CASE WHEN RIGHT(LOWER(`text`), 4) = CONCAT(' ', @searchWord) THEN 1 ELSE 0 END)
     AS matches
  FROM sentence) AS subq
 WHERE subq.prevMatches < @n AND @n <= subq.matches);

<强>解释

每行' cat '的所有实例都替换为一个字母缩短的单词。然后计算长度的差异以找出实例的数量。最后,'cat '' cat'出现在行的开头和结尾的单一可能性分别迎合了。完成此操作后,每行保留累计总计matches。这被捆绑到子查询中,通过查找累积匹配数不大于n但前一个总数小于n的行,可以从中选择 nth 匹配。

进一步的潜在改进

  1. 当然可以通过使源文本小写(如果正在预处理它似乎合理)并删除对LOWER()的所有调用来稍微简化上述内容。
  2. 子查询计算累计匹配总数。如果可能会重复使用相同的搜索项,可能会将这些结果缓存到另一个表中,并且每当更新,插入或删除记录时都使用触发器来维护 - 但这会大大增加复杂性和数据存储要求。

答案 8 :(得分:0)

我会搜索带有“cat”的所有行,但会按 n 限制行。这应该为您提供合理大小的数据子集,保证包含您要查找的行。 SQL看起来与此类似:

select id, text
  from your_table
 where text ~* 'cat'
  order by id
 limit 3 --nth time cat appears

然后,我会将您的解决方案实现为pl / pgsql函数,以获取包含第n个单词出现的id:

CREATE OR REPLACE FUNCTION your_schema.row_with_nth_occurrence(character varying, integer)
  RETURNS integer AS
$BODY$
Declare
  arg_search_word ALIAS FOR $1;
  arg_occurrence ALIAS FOR $2;

  v_sql         text;
  v_sql2        text;
  v_count       integer;
  v_count_total integer;
  v_record      your_table%ROWTYPE;

BEGIN

v_sql := 'select id, text
            from your_table
           where text ~* ' || arg_search_word || '
           order by id
           limit ' || arg_occurrence || ';';

v_count := 0;
v_count_total  := 0;
FOR v_record IN v_sql LOOP
  v_sql2 := 'SELECT count(*)
               FROM regexp_split_to_table('||v_record.text||', E'\\s+') a
              WHERE a = '|| arg_search_word ||';';
  EXECUTE v_sql2 INTO v_count;
  v_count_total := v_count_total + v_count;
  IF v_count_total >= arg_occurrence THEN
    RETURN v_record.id;
  END IF;
END LOOP;

RAISE EXCEPTION '% does not occur % times in the database.', arg_search_word, arg_occurrence;
END;

所有这个函数都循环遍历可能包含所需单词的行的子集,计算每行中出现的次数,然后在找到具有第n个单词的行时返回Id。 / p>

答案 9 :(得分:0)

解决方案一: 将行保留在内存中但集中。所有客户端都遍历同一个列表。可能足够快,合理的内存友好。

解决方案二: 使用JDBC驱动程序中的流ResultSet技术; e.g。

     Statement select = connection.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
     select.setFetchSize(Integer.MIN_VALUE);
     ResultSet result = select.executeQuery(sql);

http://dev.mysql.com/doc/connector-j/en/connector-j-reference-implementation-notes.html中所述,向下滚动到Resultset。这应该是内存友好的。

现在只需依靠结果行直到满意并关闭结果。

答案 10 :(得分:0)

我无法理解你的陈述:

  

例如:“选择单词cat的第3个匹配,即数字2   “结果:结果的第二行,第三个单词是cat

我认为你的意思是,你想要搜索文本的第3个单词是“cat”的条目,以及你想要第二次进入的条目。

由于您提到问题在于并发访问和速度,因此您需要以某种方式构建一个针对您的查询进行优化的索引。你可以使用任何东西,数据库,lucene等。我的建议是在内存中构建索引。在开始提供服务请求之前,请将其视为热身服务。

在您的情况下,您需要某种地图,其中单词和单词位置为关键字。然后,该密钥将映射到与密钥匹配的行号列表。所以最后,你只需要进行两次查找,首先是获得匹配的行号列表,然后是你想要的行号。因此,最终需要的性能将是简单的地图查找+数组列表查找(常量)。

我在下面提供了一个非常简单的例子。这是未经测试的代码,但它应该大致给你一个想法。

如果需要,您还可以在构建文件后将其保存到文件中。在你成为索引并将它们加载到内存之后,这将非常快。

// text entry from the DB
public class TextEntry {
    private int rowNb;
    private String text;
    // getters & setters
}

// your index class
public class Index {
    private Map<Key, List<Integer>> indexMap;
    // getters and setters

    public static class Key {
        private int wordPosition;
        private String word;
        // getters and setters
    }
}

// your searcher class
public class Searcher {

    private static Index index = null;
    private static List<TextEntry> allTextEntries = null;


    public static init() {
        // init all data with some synchronization check

        // synchronization check whether index has been built

        allTextEntries.forEach(entry -> {
          // split the words, and build the index based on the word position and the word
          String[] words = entry.split(" ");
          for (int i = 0; i < words.length; i++) {
              Index.Key key = new Index.Key(i + 1, words[i]);
              int rowNumber = entry.getRowNb();
              // if the key is already there, just add the row number if it's not the last one
              if (indexMap.contains(key)) {
                  List entryMatch = indexMap.get(key);
                  if (entryMatch.get(entryMatch.size() - 1) !== rowNumber) {
                    entryMatch.add(rowNumber);
                  }
              } else {
                  // if key is not there, add a new one
                  List entryMatch = new ArrayList<Integer>()
                  entryMatch.add(rowNumber);
                  indexMap.put(key, entryMatch);
              }
          }
        });
    }

    public static TextEntry search(String word, int wordPosition, int resultNb) {
        // call init if not yet called, do some check

        int rowNb = index.getIndexMap().get(new Index.Key(word, wordPosition)).get(resultNb - 1);
        return allTextEntries.get(rowNb);
    }

}

答案 11 :(得分:0)

在mysql中 我们需要一个函数来计算字段中给定子字符串的出现次数。

创建函数(此函数将计算给定列中子字符串的出现次数)

 CREATE FUNCTION substrCount(
         x varchar(255), delim varchar(12)) returns int
    return (length(x)-length(REPLACE(x,delim, '')))/length(delim);

此功能应该能够找到多少次“猫”。出现在文本中。 请关注我的代码语法,因为它可能无法完全正常运行(根据需要更正)。

我将这个问题分解为3个部分,我们可以在存储过程的帮助下完成。

  1. 选择包含字符串&#39; cat&#39;的所有行(或任何其他输入)。这应该选择最多n行(n =出现次数),因此我们将在查询中使用limit。
  2. 使用游标,在roop中迭代匹配的行。
  3. 计数变量中每行的增量匹配匹配,并在找到的匹配数后退出。(应该能够在1到n个循环内找到匹配)
  4. 创建存储过程。 假设索引正确,这应该很快。

    DELIMITER $$
    
    CREATE PROCEDURE find_match(INOUT string_to_match varchar(100),
     INOUT occurence_count INTEGER,OUT match_field varchar(100))
    BEGIN
    
     DECLARE v_count INTEGER DEFAULT 0;
     DECLARE v_text varchar(100) DEFAULT "";
    
     -- declare cursor and select by the order you want.
     DEClARE matcher_cursor CURSOR FOR 
     SELECT textField FROM myTable 
            where textField like string_to_match 
                  order by id 
                  LIMIT 0, occurence_count;
    
     -- declare NOT FOUND handler
     DECLARE CONTINUE HANDLER 
            FOR NOT FOUND SET v_finished = -1;
    
     OPEN matcher_cursor;
    
     get_matching_occurence: LOOP
    
     FETCH matcher_cursor INTO v_text;
    
     IF v_count = -1 THEN 
     LEAVE get_matching_occurence;
     END IF;
    
     -- use substring count function 
     v_count:= v_count + substrCount(v_text,string_to_match));
    
    -- if count is equal to greater than occurenece that means matching row is found.
    IF (v_count>= occurence_count) THEN
    
     SET match_field = v_text;
     v_count:=-1;
    
    END IF;
    
     END LOOP get_matching_occurence;
    
     CLOSE _
    
    END$$
    
    DELIMITER ;
    

答案 12 :(得分:0)

我只计算每行中的单词数量,然后计算累积总和。我不确定计算单词的最有效方法是什么,但长度的差异可能会胜出:

select t.*
from (select t.*, sum(cnt) over (order by id) as cumecnt
      from (select t.*,
                   (length(' ' || str || ' ') - length(replace(' ' || str || ' '), ' cat ', '')) / length(' cat ') as cnt
            from t
           ) t
      where num > 0
     ) t
where cumecnt >= 3 and cumecnt - cnt <= 3;

你只需要替换&#34; 3&#34;和&#34; cat&#34;用适当的字符串。

此方法需要在每行中扫描字符串几次(每个长度一次,替换一次)。我的猜测是,这比各种数组操作,正则表达式或文本更快。如果你对单词的定义有更复杂的定义,那么你可能需要使用正则表达式替换:

在数据库中完成工作通常是一个巨大的胜利。但是,如果您要查找一百万行中的第六个匹配项,则从子查询中读回值并在应用程序中进行累积可能会更快。我不认为有一种方法可以将数据库计算短路,以便停止在第4和第4天。行。

答案 13 :(得分:0)

我在一个有120万行的表上对此进行了测试,并在不到一秒的时间内返回数​​据。我从这里使用拆分功能(这是Jeff Modem的拆分器功能的修改形式):&#39; http://sqlperformance.com/2012/08/t-sql-queries/splitting-strings-follow-up&#39; .`

-- Step 1. Create table
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
SET ANSI_PADDING ON
GO
CREATE TABLE [dbo].[Sentence](
    [id] [int] IDENTITY(1,1) NOT NULL,
    [Text][varchar](250) NULL,
CONSTRAINT [PK_Sentence] PRIMARY KEY CLUSTERED 
    (
    [id] ASC
    )WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
    ) ON [PRIMARY]

GO
SET ANSI_PADDING OFF
GO

步骤2.创建拆分功能

CREATE FUNCTION [dbo].[SplitSentence]
(
   @CSVString NVARCHAR(MAX),
   @Delimiter NVARCHAR(255)
)
RETURNS TABLE
WITH SCHEMABINDING AS
RETURN
  WITH E1(N)        AS ( SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 
                     UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 
                     UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1),
   E2(N)        AS (SELECT 1 FROM E1 a, E1 b),
   cteTally(N)  AS (SELECT 0 
                    UNION ALL 
                    SELECT TOP (DATALENGTH(ISNULL(@CSVString,1))) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E2),
   cteStart(N1) AS (SELECT t.N+1 
                    FROM cteTally t
                     WHERE (SUBSTRING(@CSVString,t.N,1) = @Delimiter OR t.N = 0))
  SELECT Word = SUBSTRING(@CSVString, s.N1, ISNULL(NULLIF(CHARINDEX(@Delimiter,@CSVString,s.N1),0)-s.N1,50))
  FROM cteStart s;

步骤3.创建一个sql脚本以返回所需的数据

DECLARE @n int = 3
DECLARE @Word varchar(50) = 'cat'
;WITH myData AS 
(SELECT TOP (@n)
    id
    ,[Text]
    ,sp.word
    ,ROW_NUMBER() OVER (ORDER BY Id) RowNo
FROM 
    Sentence 
CROSS APPLY (SELECT * FROM SplitSentence(Sentence.[Text],' ')) sp
WHERE Word = @Word)
SELECT 
    * 
FROM 
    myData 
WHERE 
    RowNo = @n

假设:

 1. The sentence has a max length of 250 characters. If needed this can be modified in the create table statement.
 2. The sentence will not have more than a 100 words. If more than 100 words are needed, the split function will have to be modified.
 3. Any word in the sentence has a max length of 50 characters.

这里的SQL小提琴演示:http://sqlfiddle.com/#!3/0a1d0/1

Notes: 
I am aware that the original requirement is for MySQL/pgsql, 
but I have limited knowledge of these and therefore my solution has been created/tested in MSSQL.