اذهب إلى المحتوى

استخدام محرك بحث لبلب العربي كمحرك بحث داخلي لمحتويات موقعك


أكاديميّة حسوب

المحتوى هو أحد أهم العناصر في موقع الويب أيًا كان نوعه ووظيفته، إذ بنيت المواقع والتطبيقات للمستخدمين حتى يستفيدوا من محتواها سواء لقراءة أو تعلم أو تسوق وما إلى ذلك. فثبت عبر تجارب سلوكيات المستخدمين أن نصفهم يجرون عمليات البحث ضمن المواقع التي يتصفحونها، ومن هنا نشأت الحاجة إلى استخدام المطورين لأساليب بحث داخلية في مواقعهم، والأفضل من ذلك استخدام محركات بحث داخلية، حيث يوفر محرك البحث الداخلي تجربة أفضل للمستخدم تجعل الوصول إلى ما يريده من الموقع عملية سريعة وبسيطة، خاصة إذا كان الموقع يعرض منتجات تجارية مثلًا ويريد المستخدم أن يبحث عما يريد، أو إذا كان موقع محتوى متخصصًا وأراد المستخدم أن يبحث عن موضوع معين فيه.

كم وافر من المحتوى الموجود على الإنترنت موجود باللغة الإنكليزية، وهنالك وسائل ومحركات بحث وفيرة تتيح إمكانية البحث باللغة الإنكليزية، ولكن عندما يأتي الأمر إلى المستخدم العربي والمحتوى العربي، نجد قصورًا في جودة البحث إلى الدرجة التي تجعل البحث الداخلي في المواقع عملية دون فائدة، فأتى محرك البحث العربي لبلب ليحل هذه المشكلة بالاعتماد على أفضل الأساليب والتقنيات، وبمجهود موجه للجمهور المتحدث بالعربية.

سنبني في هذا المقال موقع ويب يعرض مقالات، بشكل مبسط، باستخدام Node.js وقاعدة بيانات MySQL (أو MariaDB) وسنرى معًا الفارق بين وظيفة البحث التي سننشئها يدويًا، وبين نتائج البحث التي يعطينا إياها لبلب عند استخدامه في الموقع، فهيا بنا.

جدول المحتويات

