پادشاهِ کُدنویسا شو!
کینگتو - آموزش برنامه نویسی تخصصصی - دات نت - سی شارپ - بانک اطلاعاتی و امنیت

ساخت موتور جستجوی معنایی با استفاده از Elasticsearch و NET.

16 بازدید 0 نظر ۱۴۰۵/۰۳/۲۱
در دنیای توسعه نرم‌افزار، سال‌هاست که کاربران به موتورهای جستجوی مبتنی بر متن (Lexical/Keyword Search) مانند الستیک‌سرچ کلاسیک، لوسین یا Solr عادت کرده‌اند. این موتورها بر اساس تطابق دقیق کلمات، ریشه‌یابی (Stemming) و الگوریتم‌های آماری مانند TF-IDF یا BM25 کار می‌کنند. در این مدل، اگر کاربر عبارت «خودروی سواری قرمز» را جستجو کند، موتور به دنبال دقیقاً همین کلمات یا هم‌خانواده‌های آن‌ها می‌گردد. اما اگر در سند ما عبارت «ماشین اتومبیل سرخ‌رنگ» آمده باشد، موتورهای سنتی احتمالاً آن را پیدا نمی‌کنند یا رتبه بسیار پایینی به آن می‌دهند؛ با وجود اینکه از نظر معنایی کاملاً هم‌ارز هستند. امروزه با ظهور هوش مصنوعی، پردازش زبان طبیعی (NLP) و مدل‌های زبانی بزرگ (LLMs)، معماری سیستم‌های جستجو دچار یک تحول بنیادین شده است. ما اکنون تمایل داریم که سیستم‌ها مقصود یا نیت کاربر (User Intent) و مفهوم سند (Contextual Meaning) را درک کنند. این دقیقاً همان جایی است که جستجوی معنایی (Semantic Search) یا جستجوی برداری (Vector Search) وارد میدان می‌شود. در این مقاله تخصصی به عنوان یک مهندس ارشد دات‌نت، معماری، چالش‌ها و پیاده‌سازی گام‌به‌گام یک موتور جستجوی معنایی پیشرفته را با استفاده از Elasticsearch (به عنوان وکتور دیتابیس) و .NET 8 بررسی خواهیم کرد.

مبانی نظری: امبدینگ (Embedding) و فضای برداری چیست؟

قبل از اینکه وارد کدنویسی شویم، باید زیرساخت ریاضی و مفهومی این سیستم را درک کنیم. قلب تپنده جستجوی معنایی، مفهومی به نام Dense Vectors (بردارهای متراکم) یا Embeddings است.

مدل‌های یادگیری عمیق (مانند بسترهای هگینگ‌فیس، BERT یا مدل‌های OpenAI Text Embedding) می‌توانند متون، جملات یا حتی کل یک سند را دریافت کرده و آن‌ها را به آرایه‌ای از اعداد اعشاری (فلوت) با ابعاد ثابت تبدیل کنند. این ابعاد بسته به مدل می‌تواند ۳۸۴، ۷۶۸ یا ۱۵۳۶ بعدی باشد.

وقتی متون به بردار تبدیل می‌شوند، در یک فضای چندبعدی ریاضی قرار می‌گیرند. جادوی کار اینجاست: متونی که مفهوم و معنای مشابهی دارند، در این فضای ریاضی نزدیک به یکدیگر قرار می‌گیرند.

سنجش شباهت برداری (Vector Similarity Metrics)

برای اینکه بفهمیم جستجوی کاربر چقدر به اسناد ما نزدیک است، از فرمول‌های ریاضی محاسبه فاصله استفاده می‌کنیم. الستیک‌سرچ از سه متد اصلی پشتیبانی می‌کند:

  1. Cosine Similarity (شباهت کسینوسی): زاویه بین دو بردار را می‌سنجد. جهت بردارها مهم است، نه طول آن‌ها. برای متون با طول‌های متفاوت بسیار عالی است.

  2. Dot Product (ضرب داخلی): اگر بردارها نرمال‌سازی شده باشند (طول ۱)، این سریع‌ترین روش محاسبه است.

  3. L2 Distance / Euclidean: فاصله مستقیم هندسی بین دو نقطه را می‌سنجد. هرچه کمتر باشد، شباهت بیشتر است.

 

معماری کلان سیستم (High-Level Architecture)

یک سیستم جستجوی معنایی واقعی معمولاً از دو خط لوله یا پایپ‌لاین مجزا تشکیل می‌شود:

الف) پایپ‌لاین ایندکس کردن (Ingestion/Indexing Pipeline)

  1. داده‌های خام از دیتابیس اصلی (مانند SQL Server) خوانده می‌شوند.

  2. متن اسناد به یک Embedding Service (محلی یا ابری) فرستاده می‌شود.

  3. خروجی مدل (که یک وکتور است) همراه با دیتای خام متنی، درون یک ایندکس الستیک‌سرچ با فلید نوع dense_vector ذخیره می‌شود.

