以前作的不少項目都使用solr/elasticsearch做爲全文檢索引擎,它們功能全面而強大,可是對於較小的項目而言,構建和維護成本顯然太高,尤爲是從關係數據庫/文檔數據庫到全文檢索引擎的數據同步工做很是繁瑣,且容易出錯。sql
記得好久之前就知道postgresql數據庫內置全文檢索,最近發現這個數據庫愈來愈火,因而就又研究了一番,欣喜的發現竟然支持ef core,因而對其進行了一些研究,並整理心得以下。數據庫
本文假設讀者熟悉entity framework core的基本概念和基本使用。app
創建dotnet core項目,使用postgres數據庫和ef core,實現常見的全文檢索功能,包括elasticsearch
<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,不用寫煩人的引號。ide
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using NpgsqlTypes; public class Article { public int Id { get; set; } [Required] [MaxLength(128)] public string Title { get; set; } [MaxLength(512)] public string Abst { get; set; } public NpgsqlTsVector TitleVector { get; set; } public NpgsqlTsVector AbstVector { get; set; } [NotMapped] public string TitleHL { get; set; } [NotMapped] public string AbstHL { get; set; } }
本model中的TitleVector和AbstVector分別用來存放Title和Abst字段的分詞結果,便於後續的查詢。沒必要擔憂代碼會不當心改掉這些字段以致於查詢出錯,由於後續會設置一個觸發器,每次更改數據的時候都會自動更新這些字段的內容。post
using Microsoft.EntityFrameworkCore; public class MyDbContext : DbContext { protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder .UseNpgsql("Host=localhost;Database=ft;Username=postgres;Password=123456") .UseLoggerFactory(PgFtSearch.Program.MyLoggerFactory) .UseSnakeCaseNamingConvention(); protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Entity<Article>().HasIndex(p => p.TitleVector).HasMethod("GIN"); modelBuilder.Entity<Article>().HasIndex(p => p.AbstVector).HasMethod("GIN"); } public DbSet<Article> Articles { get; set; } }
首先UseNpgsql設置了要鏈接哪一個數據庫,而後UseLoggerFactory用來打印日誌,主要是sql語句。MyLoggerFactory是怎麼來的,參考後續的代碼。性能
GIN的兩行,用來告訴數據庫這兩個字段是採用倒排索引。測試
dotnet ef migrations add Initui
而後,在生成的migration文件中手動添加觸發器,在新增或者修改數據時,自動修改索引字段的內容,應用程序沒必要擔憂索引同步的問題。this
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);");
using System; using System.Collections.Generic; using System.Linq; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; namespace PgFtSearch { class Program { public static readonly ILoggerFactory MyLoggerFactory = LoggerFactory.Create(builder => { builder.AddConsole(); }); static void Main(string[] args) { using (var db = new MyDbContext()) { if (!db.Articles.Any()) { var articles = new List<Article>{ new Article{Title="testing is ok", Abst="this is a test about postgre full text searching"}, new Article{Title="tested all bugs", Abst="there is no bug exists in this app"} }; db.AddRange(articles); db.SaveChanges(); } var query = "test"; var data = 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 (var article in data) { Console.WriteLine($"{article.Title}\t{article.Abst}\t{article.TitleHL}\t{article.AbstHL}"); } } } } }
首先,若是沒有數據,插入幾條測試數據。
下面到了最關鍵的地方,編寫數據查詢的代碼,實現的具體功能是:
最終生成的SQL以下:
SELECT
a.title AS "Title",
a.abst AS "Abst",
ts_headline(a.title, to_tsquery(@__query_0)) AS "TitleHL",
ts_headline(a.abst, to_tsquery(@__query_0)) AS "AbstHL" FROM articles AS a WHERE (a.title_vector @@ plainto_tsquery(@__query_0)) OR (a.abst_vector @@ plainto_tsquery(@__query_0)) ORDER BY (ts_rank(a.title_vector, to_tsquery(@__query_0))::double precision * 2.0) + ts_rank(a.abst_vector, to_tsquery(@__query_0))::double precision DESC
代碼在這兒,相信你們都能看懂,有問題歡迎交流。
目前還未研究中文分詞的支持狀況,也沒有測試性能。不過大體看來,徹底能夠在中小型項目中使用postgres數據庫的內置全文檢索功能替代solr/es等搜索引擎,減小系統的複雜程度,提高全文檢索功能的穩定性。