在ef core中使用postgres數據庫的全文檢索功能實戰

起源

以前作的不少項目都使用solr/elasticsearch做爲全文檢索引擎,它們功能全面而強大,可是對於較小的項目而言,構建和維護成本顯然太高,尤爲是從關係數據庫/文檔數據庫到全文檢索引擎的數據同步工做很是繁瑣,且容易出錯。sql

記得好久之前就知道postgresql數據庫內置全文檢索,最近發現這個數據庫愈來愈火,因而就又研究了一番,欣喜的發現竟然支持ef core,因而對其進行了一些研究,並整理心得以下。數據庫

前提

本文假設讀者熟悉entity framework core的基本概念和基本使用。app

目的

創建dotnet core項目,使用postgres數據庫和ef core,實現常見的全文檢索功能,包括elasticsearch

  • 創建索引字段
  • 基本查詢
  • 查詢結果排名
  • 查詢結果高亮顯示

步驟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,不用寫煩人的引號。ide

步驟2 - 創建model和dbcontext

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的兩行,用來告訴數據庫這兩個字段是採用倒排索引。測試

步驟3 - 生成migration並手動添加觸發器

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

步驟4 - 編寫程序

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

首先,若是沒有數據,插入幾條測試數據。

下面到了最關鍵的地方,編寫數據查詢的代碼,實現的具體功能是:

  • 使用test關鍵字在title或abst字段中查詢數據
  • 對查詢結果進行排序,title字段排序權重=2.0,高於abst字段權重=1.0
  • 檢索結果的title和abst進行高亮顯示

最終生成的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等搜索引擎,減小系統的複雜程度,提高全文檢索功能的穩定性。

相關文章
相關標籤/搜索