التهيئة الأولية للمشروع

    سننشئ مجلدًا جديدًا باسم my-blog للمشروع ونفتحه باستخدام محرر Visual Studio Code وبعدها نفتح نافذة سطر الأوامر، وننفذ الأمر التالي فيها لإنشاء مشروع Node.js جديد:

    npm init

    اختر ملف الانطلاق app.js عند السؤال عن ذلك ثم أنشئه بعدها في جذر المجلد بإنشاء ملف جديد ضمن محرر الأكواد vscode. يجب أن ننتبه إلى أهمية تنظيم ملفات عمل المشروع، فمن المهم جدًا أن تكون أسماء المجلدات دالةً على وظيفتها، وأن تكون الملفات مقسمةً تقسيمًا صحيحًا.

    علينا أن ننشِئ مجلدًا للضبط باسم config ونضع بداخله ملفًا لإنشاء قاعدة البيانات والجدول articles إن لم تكن موجودةً، لكن قبل ذلك، علينا أن نثبت بعض الحزم من مدير الحزم npm لتضمين الوحدات modules التي تلزمنا في عملنا. اكتب ما يلي في سطر الأوامر:

    npm install express express-session body-parser sequelize uuid ejs mysql

    سنشرح سريعًا وظيفة كل حزمة من الحزم السابقة:

    • express: هي إطار العمل المساعد في تطبيقات node.js، وحزمة express-session هي حزمة تعريف الجلسة لكل مستخدم حيث ستفيدنا في عملية البحث.
    • body-parser: هي الأداة التي ستساعدنا في قراءة الطلبات requests.
    • sequelize: هي عبارة عن orm (وسيلة تواصل بين الخادم وقاعدة البيانات تتولى جميع الاستفسارات بين التطبيق والخادم وقاعدة البيانات) سيساعدنا في إدارة عمليات قاعدة البيانات.
    • uuid: هي أداة تولد معرفات فريدة عالميًا.
    • ejs: هي محرك العرض الذي سنستخدمه لعرض صفحاتنا.
    • mysql: هي حزمة للتواصل مع قواعد البيانات.

    يمكنك الرجوع دائمًا إلى موقع npm لمعرفة المزيد عن هذه الحزم.

    سننشِئ قاعدة بيانات باسم myBlog، سنبدأ بكتابة الملف dbCreation.js ضمن مجلد config كما يلي، لا تنسَ تغيير معلومات الاتصال من المستخدم الجذر root أو أي مستخدم آخر وتغيير كلمة المرور password إلى كلمة المرور الخاصة بك:

    //تنفذ الأوامر الموجودة فى هذا الملف فى بداية إنشاء المشروع فقط
    let mysql = require('mysql');
    
    //معلومات الاتصال بالخادم المحلى 
    let con = mysql.createConnection({
      host: "localhost",
      user: "root",
      password: "password",
      multipleStatements: true 
    });
    
    //تعريف أمر إنشاء قاعدة البيانات وإنشاء الجدول المستخدم فى التطبيق
    let sqlCommand = `
    CREATE DATABASE IF NOT EXISTS myBlog CHARACTER SET utf8 COLLATE utf8_general_ci;
    
    use myBlog;
    
    CREATE TABLE IF NOT EXISTS articles (
        id int AUTO_INCREMENT,
        title varchar(200),
        author varchar(100),
        content text,
        tags varchar(500),
        createdAt datetime,
        updatedAt datetime,
        PRIMARY KEY (id)
    );
    `
    con.connect(function(err) {
    
        //تنفيذ إنشاء قاعدة البيانات 
        con.query(sqlCommand, function (err, result) {
            if (err) throw err;
        });
    
    });

    تخزن قاعدة البيانات معلومات كل مقال كما يوضح الكود إذ تحتوي على جدول فيه الحقول الآتية:

    • حقل id رقمي من النوع int والذي يمثل معرِّف المقال وهو المفتاح الرئيسي primary key للجدول، ويزداد تلقائيًا.
    • الحقول title و author و tags لتخزين عنوان المقال واسم كتابها والوسوم المستعملة فيها وذلك على التتالي وبالترتيب، وهي من النوع varchar.
    • الحقل content من النوع text الذي يمثل محتوى المقال.
    • الحقلان createdAt و updatedAt لتخزين متى أُنشِئت المقالة ومتى حدثت، وهما من النوع datetime.

    بعد إنشاء قاعدة البيانات وضبطها، لنشغل الخادم عبر كتابة الكود التالي في ملف app.js:

    const express = require("express");
    const bodyParser = require("body-parser");
    const session = require("express-session");
    const uuid = require("uuid");
    const errorHandler = require("./middleware/errorHandler");
    const db = require("./config/dbCreation");
    
    // ‫لإنشاء التطبيق باستخدام express
    const app = express();
    
    //تحديد محرك العرض
    app.set("views", `${__dirname}/views`);
    app.set("view engine", "ejs");
    
    // ‫‫خيارات حزمة body-parser
    app.use(bodyParser.urlencoded({ extended: true }));
    app.use(bodyParser.json({ extended: true }));
    
    // تعيين المجلد الحاوي للملفات القابلة للإرسال
    app.use('/public', express.static('public'));
    
    // تحديد المنفذ
    const port = process.env.PORT || 5000;
    
    // ‫تحديد session secret
    const sessionSecret = "keyboard cat";
    
    // استخدام حزمة الجلسة لتمييز كل مستخدم
    app.use(
      session({
        genid: (req) => {
          return uuid.v4();
        },
        secret: sessionSecret,
        resave: true,
        saveUninitialized: true
      })
    );
    
    // ‫استدعاء المستقبل article routes
    require("./routes/article.routes")(app);
    
    // معالجة الأخطاء
    app.use(errorHandler);
    
    
    app.listen(port, () => {
      console.log(`server is up, listening on port ${port}`);
    });

    بناء على ما كتبناه هنا، علينا إنشاء مجلد views على جذر المشروع وهو ما سيحوي صفحات العرض والتي سيكون امتدادها ejs حيث خصصنا ejs كمحرك للعرض، كما سننشئ مجلدًا اسمه public يحوي ملفًا اسمه style.css حيث عيننا هذا المجلد على أن محتوياته قابلة للإرسال إلى حاسوب العميل.

    تصميم الصفحات

    سنحتاج في مشروعنا هذا إلى الصفحات التالية:

    • الصفحة الرئيسية (فيها عرض للمقالات أو لنتائج البحث).
    • صفحة عرض المقالة.
    • صفحة إنشاء المقالة.

    إن أمعنا النظر فسنجد أن الترويسة والتذييل لهذه الصفحات نفسها، فلا حاجة لتكرار الشيفرات نفسها في أكثر من ملف، لذا سنقسم الملفات إلى الملفات التي سيلي ذكرها في هذا القسم.

    سننشئ ضمن المجلد views ملفًا نسميه header.ejs يحوي الشيفرة التالية:

    <!DOCTYPE html>
    <html lang="ar">
    
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>مدونتي</title>
        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.14.0/css/all.min.css">
        <link rel="stylesheet" href="../public/style.css">
    
    </head>
    
    <body>
        <nav class="navbar fixed-top navbar-expand-md ">
            <div class="container">
              <a class="navbar-brand mr-0 ml-4 " href="/">مدونتي</a>
              <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
                <i class="fas fa-bars"></i>
              </button>
              <div class="collapse navbar-collapse" id="navbarNav">
    
                <ul class="navbar-nav mr-md-1">
                  <li class="nav-item ml-md-2 active">
                    <a class="nav-link" href="/article/create">
                      <i class="fas fa-plus ml-1"></i>
                      إنشاء مقال 
                      <span class="sr-only">(current)</span>
                    </a>
                  </li>
                </ul>
    
                <form class="form-inline mt-2 mt-md-0" action="/search" method="GET" >
                  <input name="q" id="q" class="form-control ml-sm-2" type="text" placeholder="عن ماذا تريد أن تبحث ؟" aria-label="Search" required>
                  <button class="btn btn-primary my-2 my-sm-0" type="submit">ابحث</button>
                </form>
    
              </div>
            </div>
          </nav>

    وهذه هي ترويسة الصفحة، استدعينا فيها مكتبة Bootstrap من نظام توصيل المحتوى CDN، واستدعينا فيها مكتبة fontawesome من نظام توصيل المحتوى CDN، واستدعينا ملف style.css الذي أنشأناه، كما بنينا شريط التنقل للموقع.

    سننشئ ضمن المجلد views ملفًا آخر اسمه footer.ejs وهو تذييل الصفحة، ونكتب فيه التالي:

    </div>
        <script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
        <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script>
        <script src="../public/script.js"></script>
    </body>
    
    </html>

    وهنا أيضًا استدعينا مكتبة jQuery وملف JavaScript الخاص بمكتبة Bootstrap، وملف script.js الذى يحتوى على السكربت الخاص بالتطبيق، وأغلقنا الأوسمة المفتوحة الموجودة في ملف header.ejs.

    سنهيئ الآن الصفحة الرئيسية في الموقع والتي ستمكننا من عرض المقالات. لننشئ الآن ملف index.ejs أيضًا في مجلد views ولنكتب فيه:

    <%- include('_header') %>
    
    <main role="main" class="container-fluid">
        <div class="jumbotron">
          <div class="container">
            <h1 class="text-center">أحدث المقالات</h1>
          </div>
        </div>
        <div class="container pt-4 pb-4">
          <div class="row">
            <% if(!articles || articles.length == 0) { %>
              <h5 class="col mt-5 pt-5 text-center">لا توجد مقالات لعرضها</h5>
              <% }else { %>
              <% articles.forEach(article => { %>
                <div class="mb-4 col-md-6 col-sm-12">
                  <div class="card shadow-sm p-3 bg-white rounded">
                    <div class="card-body">
                      <h2 class="card-title mb-3">
                        <a class="text-primary text-decoration-none" href="/article/<%= article.id %>"><%= article.title %></a>
                      </h2>
                      <p class="card-text mb-4"><i class="fas fa-user-circle"></i>  <%= article.author %></p>
                      <button class="delete-btn btn btn-danger btn-sm" data-id="<%= article.id %>">حذف المقال</button>
                    </div>
                  </div>
                </div>
              <% }) %>
              <% } %>
          </div>
        </div>
      </main>
    
    
    
    <%- include('_footer') %>

    الشيفرات المحاطة بالإشارتين <% و %> هي شيفرات JavaScript يقرؤها ejs لتنفيذ ما ضمنها، وفي حالتنا هنا نستورد الترويسة والتذييل للصفحات، ونضع ما يعبر عن المقالات في الكائن الذي سنوفره عند تنفيذ الصفحة حتى نعرض بياناته.

    لنضع تنسيقًا لتحسين شكل الصفحة، إذ سنكتب شيفرة التنسيق التالية في الملف style.css الذي أنشأناه في المجلد public:

    /*-الخط المستخدم بالموقع-*/
    @import url('https://fonts.googleapis.com/css2?family=Almarai&display=swap');
    @import url('https://fonts.googleapis.com/css2?family=Almarai:wght@700&display=swap');
    
    /*-الإعدادات العامة للموقع-*/
    body {
        direction: rtl;
        text-align: right;
        font-family: 'Almarai', sans-serif;
        font-size: 16px;
    }
    
    .bg-light{
        background-color: #e9ecef !important;
    }
    
    textarea{
        resize: none;
    }
    
    .container--error{
        margin-top: 70px;
    }
    
    .container--error p{
        font-weight: 700;
    }
    
    .container-fluid{
        padding: 0 !important;
        margin-top: 56px;
    }
    
    .navbar{
        background-color: #e9ecef;
        padding: 15px;
    }
    
    .navbar-toggler{
        border: 1px solid #212529;
    }
    
    .navbar-toggler:focus{
        border:1px solid #212529 !important; 
    }
    
    .navbar-brand{
        color: #212529 !important;
        font-weight: 700;
    }
    
    .navbar .navbar-collapse form{
        margin-right: auto;
    }
    
    .navbar .navbar-nav{
        padding-right: 0;
    }
    
    .navbar .navbar-nav .nav-link{
        color: #212529;
    }
    
    .navbar .navbar-nav .nav-link i{
        font-size: 13px;
    }
    
    .jumbotron{
        border-radius: 0;
        padding: 6rem 2rem;
    }
    
    .card{
        border: none;
        height: 100%;
    }
    
    .card-title a{
        transition: all 0.3s ease;
    }
    
    .form-control{
        border: none;
        height: auto;
    }
    
    .article-head{
        display: inline-flex;
        align-items: flex-start;
        justify-content: center;
        flex-direction: column;
    }
    
    .article-head h5{
        color: #868686;
    }
    
    /*-responsive-*/
    @media (min-width: 1100px){
        .container--article-create{
            max-width: 900px;
        }
    
        .container--article-view{
            max-width: 900px;
        }
    }
    
    
    @media (max-width: 768px){
        .navbar-nav > li:first-child{
            margin-top: 20px;
        }
        .navbar  > .container{
            padding-right: 15px;
            padding-left: 15px;
        }
    }
    
    @media (max-width: 576px){
        .navbar-collapse form button[type=submit]{
            width: 100%;
        }
        .navbar  > .container{
            padding-right: 0;
            padding-left: 0;
        }
    }

    أنشأنا حتى الآن الصفحة الرئيسية التي ستعرض كل المقالات أو نتائج البحث، ولننشئ الآن الصفحة التي ستمكننا من إضافة مقالات جديدة؛ سننشئ في مجلد views ملفًا اسمه create_article.ejs ونكتب فيه:

    <%- include('_header'); %>
    <main role="main" class="container-fluid">
        <div class="jumbotron">
            <div class="container">
                <h1 class="text-center">أنشئ مقالًا</h1>
            </div>
        </div>
        <div class="container container--article-create pt4 pb-4">
            <form action="/article" class="pt-5 pb-5" method="POST" id="create-article-form">
                <div class="form-group">
                    <label for="title">عنوان المقال</label>
                    <input type="text" class="form-control pt-2 pb-2 mt-1 bg-light" id="title" name="title"
                        placeholder="أدخل عنوانًا مناسبًا لمقالك" required>
                </div>
                <div class="form-group">
                    <label for="author ">الكاتب</label>
                    <input type="text" class="form-control pt-2 pb-2 mt-1 bg-light" id="author" name="author" placeholder="ما اسم كاتب المقال؟" required>
                </div>
                <div class="form-group">
                    <label for="content">نص المقال</label>
                    <textarea class="form-control pt-2 pb-2 mt-1 bg-light" id="content" name="content" placeholder="نص المقال هنا"
                        rows="7" required></textarea>
                </div>
                <div class="form-group">
                    <label for="tags">الوسوم</label>
                    <input type="text" class="form-control pt-2 pb-2 mt-1 bg-light" id="tags" name="tags" placeholder="مثال على الوسوم: الكتابة,العمل" required>
                </div>
                <div class="form-group text-center">
                    <input type="submit" class="btn btn-lg btn-block mt-3 btn-primary" value="أنشئ المقال">
                </div>
            </form>
        </div>
    </main>
    <%- include('_footer'); %>

    والآن لننشئ الصفحة التي ستعرض مقالًا عند النقر على عنوانه في الصفحة الرئيسية؛ سننشئ ضمن المجلد views ملفًا اسمه article.ejs ونكتب فيه:

    <%- include('_header'); %>
    
    <article class="container-fluid">
        <div class="jumbotron">
            <div class="container text-center">
                <% if(!article) { %>
                <h1 class="text-center">لا يوجد مقالات!</h1>
                <% } else { %>
                <div class="article-head text-right">
                    <h1 class="text-right mb-3"><%= article.title %></h1>
                    <h5 class="text-right mt-1">الكاتب: <%= article.author %></h5>
                </div>
    
                <% } %>
            </div>
        </div>
        <div class="container container--article-view">
            <% if(!article) { %>
            <h1>المقال الذي طلبته غير موجود!</h1>
            <a href="/" class="btn btn-secondary btn-sm">عد إلى الصفحة الرئيسية</a>
            <% } else { %>
            <div>
                <%-article.content%>
            </div>
            <% } %>
        </div>
    </article>
    
    <%- include('_footer'); %>

    والآن لننشئ الصفحة التي ستعرض الخطأ للمستخدم في حالة قام بكتابة رابط غير موجود، سننشئ ضمن المجلد views ملفًا اسمه error.ejs ونكتب فيه:

    <%- include("_header"); %>
    <div class="container-fluid">
    
        <div class="jumbotron">
            <div class="container">
                <h1 class="text-center">حدث خطأ ما!</h1>
            </div>
        </div>
        <div class="container container--error text-center">
            <h2 class="text-center">أعد المحاولة لاحقًا </h2>
            <a href="/" class="btn btn-lg mt-3 btn-primary">عد إلى الصفحة الرئيسية</a>
        </div>
    
    </div>
    <%- include("_footer"); %>

    تهيئة إدارة قواعد البيانات وإنشاء الموجهات Routes

    بعد أن انتهينا من تصميم الصفحات، حان الوقت الآن لإنشاء طريقة التعامل مع قواعد البيانات، وكما تحدثنا سابقًا، سنستخدم Sequelize التي هي ORM شهيرة في Node.js.

    سننشِئ ملفًا جديدًا باسم sequelize.js، وسنضع فيه الشيفرة الآتية لتهيئة عملية الاتصال بقاعدة البيانات، وأذكِّرك بتغيير اسم المستخدم وكلمة المرور وفقًا لبيئة العمل الخاصة بك:

    const { Sequelize } = require("sequelize");
    
    const DATABASE_NAME = "myBlog";
    const USER_NAME = "root";
    const PASSWORD = "password";
    const DIALECT = "mariadb";
    
    const sequelize = new Sequelize(DATABASE_NAME, USER_NAME, PASSWORD, {
        dialect: DIALECT,
        dialectOptions: { connectTimeout: 1000 },
        logging: false,
    });
    
    module.exports = sequelize;

    لتستطيع حزمة sequelize التعرف على قاعدة البيانات من نوع MariaDB (أو MySQL)، علينا تثبيت الحزمة المُخصَّصة لها بتنفيذ الأمر التالي:

    npm install mariadb

    وبهذا أصبح المشروع جاهزًا لكتابة الموجهات routes؛ لننشئ مجلدًا باسم routes بداخله ملف article.routes.js يحوي الشيفرة الآتية:

    const controller = require("../controllers/article.controller");
    module.exports = (app) => {
      app.get("/", controller.viewIndex);
    };

    ولكن يجب أن ننشئ مجلدًا باسم controllers وننشئ ملف باسم article.controller.js ونكتب فيه ما يلي:

    const Article = require("../models/article");
    
    exports.viewIndex = (req, res, next) => {
      Article.findAll()
        .then((articles) => {
          res.render("index", { articles: articles });
        })
        .catch((error) => {
          next(error);
        });
    };

    هيأنا هنا الموجه الرئيسي، وعند طلب العنوان localhost:5000 في المتصفح فسيجلب الخادم كل المقالات من قاعدة البيانات، ويفسر الصفحة index.ejs ويعطيها الكائن articles حتى يعرض معلومات المقالات.

    نشغل المشروع عن طريق كتابة الأمر التالي في ملف المشروع في سطر الأوامر:

    npm start

    يجدر بالذكر أننا عدلنا القسم scripts في ملف package.json ووضعنا سكربت start لبدء تشغيل التطبيق عبر node للملف app.js.

    [...]
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1",
        "start": "node app.js"
      },
    [...]

    في الخطوة التالية لنهيئ المستقبل الذي يوجهنا إلى نموذج أو استمارة إنشاء مقال؛ سنضيف داخل ملف article.routes.js بعد الشيفرة السابقة:

      app.post("/article", controller.create);
      app.get("/article/create", controller.viewCreate);

    بعد ذلك نضيف الشيفرة الآتية داخل ملف article.controller.js:

    exports.viewCreate = (req, res) => {
      res.render("create_article");
    };
    
    exports.create = (req, res, next) => {
      Article.create(req.body)
        .then((article) => {
          article.save();
          res.redirect("/");
        })
        .catch((error) => {
          next(error);
        });
    };

    وبهذا نستطيع الآن إنشاء مقالات جديدة، واستعراضها من الصفحة الرئيسية، وحتى نرى المقال بعد الضغط على بطاقته في الصفحة الرئيسية، سنضيف ما يلي داخل ملف article.routes.js بعد الشيفرة السابقة:

      app.get("/article/:id", controller.viewArticle);

    بعد ذلك نضيف الشيفرة الآتية داخل ملف article.controller.js:

    exports.viewArticle = (req, res, next) => {
      Article.findOne({
        where: {
          id: req.params.id,
        },
      })
        .then((article) => {
          res.render("article", { article: article });
        })
        .catch((error) => {
          next(error);
        });
    };

    وبهذا أصبح المشروع جاهزًا لإضافة وعرض المقالات من قاعدة البيانات.

    لا تنسَ حفظ كل شيء ثم تشغيل المشروع، أو استخدم حزمة nodemon لتعيد تشغيل المشروع تلقائيًا عند كل تعديل تجريه على شيفراتك.

    إضافة المقالات

    سننشئ بداية مقالًا للتحقق من صحة الإدخال إلى قاعدة البيانات، وليكن بالمعلومات التالية:

    إضافة المقالات.png

    وبعد أن تحققت من صحة العمليات التي بنيتها، سأنشئ مجموعة من المقالات ذات المواضيع المتقاربة من أكاديمية حسوب، ولا بد أن نتحدث عن كل المقالات التي أدخلناها حتى نفهم نتائج البحث.

    سأدخل المقال الأول بعنوان «كيف تقدّر أجر مساعدك عن بعد» وسأدخل اسم الكاتب عدنان، وفي نص المقال سأنسخ فقط المقدمة، وفي صندوق الوسوم سأدخل كلمة أجر مساعدك.

    إنشاء مقال.png

    المقال الثاني بعنوان «أسس القيادة التقنية» والكاتب ابراهيم سليمان، وسأنسخ فقط المقدمة، وفي صندوق الوسوم سأدخل أسس القيادة.

    أسس القيادة التقنية.png

    والمقال الثالث بعنوان «معضلة الإنتاجية والكاتب هو عمر، وسأنسخ فقط المقدمة، وفي صندوق الوسوم سأدخل الإنتاجية.

    إنشاء مقالة معضلة الإنتاجية.png

    إذًا حصلنا الآن على ثلاثة مقالات، مواضيعها مختلفة، بالإضافة إلى المقال الأول التجريبي، وأصبح لدينا ثلاث كُتَّاب لكل منهم مقال، ولنبن الآن مستَقبِل البحث.

    بناء عملية البحث اعتمادًا على قاعدة البيانات

    يعتمد معظم المطورين على المعامل like في عبارات SQL لبحثهم، العملية كافية بالنسبة للبحث في شيء محدد يدركه المستخدم مسبقًا ولا مجال فيه للاختلاف الكبير بين آراء مختلف المستخدمين وصياغتها لعبارة البحث، وكل مافي الأمر هنا أن تبحث باستخدام هذا المعامل عن ما يشبه عبارة البحث في حقل العنوان أو حقل الوسوم tags للمقال، وتوفير مربع بحث آخر للبحث حسب الكاتب.

    وتُنفّذ العملية ببساطة بمقارنة السلاسل النصية المطلوبة مع عبارة البحث التي أدخلها المستخدم، دون أدنى شكل من أشكال تحليل محتوى المقال أو تعرف أداة البحث على موضوعه ومدى فائدته للمستخدم بناء على عبارة البحث.

    حتى أنشئ وظيفة البحث التقليدية، سأذهب إلى قسم الموجهات routes في ملف article.routes.js وأضيف الموجه التالي:

      app.get("/search", controller.search);

    بعد ذلك نضيف الشيفرة الآتية داخل ملف article.controller.js:

    exports.search = (req, res, next) => {
      Article.findAll({
        where: {
          [Op.or]: {
            title: { [Op.like]: `%${req.query.q}%` },
            tags: { [Op.like]: `%${req.query.q}%` },
            content: { [Op.like]: `%${req.query.q}%` },
          },
        },
      })
        .then((articles) => {
          res.render("index", { articles: articles });
        })
        .catch((err) => {
          next(err);
        });
    };

    سيبحث هذا التابع في حقول العنوان والمحتوى والوسوم عن ما يتضمن حرفيًا ما أدخلناه في عبارة البحث، فإذا بحثنا باستخدام إحدى العبارات: أسس القيادة التقنية، معضلة الإنتاجية، مقال تجريبي؛ فإنه سيعيد لنا نتيجة واحدة كانت قيمة وسومها تحتوي العبارة. لكن ماذا إذا بحثنا مثلًا عن "أشياء إنتاجيّة"؟ نذكر أن أحد مقالاتنا كان يتحدث عن الإنتاج وفيه فقرة عن أشياء إنتاجيّة حقيقيّة، وتعلم أن تحصيلك لنتيجة بحث دقيقة تعطي المستخدم ما هو مفيد له فقط بناء على الدالة like شيء مستحيل، جرب مثلًا البحث عن "اشياء إنتاجيّة" دون همزة ستجد أنه لا يعيد لك أي نتائج.

    ولربما تبحث عن أمر آخر، كلمة أسس مكتوبة مع همزة في المقال الذي عنوانه أسس القيادة التقنية، ابحث عن "اسس" دون همزة، وستجد أنه لا يعيد لك أي نتائج!

    مفاهيم متعلقة بمحرك البحث الداخلي

    يأتي هنا دور محرك البحث لبلب والذي له القدرة على تحليل النص ومعرفة موضوعه ومضمونه، وجدولة نتائج البحث على أساس هذا التحليل، لنجرب الآن بناء البحث باستخدام لبلب لنرى إن كان سينجح في إعادة نتائج لنا بناء على العبارات التي لم تعد نتائج في البحث السابق، ولكن قبل أن نبدأ ببناء الوظائف، لنفهم بعض المفاهيم الأساسية.

    محرك البحث لبلب هو محرك بحث سحابي، يعمل على جدولة أو فهرسة indexing المحتوى الذي يصله من الموقع، ومن ثم التفاعل مع عبارة البحث التي تصله وتوفير النتائج التي يجدها مناسبة. من هنا نعرف أنه لدينا عمليتين حتى نصل إلى المطلوب: الأولى جدولة المحتوى، والثانية هي عملية البحث.

    يوفِّر محرك البحث لبلب واجهة برمجية Web API للتعامل معها بشكل مباشر، كما يوفر حزمة برمجية SDK لكل من المنصات: Node.js و PHP Laravel و WordPress، حتى تسهل التعامل مع الواجهة البرمجية API الخاصة به.

    يمكنك الرجوع إلى التوثيق الرسمي للواجهة البرمجية وللحزم البرمجية لأي تفاصيل ومعلومات إضافية. يتطلب التعامل مع واجهة لبلب البرمجية حسابًا على موقع لبلب، ويتم الحفاظ على وثوقية وأمان المعلومات عند التعامل مع لبلب من خلال استخدام مفاتيح خاصة بمشروعك API Key تستطيع توليد مفاتيح جديدة من خلال لوحة التحكم الخاصة بمشروعك (من خلال الدخول الى لوحة التحكم -> ثم الضغط على اسم المشروع الخاص بك -> ثم الدخول الى قسم API Keys) ونلاحظ وجود مفتاحين: الأول للقيام بعملية الفهرسة عند لبلب والثاني لإجراء عمليات البحث من بإستخدام لبلب.

    تهيئة حزمة لبلب

    لدي مشروع اسمه academyhsoub في حسابي على لبلب، ولهذا المشروع جدول collection اسمه posts وله البنية التالية:

    بنية posts.png

    أستطيع من هنا التحكم في حقول هذا الجدول بإضافة حقول جديدة ومنحها أسماء أو حذف حقول موجودة، وجعلها إلزامية بتفعيل الخاصية required لها.

    سنثبت حزمة لبلب بالأمر الآتي:

    npm install @lableb/javascript-sdk

    وبعدها ننشئ ملفًا باسم lableb.js داخل المجلد config ونضع به الشيفرة الآتية:

    const { LablebClient } = require("@lableb/javascript-sdk");
    
    const PROJECT_NAME = "academyhsoub";
    const INDEXING_TOKEN = "محتوى هذا الحقل من حسابك على لبلب";
    const SEARCH_TOKEN = "محتوى هذا الحقل من حسابك على لبلب ";
    
    let lableb;
    
    LablebClient({
        platformName: PROJECT_NAME,
        APIKey: SEARCH_TOKEN,
        indexingAPIKey: INDEXING_TOKEN
    })
    .then(lablebClient => {
        lableb = lablebClient;
    })
    .catch(error => {
        console.error(error);
    })
    
    module.exports = lableb;

    وهكذا سنستطيع استخدام المتغير lableb لمختلف العمليات. هيأنا هذا الثابت بإعطائه اسم المشروع ومفاتيح الواجهة API KEYs الخاصة بالجدولة والبحث، حيث أن الحزمة ستوجه الطلبات إلى رابط واجهة لبلب البرمجية بالشكل التالي في حال عملية الجدولة:

    https://api.lableb.com/api/v2/${PROJECT_NAME}/indices/${COLLECTION_NAME}/documents?apikey=${INDEXING_TOKEN}

    وبالشكل التالي في عملية البحث:

    https://api.lableb.com/api/v2/${PROJECT_NAME}/indices/${COLLECTION_NAME}/search/default?q=${SEARCH_QUERY}&cat=Lifestyle&limit=1&apikey=${SEARCH_TOKEN}

    وبذلك تختصر علينا الحزمة كتابة هذه العناوين وتشكيلها يدويًا.

    بناء عملية الفهرسة Indexing

    سأضيف الآن عمليات الفهرسة بتوسيع extend صنف قاعدة البيانات حتى نعمل ببنية صحيحة. سنبني عملية فهرسة Indexing مرتبطة بالصنف Article وظيفتها جدولة كل المقالات السابقة في قاعدة البيانات، وستفيدنا هذه العملية في التأكد من فهرسة المقالات التي أضيفت إلى قاعدة البيانات قبل استخدام محرك البحث، أو عندما يكون قد جرى تعديل على المقالات ولم تتم فهرسته.

    وعملية الفهرسة مرتبطة بالكائن article عند إنشاء كل كائن جديد حتى تتم فهرسته بعد الإنشاء مباشرة. ولبناء العمليتان، ننشئ مجلدًا جديدًا باسم models وبداخله ننشئ ملف باسم article.js ونكتب بداخله الشيفرة التالية:

    const { Sequelize, Model } = require("sequelize");
    const sequelize = require("../config/sequelize");
    const lableb = require("../config/lableb");
    
    class Article extends Model {
      //إنشاء مستند قابل للفهرسة خارج الكائن 
      _document() {
    
    
        return {
          id: this.id,
          title: this.title,
          content: this.content,
          url: `/article/${this.id}`,
          tags: this.tags,
          authors: this.author,
        };
      }
    
      // إنشاء مستندات قابلة للفهرسة لجميع المقالات
      static async _findAllDocuments() {
        return Article.findAll().map((a) => a._document());
      }
    
      // فهرسة مستند واحد
      _index() {
        return lableb.index({
                documents: [this._document()]
            });
      }
    
      // حذف مستند واحد من الفهرسة
      async _deleteIndex() {
        return lableb.delete({
                documentId: this.id
            });
      }
    
      // فهرسة جميع المقالات
      static async indexAll() {
        const documents = await Article._findAllDocuments();
    
         return lableb.index({
                documents: documents
            });
    
      }
    
      // تحويل نتائج البحث القادمة من لبلب إلى مقالات قابلة للعرض
      static async search(query, limit) {
    
            const lablebResult = await lableb.search({
                query: encodeURIComponent(query),
                limit: limit
            });
    
            const searchResults = lablebResult.response.results;
    
    
        return searchResults.map((searchResult) => ({
          id: searchResult.id,
          title: searchResult.title,
          content: searchResult.content,
          author: searchResult.authors[0],
          tags: searchResult.tags[0],
        }));
      }
    }
    
    Article.init(
      {
        title: Sequelize.STRING,
        author: Sequelize.STRING,
        content: Sequelize.STRING,
        tags: Sequelize.STRING,
      },
      {
        hooks: {
          afterSave: function (article) {
            article._index();
          },
          beforeDestroy: function(article) {
            article._deleteIndex();
          }
        },
        sequelize,
        modelName: "article",
      }
    );
    
    module.exports = Article;

    سنشرح الشيفرة السابقة؛ أنشأنا في البداية صنفًا يحتوي على الدالة document_ ليكون وسيطًا معبرًا عن المقال الواجب جدولته.

    بعد ذلك أنشأنا التابع findAllDocuments_ لإنشاء مستندات قابلة للفهرسة لجميع المقالات وأنشأنا بعد ذلك التابع index_ لفهرسة مستند واحد والتابع indexAll لفهرسة جميع المقالات، ولا بد أن نذكر هنا أن خدمة لبلب تتعامل مع المعلومات المفهرسة فيها عبر حقل id، فإذا تم إرسال المقالات إلى لبلب وكانت مُعرِّفاتُها موجودة مسبقًا، فسيتم تحديثها لا تكرارها. ونلاحظ أثناء بناء المصفوفة documents_ أننا خزننا حتى روابط هذه المقالات.

    ونُذكِّر أننا أضفنا هذا الجزء من قبل وهو مسؤول عن عرض وجدولة كل المقالات:

    exports.viewIndex = (req, res, next) => {
      Article.findAll()
        .then((articles) => {
          res.render("index", { articles: articles });
        })
        .catch((error) => {
          next(error);
        });
    };

    تعديل عملية البحث وتجارب البحث

    رأينا في السابق نتائج البحث التي لم تكن ذات جدوى عالية، والآن لنعلق دالة البحث القديمة بحيث تصبح بدون تأثير على التطبيق ونضع الدالة الآتية بدلًا منها فى الملف article.controller.js وجعلها كالآتي:

    exports.search = (req, res, next) => {
      Article.search(req.query.q, 10)
        .then((articles) => {
          res.render("index", { articles: articles });
        })
        .catch((error) => {
          next(error);
        });
    };
    
    exports.indexAll = (req, res, next) => {
      Article.indexAll();
    };

    ونحصل من دالة بحث لبلب على نتائج البحث لنتصرف فيها.

    إذا علقنا كل محتوى القديم وكتبنا الأسطر التي تنفذ عملية البحث باستخدام لبلب، وتعتمد على نتائج بحث لبلب في عرض المقالات، عوضًا عن قاعدة بياناتنا وهكذا بنينا كامل عملية البحث.

    تجارب البحث باستخدام لبلب

    لدينا ثلاثة مقالات تتحدث عن مواضيع مختلفة، وموضوع واحد بعنوان مقال تجريبي، لنجرب البحث عن كلمة "اشياء إنتاجيّة":

    تجارب البحث باستخدام لبلب.png

    ونلاحظ أنه أعاد لنا المقالة التى تتحدث عن أشياء إنتاجية وهى "معضلة الإنتاجية "على الرغم أنها لا تبدأ بهمزة، وأعاد أيضًا مقال أسس القيادة التقنية بعدها لأنها تحتوى على كلمة الأشياء.

    البحث باستخدام لبلب.png

    جرب الآن البحث عن كلمة "اسس" دون همزة.

    البحث بدون همزة.png

    و"أسس" مع همزة.

    البحث مع همزة.png

    ونلاحظ هنا أنه جلب لنا مقال أسس القيادة التقنية فى كلتا الحالتين، هل كنا سنستطيع برمجة دالة بحث تعطينا نفس الدقة في النتائج؟

    برمجة دالة بحث.png

    حسنٌ، لنبحث عن عبارة مخصصة جدًا في مقال كيف تقدّر أجر مساعدك عن بعد، لنبحث عن عبارة "ميزانية مشروعك"

    البحث عن عبارة مخصصة.png

    ونلاحظ أننا حصلنا فقط على نتيجة واحدة وهي مقالة كيف تقدّر أجر مساعدك عن بعد، ويجب أن تتذكر أننا وضعنا المقدمة فقط فى المحتوى الخاص بالمقالة، لذلك اخترت عبارة من المقدمة وبحثت عنها، وسنفعل المثل فى عمليات البحث القادمة.

    نتائج البحث عن عبارة مخصصة.png

    لنبحث عن كلمة "نيويوركر"

    نيويوركر.png

    ونلاحظ مجددًا أنه أعاد مقالة "معضلة الإنتاجية" فقط إذ أنَّها المقالة الوحيدة لدينا التى تحتوى على هذه الكلمة.

    نتائج البحث عن نيويوركر.png

    لنبحث الآن باستخدام عبارة أقرب لما يكتبه البشر عادة، لنبحث عن "ما هى المعضلة الانتاجية؟"

    بحث ما هى المعضلة الانتاجية.png

    وسنرى أنه أعاد لنا مقالة معضلة الإنتاجية فقط، واستثنى المقالات الأخرى:

    نتائج البحث عن ما هى المعضلة الانتاجية.png

    نستنتج أن محرك البحث استطاع عرض نتيجة بناء على عبارة سؤال طبيعي.

    لنجرب البحث عن عبارة "مقال تجريبي"

    بحث مقال تجريبي.png

    وسنجد أنه أعاد المقالات التى تحتوى على كلمة مقال أو تجريبى ولكن "مقال تجريبى" فى بداية الترتيب.

    نتائج بحث مقال تجريبي.png

    لنبحث عن اسم الكاتب "عدنان":

    البحث عن اسم الكاتب.png

    ونرى أن النتيجة عادت فقط بالمقالات التي كتبها عدنان، دون أي مجهود منا لجعل عملية البحث تستهدف حقل الكاتب من كائن المقال.

    نتائج البحث عن اسم الكاتب.png

    الخلاصة

    رأينا الفائدة من استخدام محرك بحث تجريبي، تخيل أن لديك موقع تجارة إلكترونية يعرض منتجاته باللغة العربية، أليس جميلًا أن تحصل على دقة كهذه في إرجاع النتائج، فضلًا عن ترتيبها حسب الأقرب للبحث؟ وهل تستطيع بناء دالة بحث بنفس القدرات باستخدام الأدوات التقليدية مثل تعليمات SQL أو مقارنة السلاسل النصية باستخدام لغة البرمجة التي بنيت موقعك بها؟ أجرينا في هذا المقال مقارنة بين البحث باستخدام دالة بدائية يدوية، والبحث باستخدام محرك بحث عربي، واخترنا لبلب لهذه المهمة، واستنتجنا أن محرك البحث يتجه أكثر نحو فهم الموضوع واستيعاب مختلف صيغ عبارات البحث التي يدخلها المستخدم بلغة طبيعية غير مصطنعة كما هو الحال في البحث باستخدام الوسوم، ونرجو أن يكون المقال مفيدًا ونرحب بأية تعليقات أو استفسارات لديكم.

    اقرأ أيضًا


    تفاعل الأعضاء

    أفضل التعليقات

    لا توجد أية تعليقات بعد



    انضم إلى النقاش

    يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

    زائر
    أضف تعليق

    ×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

      Only 75 emoji are allowed.

    ×   Your link has been automatically embedded.   Display as a link instead

    ×   جرى استعادة المحتوى السابق..   امسح المحرر

    ×   You cannot paste images directly. Upload or insert images from URL.


    ×
    ×
    • أضف...