ب) پایپ‌لاین جستجو (Search Pipeline)

  1. کاربر عبارت جستجوی خود را به زبان عامیانه وارد می‌کند.

  2. اپلیکیشن دات‌نت این عبارت را گرفته و به همان Embedding Service می‌فرستد تا بردار کوئری تولید شود.

  3. بردار کوئری به الستیک‌سرچ ارسال می‌شود.

  4. الستیک‌سرچ با استفاده از الگوریتم k-NN (k-Nearest Neighbors) سریع‌ترین و نزدیک‌ترین اسناد را پیدا کرده و به دات‌نت برمی‌گرداند.

 

پیکربندی Elasticsearch برای وکتورها

از نسخه 8.x به بعد، الستیک‌سرچ قابلیت‌های فوق‌العاده‌ای برای جستجوی برداری نیتیو تحت عنوان Hierarchical Navigable Small World (HNSW) ارائه داده است. الگوریتم HNSW یک گراف از بردارها می‌سازد تا بدون نیاز به گشتن تک‌تک اسناد (که بسیار کند است)، در زمان میلی‌ثانیه نزدیک‌ترین‌ها را پیدا کند.

ابتدا باید المان‌های ایندکس خود را تعریف کنیم. فرض کنید می‌خواهیم مقالات یک وب‌سایت خبری یا محصول را ذخیره کنیم. کوئری زیر را در نظر بگیرید که نگاشت (Mapping) ایندکس ما را در الستیک‌سرچ مشخص می‌کند:

PUT /semantic-articles
{
  "mappings": {
    "properties": {
      "id": { "type": "keyword" },
      "title": { "type": "text" },
      "content": { "type": "text" },
      "title_vector": {
        "type": "dense_vector",
        "dims": 384,
        "index": true,
        "similarity": "cosine",
        "index_options": {
          "type": "hnsw",
          "m": 16,
          "ef_construction": 100
        }
      }
    }
  }
}
  • نکته فنی: پارامتر dims نشان‌دهنده تعداد ابعاد بردار خروجی مدل هوش مصنوعی شماست (در اینجا ۳۸۴ مابازای مدل‌های سبک مانند all-MiniLM-L6-v2).

  • پارامتر m و ef_construction دقت و سرعت ساخت گراف HNSW را کنترل می‌کنند. مقادیر بالاتر دقت را بالا برده ولی زمان ایندکس و مصرف رم را افزایش می‌دهند.

 

پیاده‌سازی بک‌اند با .NET 8 و Elastic.Clients.Elasticsearch

مایکروسافت در .NET 8 اکوسیستم هوش مصنوعی خود را با معرفی کامپوننت‌های مدرنی نظیر Microsoft.Extensions.AI یکپارچه کرده است. ما برای تولید امبدینگ‌ها می‌توانند از پکیج‌های محلی مانند Ollama یا ابزارهای ابری استفاده کنیم. در این پیاده‌سازی، فرض را بر استفاده از یک کلاینت تمیز استاندارد دات‌نت می‌گذاریم.

ابتدا پکیج‌های ناگت زیر را نصب کنید:

dotnet add package Elastic.Clients.Elasticsearch --version 8.11.0
dotnet add package Microsoft.Extensions.AI

گام اول: طراحی مدل‌های داده (Data Models)

namespace SemanticSearch.Models;

public class Article
{
    public string Id { get; set; } = string.Empty;
    public string Title { get; set; } = string.Empty;
    public string Content { get; set; } = string.Empty;
    public float[]? TitleVector { get; set; }
}

public class SearchRequestModel
{
    public string Query { get; set; } = string.Empty;
    public int Size { get; set; } = 10;
}

گام دوم: اینترفیس و سرویس تولید وکتور (Embedding Service)

برای اینکه به معماری مایکروسافت وفادار بمانیم، یک سرویس ساده برای دریافت وکتورها می‌نویسیم. این سرویس می‌تواند به هر پرووایدر متصل شود، در اینجا فرض می‌کنیم به یک API محلی یا کلاینت متصل است:

using System.Net.Http.Json;

namespace SemanticSearch.Services;

public interface IEmbeddingService
{
    Task GetEmbeddingAsync(string text);
}

public class LocalEmbeddingService : IEmbeddingService
{
    private readonly HttpClient _httpClient;

