ABP VNext + RediSearch:微服務級全文檢索

ABP VNext + RediSearch:微服務級全文檢索 🚀


📚 目錄

  • ABP VNext + RediSearch:微服務級全文檢索 🚀
    • 📚 一、背景與動機 🚀
    • 🛠? 二、環境與依賴 🐳
      • 2.1 Docker Compose 啟動 Redis Stack
      • 2.2 Kubernetes 部署(示例 Manifest)
      • 2.3 ABP VNext & NuGet 包
    • 🏗? 三、架構與流程圖 🏗?
    • 🔧 四、索引模型與依賴注入 🔧
      • 4.1 模型定義
      • 4.2 服務注冊
    • 🛠? 五、IndexService & SearchService 實現 🛠?
      • 接口
      • `RedisOmIndexService`
      • `RedisOmSearchService`
    • ?? 六、數據同步策略 🔄
      • 6.1 EF Core 批量擴展
      • 6.2 實時新增/更新/刪除
      • 6.3 批量重建:`RebuildIndexJob`
    • 📄 七、復雜查詢示例 🔍
    • 📊 八、性能對比測試示例腳本 📈
    • 🚦 九、生產最佳實踐 & 陷阱提示 ??
    • 📂 參考資料 📚


? TL;DR

  • 🚀 利用 Redis Stack(內置 RediSearch)+ Redis.OM,在 ABP VNext 微服務中實現毫秒級全文檢索
  • 🐳 Docker Compose & 🎯 Kubernetes Manifest:持久化、ACL 認證、RedisInsight 可視化
  • 🏷? 全局 Prefix + 動態 IndexName,完美隔離多租戶索引與數據
  • 🔄 完整功能:索引創建/刪除/寫入/批量、實時/刪除同步、批量重建、Polly 重試
  • 🔍 支持全文、Tag、數值、地理、Facet 聚合;📈 性能對比 PostgreSQL LIKE/FTS vs. RediSearch
  • 🔒 生產建議:AOF/RDB、ACL、Pre-commit/SAST、監控 & 慢查詢、Testcontainers 集成測試

📚 一、背景與動機 🚀

傳統關系型數據庫全文檢索(LIKE '%關鍵詞%' 或 FTS)在微服務、高并發場景下常遇:

  • 性能瓶頸:百萬級文檔延時 100+ ms;
  • 功能受限:地理半徑、Facet 聚合需自研;
  • 擴展復雜:分片與高可用運維成本高。

RediSearch 基于內存倒排索引,支持次毫秒級響應實時更新地理 & 聚合,完美契合高吞吐、低延遲檢索需求。


🛠? 二、環境與依賴 🐳

2.1 Docker Compose 啟動 Redis Stack

version: "3.8"
services:redis:image: redis/redis-stack:latestcontainer_name: redis-stackports:- "6379:6379"- "8001:8001"volumes:- redis-data:/datacommand:- redis-server- --requirepass YourStrong!Pass- --appendonly yes
volumes:redis-data:
  • 🔐 安全--requirepass 強制認證
  • 💾 持久化--appendonly yes 開啟 AOF
  • 🔍 GUI:訪問 http://localhost:8001 使用 RedisInsight

提示:Docker Compose v3 下資源限制字段無效,如需限內存請用 Swarm 或 CLI 參數 --memory

docker-compose up -d

2.2 Kubernetes 部署(示例 Manifest)

apiVersion: apps/v1
kind: Deployment
metadata:name: redis-stack
spec:replicas: 1selector: { matchLabels: { app: redis-stack } }template:metadata: { labels: { app: redis-stack } }spec:containers:- name: redisimage: redis/redis-stack:latestargs: ["redis-server", "--requirepass", "YourStrong!Pass", "--appendonly", "yes"]ports:- containerPort: 6379- containerPort: 8001volumeMounts:- mountPath: /dataname: redis-datavolumes:- name: redis-datapersistentVolumeClaim:claimName: redis-pvc
---
apiVersion: v1
kind: Service
metadata:name: redis-stack
spec:type: ClusterIPports:- port: 6379- port: 8001selector:app: redis-stack

