在ef core中使用postgres数据库的全文检索功能实战

起源

之前做的很多项目都使用solr/elasticsearch作为全文检索引擎,它们功能全面而强大,但是对于较小的项目而言,构建和维护成本显然过高,尤其是从关系数据库/文档数据库到全文检索引擎的数据同步工作非常繁琐,且容易出错。

记得很久以前就知道postgresql数据库内置全文检索,最近发现这个数据库越来越火,于是就又研究了一番,欣喜的发现居然支持ef core,于是对其进行了一些研究,并整理心得如下。

前提

本文假设读者熟悉entity framework core的基本概念和基本使用。

目的

建立dotnet core项目,使用postgres数据库和ef core,实现常见的全文检索功能,包括

建立索引字段

基本查询

查询结果排名

查询结果高亮显示

步骤1 - 新建项目并引入packages

Exenetcoreapp3.1

注意NamingConventions包是可选的,其作用是将表和字段名称翻译成蛇形,如MyData -> my_data,这样比较方便手写sql,不用写烦人的引号。

步骤2 - 建立model和dbcontext

using System.ComponentModel.DataAnnotations;using System.ComponentModel.DataAnnotations.Schema;using NpgsqlTypes;publicclass Article

{

    publicintId {get;set; }

    [Required]

    [MaxLength(128)]

    publicstringTitle {get;set; }

    [MaxLength(512)]

    publicstringAbst {get;set; }

    publicNpgsqlTsVector TitleVector {get;set; }

    publicNpgsqlTsVector AbstVector {get;set; }

    [NotMapped]

    publicstringTitleHL {get;set; }

    [NotMapped]

    publicstringAbstHL {get;set; }

}

本model中的TitleVector和AbstVector分别用来存放Title和Abst字段的分词结果,便于后续的查询。不必担心代码会不小心改掉这些字段以至于查询出错,因为后续会设置一个触发器,每次更改数据的时候都会自动更新这些字段的内容。

using Microsoft.EntityFrameworkCore;publicclass MyDbContext : DbContext

{

    protectedoverridevoidOnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder

        .UseNpgsql("Host=localhost;Database=ft;Username=postgres;Password=123456")

        .UseLoggerFactory(PgFtSearch.Program.MyLoggerFactory)

        .UseSnakeCaseNamingConvention();

    protectedoverridevoid OnModelCreating(ModelBuilder modelBuilder)

    {

        base.OnModelCreating(modelBuilder);

        modelBuilder.Entity().HasIndex(p => p.TitleVector).HasMethod("GIN");

        modelBuilder.Entity().HasIndex(p => p.AbstVector).HasMethod("GIN");

    }

    publicDbSet Articles {get;set; }

}

首先UseNpgsql设置了要连接哪个数据库,然后UseLoggerFactory用来打印日志,主要是sql语句。MyLoggerFactory是怎么来的,参考后续的代码。

GIN的两行,用来告诉数据库这两个字段是采用倒排索引。

步骤3 - 生成migration并手动添加触发器

dotnet ef migrations add Init

然后,在生成的migration文件中手动添加触发器,在新增或者修改数据时,自动修改索引字段的内容,应用程序不必担心索引同步的问题。

migrationBuilder.Sql(

            @"CREATE TRIGGER article_title_search_vector_update BEFORE INSERT OR UPDATE

              ON articles FOR EACH ROW EXECUTE PROCEDURE

              tsvector_update_trigger(title_vector, 'pg_catalog.english', title);");

migrationBuilder.Sql(

            @"CREATE TRIGGER article_abst_search_vector_update BEFORE INSERT OR UPDATE

              ON articles FOR EACH ROW EXECUTE PROCEDURE

              tsvector_update_trigger(abst_vector, 'pg_catalog.english', abst);");

步骤4 - 编写程序

using System;using System.Collections.Generic;using System.Linq;using Microsoft.EntityFrameworkCore;using Microsoft.Extensions.Logging;namespace PgFtSearch

{

    class Program

    {

        publicstaticreadonly ILoggerFactory MyLoggerFactory

            = LoggerFactory.Create(builder => { builder.AddConsole(); });


        staticvoidMain(string[] args)

        {

            using(vardb =new MyDbContext())

            {

                if(!db.Articles.Any())

                {

                    vararticles =newList{

                        newArticle{Title="testing is ok", Abst="this is a test about postgre full text searching"},

                        newArticle{Title="tested all bugs", Abst="there is no bug exists in this app"}

                    };

                    db.AddRange(articles);

                    db.SaveChanges();

                }

                varquery ="test";

                vardata = db.Articles

                    .Where(p => p.TitleVector.Matches(query) || p.AbstVector.Matches(query))

                    .OrderByDescending(p=>p.TitleVector.Rank(EF.Functions.ToTsQuery(query)) *2.0+ p.AbstVector.Rank(EF.Functions.ToTsQuery(query)))

                    .Select(p=>new Article{

                        Title = p.Title,

                        Abst = p.Abst,

                        TitleHL = EF.Functions.ToTsQuery(query).GetResultHeadline(p.Title),

                        AbstHL = EF.Functions.ToTsQuery(query).GetResultHeadline(p.Abst),

                    });

                foreach(vararticlein data)

                {

                    Console.WriteLine($"{article.Title}\t{article.Abst}\t{article.TitleHL}\t{article.AbstHL}");

                }

            }

        }

    }

}

首先,如果没有数据,插入几条测试数据。

下面到了最关键的地方,编写数据查询的代码,实现的具体功能是:

使用test关键字在title或abst字段中查询数据

对查询结果进行排序,title字段排序权重=2.0,高于abst字段权重=1.0

检索结果的title和abst进行高亮显示

最终生成的SQL如下:

SELECT

a.titleAS"Title",

a.abstAS"Abst",

ts_headline(a.title, to_tsquery(@__query_0))AS"TitleHL",

ts_headline(a.abst, to_tsquery(@__query_0))AS "AbstHL"FROMarticlesAS aWHERE(a.title_vector @@ plainto_tsquery(@__query_0))OR(a.abst_vector @@ plainto_tsquery(@__query_0))ORDERBY(ts_rank(a.title_vector, to_tsquery(@__query_0))::doubleprecision*2.0)+ts_rank(a.abst_vector, to_tsquery(@__query_0))::doubleprecisionDESC

代码在这儿,相信大家都能看懂,有问题欢迎交流。

总结

目前还未研究中文分词的支持情况,也没有测试性能。不过大致看来,完全可以在中小型项目中使用postgres数据库的内置全文检索功能替代solr/es等搜索引擎,减少系统的复杂程度,提升全文检索功能的稳定性。

你可能感兴趣的:(在ef core中使用postgres数据库的全文检索功能实战)