    public LocalEmbeddingService(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public Task GetEmbeddingAsync(string text)
    {
        // در پروژه‌های واقعی اینجا از مدل‌های محلی ONNX با Microsoft.ML.OnnxRuntime
        // یا سرویس‌های ابری استفاده می‌شود.
        // خروجی فرضی یک آرایه فلوت ۳۸۴ بعدی است.
        throw new NotImplementedException("اتصال به مدل هوش مصنوعی برای تولید بردار");
    }
}

گام سوم: سرویس اصلی جستجو و ایندکس در الستیک‌سرچ

در این بخش هسته اصلی تعامل دات‌نت با الستیک‌سرچ نسخه ۸ را پیاده‌سازی می‌کنیم. توجه داشته باشید که در نسخه جدید کلاینت رسمی دات‌نت (Elastic.Clients.Elasticsearch)، پترن نوشتن کوئری‌ها به صورت Fluent API تغییرات شگرفی داشته است.

using Elastic.Clients.Elasticsearch;
using Elastic.Clients.Elasticsearch.QueryDsl;
using SemanticSearch.Models;

namespace SemanticSearch.Services;

public class SearchEngineService
{
    private readonly ElasticsearchClient _elasticClient;
    private readonly IEmbeddingService _embeddingService;
    private const string IndexName = "semantic-articles";

    public SearchEngineService(ElasticsearchClient elasticClient, IEmbeddingService embeddingService)
    {
        _elasticClient = elasticClient;
        _embeddingService = embeddingService;
    }

    // ۱. عملیات ایندکس کردن سند همراه با بردار معنایی آن
    public async Task IndexArticleAsync(Article article)
    {
        // تولید بردار برای عنوان مقاله
        article.TitleVector = await _embeddingService.GetEmbeddingAsync(article.Title);

        var response = await _elasticClient.IndexAsync(article, idx => idx.Index(IndexName));
        
        return response.IsSuccess();
    }

    // ۲. اجرای کوئری جستجوی معنایی خالص (K-NN)
    public async Task> SemanticSearchAsync(string userQuery, int limit = 5)
    {
        // تبدیل کوئری متنی متنی کاربر به بردار عددی
        float[] queryVector = await _embeddingService.GetEmbeddingAsync(userQuery);

        var response = await _elasticClient.SearchAsync

(s => s .Index(IndexName) .Knn(knn => knn .Field(f => f.TitleVector) .QueryVector(queryVector) .K(limit) .NumCandidates(100) ) ); if (!response.IsSuccess()) { // اینجا باید لاگین و پایش خطای عملیاتی صورت گیرد return Enumerable.Empty

(); } return response.Documents; } }