2.3 ABP VNext & NuGet 包

dotnet add package Redis.OM
dotnet add package StackExchange.Redis
dotnet add package Volo.Abp.Caching.StackExchangeRedis
dotnet add package Polly

appsettings.json

{"Abp": {"DistributedCache": {"Redis": {"Configuration": "localhost:6379,password=YourStrong!Pass,allowAdmin=true","InstanceName": "MyApp:"}}}
}

🏗? 三、架構與流程圖 🏗?

服務端
SaveChanges
領域事件
Upsert/Delete
BulkInsert
SearchAsync
FT.SEARCH
API/ApplicationService
EF Core → PostgreSQL
RediSearch 索引 ← Redis.OM
DataSyncHandler
RebuildIndexJob
前端

🔧 四、索引模型與依賴注入 🔧

4.1 模型定義

using Redis.OM.Modeling;[Document(IndexName = "product-idx")]  // 基礎索引名
public class ProductIndex
{[RedisIdField]                   // 主鍵public string Id { get; set; }[Searchable]                     // 全文public string Name { get; set; }[Indexed(IsTag = true)]          // Tagpublic string Category { get; set; }[Indexed(IsSortable = true)]     // 數值/排序public decimal Price { get; set; }[Indexed(IsGeo = true)]          // 地理public GeoLoc Location { get; set; }
}

4.2 服務注冊

public override void ConfigureServices(ServiceConfigurationContext context)
{// 1. ABP Redis 緩存context.Services.AddStackExchangeRedisCache(options => {});// 2. ConnectionMultiplexercontext.Services.AddSingleton(sp =>ConnectionMultiplexer.Connect(sp.GetRequiredService<IConfiguration>().GetSection("Abp:DistributedCache:Redis:Configuration").Value));// 3. Redis.OM Providercontext.Services.AddSingleton(sp =>{var mux      = sp.GetRequiredService<ConnectionMultiplexer>();var tenantId = sp.GetService<ICurrentTenant>()?.GetId()?.ToString() ?? "global";return new RedisConnectionProvider(new RedisConnectionProviderOptions{RedisConnection = mux,Prefix          = $"tenant:{tenantId}:"});});// 4. 注入索引/搜索服務context.Services.AddTransient<IIndexService, RedisOmIndexService>();context.Services.AddTransient<ISearchService, RedisOmSearchService>();
}

注意Prefix 僅對文檔 HashKey 生效,不會自動修改 FT.CREATE 的索引名。若需隔離多租戶索引,需在 CreateIndexAsync/DropIndexAsync 中手動拼接:

var indexName = $"{prefix}product-idx";

🛠? 五、IndexService & SearchService 實現 🛠?

接口

public interface IIndexService
{Task CreateIndexAsync<T>() where T : class;Task DropIndexAsync<T>()   where T : class;Task UpsertAsync<T>(T doc) where T : class;Task DeleteAsync<T>(string id) where T : class;Task BulkInsertAsync<T>(IEnumerable<T> docs) where T : class;
}public interface ISearchService
{Task<SearchResult<T>> SearchAsync<T>(string query, int skip = 0, int take = 20) where T : class;Task<SearchResult<T>> SearchAsync<T>(SearchDefinition def) where T : class;
}

RedisOmIndexService

