美文网首页go
【Go Web开发】全文搜索

【Go Web开发】全文搜索

作者: Go语言由浅入深 | 来源:发表于2022-02-20 17:47 被阅读0次

在本节中,我们将实现对电影title过滤更容易使用,通过调整它来支持部分匹配,而不是要求title完整匹配。因此,如果一个用户想要找到电影名称为Breakfast Clud,可以通过查询字符串title=Breakfast找到它。

在我们的代码库中有几种不同的方法可以实现这个特性,但一个有效且直观的方法(从客户端的角度来看)是利用PostgreSQL的全文搜索功能,它允许你对数据库中的文本字段执行“自然语言”搜索。

PostgreSQL的全文搜索是一个功能强大且高度可配置的工具,要完整地解释它的工作原理和可用选项可能会占用较多篇幅。因此,我们将在本文中概括下其用法,并将重点放在功能实现上。

为了在title字段实现全文搜索,需要更新SQL查询:

SELECT id, create_at, title, year, runtime, genres, version
FROM movies
WHERE (to_tsvector('simple', title) @@ plainto_tsquery('simple', $1) OR $1= '')
AND (genres @> $2 OR $2 = '{}')
ORDER BY id

这看起来很复杂,我们来解释一下里面字段含义:to_tsvector('simple', title)函数提取电影标题(title)并将其拆分为词汇。我们指定simple配置,意味着对title不做大小写区分。例如电影的title是“The Breakfast Club”,将被分解为:'breakfast', 'club', 'the'三个词。

其他“non-simple”配置会对词汇应用其他的规则,例如删除常见单词或应用特定于语言的词干提取。

plainto_tsquery('simple', $1)函数将查询参数转为PostgreSQL全文搜索可以理解的格式化查询词。将查询值标准化(再次使用simple配置),删除任何特殊字符,并在单词之间插入and操作符&。例如,查询值为“The Club”将产生查询词:“The”&“Club”。

@@操作符指的是匹配操作符。在上面申明中,用于检查生成的查询词与to_tsvector函数生成的词位匹配。接着刚才的例子,查询词'the'&'club'将匹配同时包含'the'和'club'的记录。

在上面的段落中有很多专业词汇,如果我们用几个例子来说明,实际上是非常直观的:

// 返回title包含'panther'的所有movies记录
/v1/movies?title=panther

// 返回title包含'the'和‘club的所有movies记录
/v1/movies?title=the+club

下面我们将以上查询应用到项目开发上去。打开internal/data/movies.go文件,更新GetAll()方法并使用新的SQL查询:

File: internal/data/movies.go


package data

...
func (m MovieModel)GetAll(title string, genres []string, filters Filters) ([]*Movie, error)  {
    query := `
        SELECT id, created_at, title, year, runtime, genres, version
        FROM movies
        WHERE (to_tsvector('simple', title) @@ plainto_tsquery('simple', $1) OR $1 = '') 
        AND (genres @> $2 OR $2 = '{}')
        ORDER BY id`
    //创建3s超时上下文实例
  
        //其他代码没变
       ...
}

重启服务并尝试使用不同的查询参数发起请求。您应该会发现,部分搜索与上面描述的一样。例如:

$ curl "localhost:4000/v1/movies?title=panther"
{
        "movies": [
                {
                        "id": 2,
                        "title": "Black Panther",
                        "year": 2018,
                        "runtime": "134 mins",
                        "genres": [
                                "sci-fi",
                                "action",
                                "adventure"
                        ],
                        "Version": 2
                }
        ]
}
$ curl "localhost:4000/v1/movies?title=the+club"
{
        "movies": [
                {
                        "id": 4,
                        "title": "The Breakfast Club",
                        "year": 1985,
                        "runtime": "97 mins",
                        "genres": [
                                "comedy",
                                "drama"
                        ],
                        "Version": 15
                }
        ]
}

添加索引

为了在数据集增长时保持SQL查询的快速执行,明智的做法是使用索引来避免全表扫描,并避免在每次运行查询时为标题字段生成词汇。

如果你对SQL数据库的索引不熟悉,PostgreSQL官方文档提供详细介绍,还总结了不同的索引类型。我推荐您先了解下这些在继续阅读本书。

