【问题标题】:How to get position of regexp match in string in PostgreSQL?如何在PostgreSQL的字符串中获取正则表达式匹配的位置?
【发布时间】:2014-01-14 01:06:31
【问题描述】:

我有一个包含书名的表格,我想选择书名与正则表达式匹配的书,并按正则表达式在标题中匹配的位置对结果进行排序。

单字搜索很容易。例如

TABLE book
id   title
1    The Sun
2    The Dead Sun
3    Sun Kissed

在将查询发送到数据库之前,我将在客户搜索词中的单词之间放置.*,因此我将在此处使用准备好的正则表达式编写 SQL。

SELECT book.id, book.title FROM book
    WHERE book.title ~* '.*sun.*'
    ORDER BY COALESCE(NULLIF(position('sun' in book.title), 0), 999999) ASC;

RESULT
id   title
3    Sun Kissed
1    The Sun
2    The Dead Sun

但是,如果搜索词有多个词,我想匹配包含搜索词中所有词的标题以及它们之间的任何内容,并像以前一样按位置排序,所以我需要一个返回正则表达式位置的函数,我在官方 PostgreSQL 文档中没有找到合适的。

TABLE books
id   title
4    Deep Space Endeavor
5    Star Trek: Deep Space Nine: The Never Ending Sacrifice
6    Deep Black: Space Espionage and National Security

SELECT book.id, book.title FROM book
    WHERE book.title ~* '.*deep.*space.*'
    ORDER BY ???REGEXP_POSITION_FUNCTION???('.*deep.*space.*' in book.title);

DESIRED RESULT
id   title
4    Deep Space Endeavor
6    Deep Black: Space Espionage and National Security
5    Star Trek: Deep Space Nine: The Never Ending Sacrifice

没有找到类似???REGEXP_POSITION_FUNCTION???的函数,大家有什么想法吗?

【问题讨论】:

  • 为什么最后一个结果中的第 4 行在第 6 行之前?两者都在位置 0 匹配。任意?还是有更多标准?
  • @ErwinBrandstetter 理想情况下,我希望更长(更多字符)匹配的“排名”更少,但目前这不是主要问题。

标签: sql regex database postgresql


【解决方案1】:

一种(多种)方法:删除从匹配开始的字符串的其余部分并测量截断字符串的长度:

SELECT id, title
FROM   book
WHERE  title ILIKE '%deep%space%'
ORDER  BY length(regexp_replace(title, 'deep.*space.*', '','i'));

在 WHERE 子句中使用 ILIKE,因为这通常更快(在这里也是如此)。
还要注意regexp_replace() 函数的第四个参数('i'),使其不区分大小写。

替代方案

根据评论中的要求。
同时演示如何首先对匹配项(和NULLS LAST)进行排序。

SELECT id, title
      ,substring(title FROM '(?i)(^.*)deep.*space.*') AS sub1
      ,length(substring(title FROM '(?i)(^.*)deep.*space.*')) AS pos1

      ,substring(title FROM '(?i)^.*(?=deep.*space.*)') AS sub2
      ,length(substring(title FROM '(?i)^.*(?=deep.*space.*)')) AS pos2

      ,substring(title FROM '(?i)^.*(deep.*space.*)') AS sub3
      ,position((substring(title FROM '(?i)^.*(deep.*space.*)')) IN title) AS p3

      ,regexp_replace(title, 'deep.*space.*', '','i') AS reg4
      ,length(regexp_replace(title, 'deep.*space.*', '','i')) AS pos4
FROM   book
ORDER  BY title ILIKE '%deep%space%' DESC NULLS LAST
         ,length(regexp_replace(title, 'deep.*space.*', '','i'));

您可以在手册herehere 中找到上述所有内容的文档。

-> SQLfiddle 演示全部。

【讨论】:

  • 不错!真的很简单,很好的解决方案。你说可以有很多其他的解决方案,你能告诉我吗?我没有看到这个解决方案,甚至现在也无法想象其他人:)
  • 如何删除 WHERE 子句并进行相同的排序?我的意思是我想获得所有标题,但最后没有匹配的标题?使用这个长度(),我将首先得到非常短的标题没有匹配。
  • 我刚刚编辑了这个问题,因为我发现我根本没有能力创建程序函数(感谢 heroku)。
  • ORDER BY title ILIKE '%deep%space%' DESC, length(regexp_replace(title, 'deep.*space.*', '','i')) 看起来像一个解决方案
  • 是的,您的回答很有帮助。最后我取了匹配前的字符串长度和匹配的长度,并根据这两个值排序,非常感谢您的启发!
【解决方案2】:

另一种方法是首先获取模式的文字匹配,然后找到文字匹配的位置:

strpos(input, (regexp_match(input, pattern, 'i'))[1]);

或者在这种情况下:

SELECT   id, title
FROM     book
ORDER BY strpos(book.title, (regexp_match(book.title, '.*deep.*space.*', 'i'))[1]);

但是,有一些警告:

  1. 这不是很有效,因为它会扫描输入字符串两次。

  2. 这将忽略环视(lookbehind,lookahead)约束,因为文字匹配可以在模式匹配之前出现多次。 例如:对于输入'aba' 和模式'(?<=b)a'strpos 将返回1(对于第一个'a'),尽管实际位置应该是3(对于第二个'a')。

顺便说一句,您可能应该使用贪婪的量词并尽可能缩小您的字符类别,而不是 .* 以提高性能(例如 'deep [\w\s]*? space'

【讨论】:

    猜你喜欢
    • 2013-06-12
    • 2012-03-23
    • 1970-01-01
    • 2011-02-10
    • 1970-01-01
    • 1970-01-01
    • 2022-08-03
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多