public class RedisOmIndexService : IIndexService
{private readonly RedisConnectionProvider _prov;private readonly IDatabase _db;private readonly string _prefix;public RedisOmIndexService(RedisConnectionProvider prov,ConnectionMultiplexer mux){_prov   = prov;_db     = mux.GetDatabase();_prefix = prov.Prefix;  // 如 "tenant:1:"}public Task CreateIndexAsync<T>() where T : class{var baseIdx = _prov.RedisCollection<T>().IndexName;var idxName = $"{_prefix}{baseIdx}";// 使用 Redis.OM 默認 schemareturn _db.ExecuteAsync("FT.CREATE",idxName, "ON", "HASH","PREFIX", "1", $"{_prefix}{typeof(T).Name.ToLowerInvariant()}:","SCHEMA", /* ... schema args ... */);}public async Task DropIndexAsync<T>() where T : class{var idxName = $"{_prefix}{_prov.RedisCollection<T>().IndexName}";var rl      = (RedisResult[])await _db.ExecuteAsync("FT._LIST");var list    = rl.Select(r => (string)r).ToArray();if (list.Contains(idxName))await _db.ExecuteAsync("FT.DROPINDEX", idxName, "DD");}public Task UpsertAsync<T>(T doc) where T : class=> _prov.RedisCollection<T>().InsertAsync(doc);public Task DeleteAsync<T>(string id) where T : class=> _prov.RedisCollection<T>().DeleteAsync(id);public async Task BulkInsertAsync<T>(IEnumerable<T> docs) where T : class{// 限制并發,防止瞬時打垮 Redisusing var sem = new SemaphoreSlim(50);var tasks = docs.Select(async d =>{await sem.WaitAsync();try { await _prov.RedisCollection<T>().InsertAsync(d); }finally { sem.Release(); }});await Task.WhenAll(tasks);}
}

RedisOmSearchService

public class RedisOmSearchService : ISearchService
{private readonly RedisConnectionProvider _prov;public RedisOmSearchService(RedisConnectionProvider prov) => _prov = prov;public async Task<SearchResult<T>> SearchAsync<T>(string query, int skip = 0, int take = 20) where T : class{var col = _prov.RedisCollection<T>();var res = await col.SearchAsync(new SearchDefinition(query).Limit(skip, take));return new SearchResult<T>{Items = res.Documents.Select(d => d.Object).ToList(),Total = res.TotalResults};}public async Task<SearchResult<T>> SearchAsync<T>(SearchDefinition def) where T : class{var col = _prov.RedisCollection<T>();var res = await col.SearchAsync(def);return new SearchResult<T>{Items = res.Documents.Select(d => d.Object).ToList(),Total = res.TotalResults};}
}

?? 六、數據同步策略 🔄

6.1 EF Core 批量擴展

public static class IQueryableExtensions
{public static async IAsyncEnumerable<List<T>> BatchAsync<T>(this IQueryable<T> source, int size){var total = await source.CountAsync();for (int i = 0; i < total; i += size)yield return await source.Skip(i).Take(size).ToListAsync();}
}

6.2 實時新增/更新/刪除

// 新增/更新
public class ProductChangedHandler: ILocalEventHandler<EntityChangedEventData<Product>>
{private readonly IIndexService _idx;private readonly AsyncPolicy _retry = Policy.Handle<Exception>().WaitAndRetryAsync(new[]{TimeSpan.FromMilliseconds(50),TimeSpan.FromMilliseconds(100)});public ProductChangedHandler(IIndexService idx) => _idx = idx;public async Task HandleEventAsync(EntityChangedEventData<Product> e){var doc = new ProductIndex {Id       = e.Entity.Id.ToString(),Name     = e.Entity.Name,Category = e.Entity.Category,Price    = e.Entity.Price,Location = new GeoLoc(e.Entity.Lat, e.Entity.Lng)};await _retry.ExecuteAsync(() => _idx.UpsertAsync(doc));}
}// 刪除
public class ProductDeletedHandler: ILocalEventHandler<EntityDeletedEventData<Product>>
{private readonly IIndexService _idx;public ProductDeletedHandler(IIndexService idx) => _idx = idx;public Task HandleEventAsync(EntityDeletedEventData<Product> e)=> _idx.DeleteAsync<ProductIndex>(e.EntityId.ToString());
}

6.3 批量重建:RebuildIndexJob

public class RebuildIndexJob : IBackgroundJob
{private readonly IRepository<Product, Guid> _repo;private readonly IIndexService _idx;public RebuildIndexJob(IRepository<Product, Guid> repo, IIndexService idx){_repo = repo; _idx = idx;}public async Task ExecuteAsync(){await _idx.DropIndexAsync<ProductIndex>();await _idx.CreateIndexAsync<ProductIndex>();var q = _repo.WithDetails().Select(p => new ProductIndex {Id       = p.Id.ToString(),Name     = p.Name,Category = p.Category,Price    = p.Price,Location = new GeoLoc(p.Lat, p.Lng)});await foreach (var batch in q.BatchAsync(500))await _idx.BulkInsertAsync(batch);}
}

📄 七、復雜查詢示例 🔍

// 1. 簡單全文
var r1 = await _search.SearchAsync<ProductIndex>("\"wireless headphones\"", 0, 20);// 2. Tag + Range + Geo + 排序
var def = new SearchDefinition().FilterByTag(nameof(ProductIndex.Category), "Audio").FilterByRange(nameof(ProductIndex.Price), 50, 200).FilterByGeo(nameof(ProductIndex.Location), lat, lng, 10).OrderByDescending(nameof(ProductIndex.Price)).Limit(0, 20);
var r2 = await _search.SearchAsync<ProductIndex>(def);// 3. Facet 聚合
var fdef = new SearchDefinition("headphones").AddFacet(nameof(ProductIndex.Category));
var agg = await _search.SearchAsync<ProductIndex>(fdef);

📊 八、性能對比測試示例腳本 📈

public async Task TestPerformanceAsync()
{var db  = new MyAppDbContext();var sw  = new Stopwatch();var idx = _search;sw.Start();await db.Products.Where(p => EF.Functions.Like(p.Name, "%headphones%")).ToListAsync();Console.WriteLine($"SQL LIKE: {sw.ElapsedMilliseconds} ms");sw.Restart();await db.Products.Where(p => EF.Functions.ToTsVector("english", p.Name).Matches(EF.Functions.PlainToTsQuery("english", "headphones"))).ToListAsync();Console.WriteLine($"PostgreSQL FTS: {sw.ElapsedMilliseconds} ms");sw.Restart();await idx.SearchAsync<ProductIndex>("headphones");Console.WriteLine($"RediSearch: {sw.ElapsedMilliseconds} ms");
}

🚦 九、生產最佳實踐 & 陷阱提示 ??