在我们的greenlight项目中,在grenres字段和to_tsvector()生成的词中创建GIN索引是明智的,因为它们都在WHERE字句中使用到。下面我们创建一组数据库迁移文件:

$  migrate create -seq -ext=.sql -dir=./migrations add_movies_indexes

然后在“up”和“down”迁移文件中添加以下语句,创建和删除必要的索引:

File: migration/000003_add_movies_indexes.up.sql


CREATE INDEX IF NOT EXISTS movies_title_idx ON movies USING GIN (to_tsvector('simple', title));
CREATE INDEX IF NOT EXISTS movies_genres_idx ON movies USING GIN (genres);

File: migration/000003_add_movies_indexes.down.sql


DROP INDEX IF EXISTS movies_title_idx;
DROP INDEX IF EXISTS movies_genres_idx;

现在,你应该能够执行'up'迁移,以添加索引到你的数据库:

$  migrate -path=./migrations -database=$GREENLIGHT_DB_DSN up
3/u add_movies_indexes (33.802583ms)

附加内容

Non-simple配置及其补充

如前面所述,您还可以为全文搜索使用特定于语言的配置,而不是我们目前使用的simple配置。当使用特定于语言的配置创建词汇或查询术语时,它将剥离该语言的常见单词并执行单词提取。假如我们使用english配置,那么"One Flew Over the Cuckoo's Nest"查询参数将生成'cuckoo' 'flew' 'nest' 'one'搜索词。也可以使用spanish配置。你可以通过运行PostgreSQL的\dF命令来检索所有可用配置的列表:

postgres=# \dF
               List of text search configurations
   Schema   |    Name    |              Description  
------------+------------+---------------------------------------
 pg_catalog | arabic     | configuration for arabic language
 pg_catalog | danish     | configuration for danish language
 pg_catalog | dutch      | configuration for dutch language
 pg_catalog | english    | configuration for english language
 pg_catalog | finnish    | configuration for finnish language

...

如果你想使用english配置来搜索movies记录,可以更新SQL查询如下所示:

SELECT id, created_at, title, year, runtime, genres, version
FROM movies
WHERE (to_tsvector('english', title) @@ plainto_tsquery('english', $1) OR $1 = '') 
AND (genres @> $2 OR $2 = '{}')
ORDER BY id

如果您想了解更多关于PostgreSQL全文搜索的内容,那么阅读这篇博客是一个很好的补充,并且官方文档也很不错。

使用STRPOS和ILIKE

如果不想对movie的title查找使用全文搜索,可以使用PostgreSQS的STRPOS()函数和ILIKE操作符。

PostgreSQL的STRPOS()函数允许您检查特定数据库字段中是否存在子字符串。我们可以像这样在SQL查询中使用它:

SELECT id, created_at, title, year, runtime, genres, version 
FROM movies
WHERE (STRPOS(LOWER(title), LOWER($1)) > 0 OR $1 = '')
AND (genres @> $2 OR $2 = '{}')
ORDER BY id

从客户端角度来看,这样做的缺点是可能返回一些不直观的结果。例如,搜索title=the将返回数据集中的the Breakfast Club和the Black Panther,因为title中都包含‘the'。【这种匹配就不是单词匹配了,而是字符串子串匹配】

从服务器的角度来看,它也不适合用于大型数据集。因为没有有效的方法来索引title字段以查看STRPOS()条件是否满足,这意味着每次运行查询时可能需要全表扫描

另一个方法是ILIKE操作符,它允许您查找匹配特定(不区分大小写)模式的行。我们可以像这样在SQL查询中使用它:

SELECT id, created_at, title, year, runtime, genres, version 
FROM movies
WHERE (title ILIKE $1 OR $1 = '')
AND (genres @> $2 OR $2 = '{}')
ORDER BY id

从服务器的角度来看,这种方法会更好,因为可以使用pg_trgm扩展和GIN索引在title字段上创建索引(详细信息请参阅这篇文章)。

从客户端来看,STRPOS()方法也可能更好,因为它可以通过使用%通配符作为搜索词的前缀/后缀来控制匹配行为(在URL查询字符串中需要转义为%25)。例如,要搜索title以“the”开头的电影,客户端可以发送查询字符串参数title=the%25。

相关文章

网友评论

    本文标题:【Go Web开发】全文搜索

    本文链接:https://www.haomeiwen.com/subject/ablvlrtx.html