  • پارامتر NumCandidates چیست؟ این پارامتر تعیین می‌کند که در هر بخش (Shard) از الستیک‌سرچ چند سند کاندید بررسی اولیه شوند تا دقیق‌ترین‌ها بر اساس گراف استخراج گردند. افزایش این عدد دقت جستجو را بالا برده اما سرعت را کاهش می‌دهد.

 

جستجوی ترکیبی (Hybrid Search) و تکنیک Reciprocal Rank Fusion (RRF)

اگرچه جستجوی معنایی فوق‌العاده هوشمند است، اما یک پاشنه آشیل بزرگ دارد: جستجوی کلمات کلیدی خاص یا کدهای محصول!

به عنوان مثال، اگر کاربر عبارت «خطای ۵۰۰ در سیستم» یا شناسه کالا مثل «PRD-9021» را جستجو کند، مدل‌های معنایی ممکن است عملکرد ضعیف‌تری نسبت به لوسین سنتی دات‌نت (BM25) داشته باشند، زیرا این شناسه‌ها فاقد بار معنایی زبانی متعارف هستند.

[Image showing Hybrid Search combining BM25 lexical search and KNN vector search into a single pipeline]

راهکار استاندارد صنعتی در پروژه‌های واقعی، Hybrid Search (جستجوی ترکیبی) است. در این متدولوژی، ما یک بار جستجوی متنی (BM25) و یک بار جستجوی معنایی (k-NN) را به صورت همزمان اجرا کرده و نتایج را با روشی به نام RRF ترکیب می‌کنیم. الستیک‌سرچ این قابلیت را به صورت بومی در اختیار ما می‌گذارد.

بیایید متد سرچ را در سرویس دات‌نت خود ارتقا دهیم تا از جستجوی ترکیبی استفاده کند:

public async Task> HybridSearchAsync(string userQuery, int limit = 5)
{
    float[] queryVector = await _embeddingService.GetEmbeddingAsync(userQuery);

    var response = await _elasticClient.SearchAsync

(s => s .Index(IndexName) // ۱. جستجوی متنی سنتی روی فیلدهای متنی .Query(q => q .MultiMatch(mm => mm .Fields(new[] { "title", "content" }) .Query(userQuery) ) ) // ۲. جستجوی معنایی برداری .Knn(knn => knn .Field(f => f.TitleVector) .QueryVector(queryVector) .K(limit) .NumCandidates(100) ) // ۳. ادغام نتایج بر اساس الگوریتم RRF .Rank(r => r.Rrf(rrf => rrf.WindowSize(50).RankConstant(60))) .Size(limit) ); return response.Documents; }

با این کار، شما بهترین‌های هر دو جهان را دارید: اگر کاربر کلمه کلیدی دقیقی بزند، بخش متنی کارش را انجام می‌دهد و اگر مفهومی بنویسد، بخش معنایی وارد عمل می‌شود.

 

ملاحظات عملکردی، بهینه‌سازی و چالش‌های محیط عملیاتی (Production)

وقتی فرآیند پیاده‌سازی محلی تمام می‌شود، در محیط Production با چالش‌های مقیاس‌پذیری روبه‌رو می‌شوید که نادیده گرفتن آن‌ها می‌تواند کل سرور را از کار بیندازد.

۱. مصرف حافظه RAM در الستیک‌سرچ

الگوریتم HNSW برای اینکه بتواند پاسخ کوئری‌ها را در سطح زیر ۱۰ میلی‌ثانیه نگه‌دارد، کل گراف بردارها را درون Off-Heap Memory (حافظه مستقیم سیستم‌عامل خارج از JVM) لود می‌کند.

  • فرمول محاسبه حدودی رم مورد نیاز:

     

    حافظه = تعداد اسناد × ابعاد × 4 بایت × 1.5

    اگر ۱۰ میلیون سند با بردار ۷۶۸ بعدی داشته باشید، به حداقل ۴۵ گیگابایت فضای رم فعال فقط برای سرچ وکتورها نیاز دارید.

  • راهکار بهینه‌سازی: استفاده از تکینک‌های Quantization (مانند اسکالر کوانتیزاسیون INT8 در الستیک‌سرچ) که ابعاد داده‌ها را فشرده کرده و مصرف رم را تا ۷۵٪ کاهش می‌دهد بدون اینکه افت دقت شدیدی رخ دهد.

۲. تکنیک‌های Bulk Indexing در دات‌نت

  • هیچ‌گاه اسناد را تک‌تک ایندکس نکنید. فرآیند فراخوانی API مدل هوش مصنوعی برای تولید بردار سنگین است. در دات‌نت باید از پترن‌های همزمان نظیر Task.WhenAll یا کامپوننت‌های توکار الستیک‌سرچ مانند BulkAllObservable استفاده کنید تا داده‌ها را در چانک‌های (مثلاً ۱۰۰۰ تایی) آپلود کنید.

public async Task BulkIndexAsync(IEnumerable

articles) { // تولید بردارها به صورت بچ یا موازات کنترل شده با SemaphoreSlim foreach (var article in articles) { article.TitleVector = await _embeddingService.GetEmbeddingAsync(article.Title); } var bulkResponse = await _elasticClient.BulkAsync(b => b .Index(IndexName) .IndexMany(articles) ); if (bulkResponse.HasErrors) { // پردازش خطاهای ثبت بچ } }

۳. مدیریت چرخه عمر لایف‌تایم کلاینت الستیک‌سرچ

  • دقت کنید که شیء ElasticsearchClient در دات‌نت کاملاً Thread-Safe است و باید به عنوان یک سرویس Singleton در کانتینر Dependency Injection سیستم ثبت شود تا از باز شدن بیهوده اتصالات TCP (مشکل Socket Exhaustion) جلوگیری به عمل آید.

جمع‌بندی

طراحی و ساخت یک موتور جستجوی معنایی پیشرفته با ترکیب دات‌نت ۸ و الستیک‌سرچ، راهکاری بسیار مدرن و صنعتی برای ارتقای تجربه کاربری سیستم‌های نرم‌افزاری بزرگ است. ما در این مقاله آموختیم که چگونه متن‌ها را به بردار تبدیل کنیم، نگاشت‌های HNSW را در الستیک‌سرچ بنویسیم، جستجوی معنایی خالص انجام دهیم و در نهایت با تکنیک Hybrid Search و الگوریتم پیشرفته RRF، دقت سیستم را به بیشترین حد ممکن برسانیم. رعایت اصول مدیریت حافظه رم در کلاستر الستیک‌سرچ و بهره‌گیری از معماری همزمان در دات‌نت، ضامن پایداری سیستم شما در زیر لودهای کاری شدید دنیای واقعی خواهد بود.

 
لینک استاندارد شده: B4EfY7H

0 نظر

    هنوز نظری برای این مقاله ثبت نشده است.
جستجوی مقاله و آموزش
دوره‌ها با تخفیفات ویژه