  1. 持久化 & 安全

    • --appendonly yes + 掛載 /data
    • ACL/requirepass + 客戶端配置密碼;
  2. 多租戶索引隔離

    • Prefix 僅對文檔 Key 生效;

    • 手動拼接索引名:

      var idxName = $"{prefix}product-idx";
      
  3. 異常 & 重試

    • Polly 重試 + CancellationToken 超時;
  4. 監控 & 告警

    • FT.SLOWLOG、Redis slowlog;
    • RedisInsight/Prometheus Exporter;
  5. 安全掃描 & 質量

    • Pre-commitdotnet-format、StyleCop;
    • 依賴掃描:OWASP Dependency-Check;
    • SAST:GitHub CodeQL/SonarQube;
  6. 集成測試

    • Testcontainers 啟動 Redis Stack,覆蓋 CRUD/Search;

📂 參考資料 📚

  • Redis Stack
  • Redis OM .NET
  • ABP 文檔

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/88002.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/88002.shtml
英文地址,請注明出處:http://en.pswp.cn/web/88002.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

TensorFlow深度學習實戰——基于自編碼器構建句子向量

TensorFlow深度學習實戰——基于自編碼器構建句子向量 0. 前言1. 句子向量2. 基于自編碼器構建句子向量2.1 數據處理2.2 模型構建與訓練 3. 模型測試相關鏈接 0. 前言 在本節中&#xff0c;我們將構建和訓練一個基于長短期記憶 (Long Short Term Memory, LSTM) 的自編碼器&…

C語言使用Protobuf進行網絡通信

筆者前面博文Go語言網絡游戲服務器模塊化編程介紹了Go語言在開發網絡游戲時如何進行模塊化編程&#xff0c;在其中使用了Protobuf進行網絡通信。在Protobuf官方實現中并沒有生成C語言的實現&#xff0c;不過有一個開源的protobuf-c可以使用。 先來看看protobuf-c生成的代碼&am…

vue3 隨手筆記12--組件通信方式9/5--useAttrs

一 什么是useAttrsuseAttrs 是 Vue 3 Composition API 中提供的一個函數&#xff0c;它屬于 Vue 的組合式 API 工具集的一部分。通過 useAttrs&#xff0c;你可以訪問傳遞給組件但未被聲明為 props 的所有屬性。這對于處理非 prop 特性&#xff08;attributes&#xff09;特別有…

HumanRisk-自動化安全意識與合規教育平臺方案

權威數據顯示&#xff0c;74%以上的數據泄露與網絡安全事件歸根結底與人為因素有關&#xff0c;60%以上的網絡安全事件是由內部人員失誤造成的。這一現狀揭示了一個核心命題&#xff1a;網絡安全威脅正從技術漏洞轉向“人為因素風險”。Gartner的調查發現&#xff0c;即便接受了…

2025年食品科學與健康大數據國際會議(SHBD 2025)

2025年食品科學與健康大數據國際會議 2025 International Conference on Food Science and Health Big Data&#xff08;一&#xff09;大會信息 會議簡稱&#xff1a;ICFSHBD 2025 大會地點&#xff1a;中國上…

CompareFace人臉識別算法環境部署

一、docker 安裝 步驟1&#xff1a;啟用系統功能 右鍵開始菜單 → 應用和功能 → 點擊 程序和功能 → 勾選 Hyper-V 和 Windows子系統Linux 步驟2&#xff1a;獲取安裝包 訪問Docker官網安裝包下載頁 &#xff0c;下載「Docker Desktop Installer.rar」壓縮包 步驟3&#…

STM32固件升級設計——內部FLASH模擬U盤升級固件

目錄 一、功能描述 1、BootLoader部分&#xff1a; 2、APP部分&#xff1a; 二、BootLoader程序制作 1、分區定義 2、 主函數 3、配置USB 4、配置fatfs文件系統 5、程序跳轉 三、APP程序制作 四、工程配置&#xff08;默認KEIL5&#xff09; 五、運行測試 結束語…

操作系統引導過程

操作系統引導是指計算機利用 CPU 運行特定程序&#xff0c;通過程序識別硬盤&#xff0c;識別硬盤分區&#xff0c;識別硬盤分區上的操作系統&#xff0c;最后通過程序啟動操作系統。 引導流程&#xff08;8步核心環節&#xff09; 1. 激活CPU 加電后CPU自動讀取 ROM中的Boot…

Safetensors與大模型文件格式全面解析

Safetensors是一種專為存儲大型張量數據設計的文件格式&#xff0c;由Hugging Face團隊開發&#xff0c;旨在提供安全高效的模型參數存儲解決方案。下面將詳細介紹Safetensors格式及其特點&#xff0c;并全面梳理當前主流的大模型文件格式。 一、Safetensors格式詳解 1. 基本概…

分布式理論:CAP、Base理論

目錄 1、CAP理論 1.1、介紹 1.2、CAP的三種選擇 1.3、CAP的注意事項 2、BASE理論 2.1、定義介紹 2.2、最終一致性的介紹 2.3、BASE的實現方式 2.4、與ACID的對比 3、CAP與BASE的聯系 4、如何選擇CAP 前言 在分布式系統中&#xff0c;CAP理論和BASE理論是指導系統設計…

【最新】飛算 JavaAl安裝、注冊,使用全流程,讓ai自己給你寫代碼,解放雙手

目錄 飛算 JavaAl 產品介紹 安裝飛算 JavaAl 第一步&#xff1a;點擊 File->Setting 第二步&#xff1a;點擊 Plugins 第三步&#xff1a;搜索 CalEx-JavaAI 第四步&#xff1a;點擊 Install 進行安裝 第五步&#xff1a;點擊 Install &#xff0c;查看安裝好的飛算…

無人設備遙控器之姿態控制算法篇

無人設備遙控器的姿態控制算法通過傳感器數據融合、控制算法優化和執行機構調節實現動態平衡&#xff0c;核心算法包括PID控制、自適應控制、模型預測控制&#xff08;MPC&#xff09;&#xff0c;以及數據融合中的互補濾波和卡爾曼濾波&#xff0c;同時涉及四元數算法和深度強…

【加解密與C】Base系列(三)Base85

Base85 編碼簡介 Base85&#xff08;也稱為 Ascii85&#xff09;是一種二進制到文本的編碼方案&#xff0c;用于將二進制數據轉換為可打印的ASCII字符。它的效率高于Base64&#xff0c;但生成的字符串可能包含特殊字符&#xff08;如引號或反斜杠&#xff09;&#xff0c;需在…

Docker企業級應用:從入門到生產環境最佳實踐

一、Docker核心概念與架構 1.1 Docker技術棧 #mermaid-svg-CUEiyGo05ZYG524v {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-CUEiyGo05ZYG524v .error-icon{fill:#552222;}#mermaid-svg-CUEiyGo05ZYG524v .error-te…

8、保存應用數據

目錄用戶首選項的使用用戶首選項主要API用戶首選項開發流程用戶首選項開發實踐關系型數據庫的使用關系型數據庫工作流程關系型數據庫開發實踐用戶首選項的使用 用戶首選項主要API 用戶首選項開發流程 成功的獲取了一個名為myStore的Preferences實例 保存了一個鍵值對&#x…

(C++)list列表相關基礎用法(C++教程)(STL庫基礎教程)

源代碼&#xff1a;#include <iostream> #include <list>using namespace std;int main(){list<int> numbers{10,20,30};numbers.push_front(5);numbers.push_back(40);auto it numbers.begin();advance(it,2);numbers.insert(it,15);cout<<"該列…

Spring CGLIB私有方法訪問成員變量為null問題

場景 代碼 RestController public class TestJob {Autowiredprivate XxService xxService;XxlJob("testCGLIB")private void doTest(){System.out.println("方法調用");System.out.println("成員變量注入:"(xxService!null));this.doInnerTest()…

Paimon本地表查詢引擎LocalTableQuery詳解

LocalTableQueryLocalTableQuery 是 Paimon 中實現本地化、帶緩存的表查詢的核心引擎。它的主要應用場景是 Flink 中的 Lookup Join。當 Flink 作業需要根據一個流中的 Key 去關聯一個 Paimon 維表時&#xff0c;LocalTableQuery 可以在 Flink 的 TaskManager 節點上&#xff0…

使用協程簡化異步資源獲取操作

異步編程的兩種場景 在異步編程中&#xff0c;回調函數通常服務于兩種不同場景&#xff1a; 一次性資源獲取&#xff1a;等待異步操作完成并返回結果。持續事件通知。監聽并響應多個狀態變更。 Kotlin為這兩種場景提供了解決方案&#xff1a;使用掛起函數簡化一次性資源獲取…

ABP VNext + Cosmos DB Change Feed:搭建實時數據變更流服務

ABP VNext Cosmos DB Change Feed&#xff1a;搭建實時數據變更流服務 &#x1f680; &#x1f4da; 目錄ABP VNext Cosmos DB Change Feed&#xff1a;搭建實時數據變更流服務 &#x1f680;TL;DR ?&#x1f680;1. 環境與依賴 &#x1f3d7;?2. 服務注冊與依賴注入 &…