在ef core中使用postgres數(shù)據(jù)庫的全文檢索功能實戰(zhàn)

起源

之前做的很多項目都使用solr/elasticsearch作為全文檢索引擎,它們功能全面而強大,但是對于較小的項目而言,構(gòu)建和維護成本顯然過高,尤其是從關(guān)系數(shù)據(jù)庫/文檔數(shù)據(jù)庫到全文檢索引擎的數(shù)據(jù)同步工作非常繁瑣,且容易出錯。

記得很久以前就知道postgresql數(shù)據(jù)庫內(nèi)置全文檢索,最近發(fā)現(xiàn)這個數(shù)據(jù)庫越來越火,于是就又研究了一番,欣喜的發(fā)現(xiàn)居然支持ef core,于是對其進行了一些研究,并整理心得如下。

前提

本文假設(shè)讀者熟悉entity framework core的基本概念和基本使用。

目的

建立dotnet core項目,使用postgres數(shù)據(jù)庫和ef core,實現(xiàn)常見的全文檢索功能,包括

建立索引字段

基本查詢

查詢結(jié)果排名

查詢結(jié)果高亮顯示

步驟1 - 新建項目并引入packages

<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><OutputType>Exe</OutputType><TargetFramework>netcoreapp3.1</TargetFramework></PropertyGroup><ItemGroup><PackageReference Include="EFCore.NamingConventions" Version="1.1.0"/><PackageReference Include="Microsoft.Extensions.Logging.Console" Version="3.1.4"/><PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="3.1.3"/><PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.3"/></ItemGroup></Project>

注意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字段的分詞結(jié)果,便于后續(xù)的查詢。不必擔(dān)心代碼會不小心改掉這些字段以至于查詢出錯,因為后續(xù)會設(shè)置一個觸發(fā)器,每次更改數(shù)據(jù)的時候都會自動更新這些字段的內(nèi)容。

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設(shè)置了要連接哪個數(shù)據(jù)庫,然后UseLoggerFactory用來打印日志,主要是sql語句。MyLoggerFactory是怎么來的,參考后續(xù)的代碼。

GIN的兩行,用來告訴數(shù)據(jù)庫這兩個字段是采用倒排索引。

步驟3 - 生成migration并手動添加觸發(fā)器

dotnet ef migrations add Init

然后,在生成的migration文件中手動添加觸發(fā)器,在新增或者修改數(shù)據(jù)時,自動修改索引字段的內(nèi)容,應(yīng)用程序不必擔(dān)心索引同步的問題。

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}");

? ? ? ? ? ? ? ? }

? ? ? ? ? ? }

? ? ? ? }

? ? }

}

首先,如果沒有數(shù)據(jù),插入幾條測試數(shù)據(jù)。

下面到了最關(guān)鍵的地方,編寫數(shù)據(jù)查詢的代碼,實現(xiàn)的具體功能是:

使用test關(guān)鍵字在title或abst字段中查詢數(shù)據(jù)

對查詢結(jié)果進行排序,title字段排序權(quán)重=2.0,高于abst字段權(quán)重=1.0

檢索結(jié)果的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

代碼在這兒,相信大家都能看懂,有問題歡迎交流。

總結(jié)

目前還未研究中文分詞的支持情況,也沒有測試性能。不過大致看來,完全可以在中小型項目中使用postgres數(shù)據(jù)庫的內(nèi)置全文檢索功能替代solr/es等搜索引擎,減少系統(tǒng)的復(fù)雜程度,提升全文檢索功能的穩(wěn)定性。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容