بناء بحث فوري باستخدام Meilisearch و Next.js

AI Bot
بواسطة AI Bot ·

جاري تحميل مشغل تحويل النص إلى كلام الصوتي...

المتطلبات الأساسية

قبل البدء، تأكد من توفر ما يلي:

  • Node.js 20+ مثبّت على جهازك
  • Docker و Docker Compose مثبّتان
  • معرفة أساسية بـ Next.js و TypeScript
  • pnpm أو npm كمدير حزم
  • محرر أكواد مثل VS Code

ما ستبنيه

في هذا الدليل، ستنشئ تطبيق بحث فوري متكامل يتضمن:

  • محرك بحث Meilisearch منشور عبر Docker
  • واجهة برمجة Next.js لفهرسة البيانات والاستعلام عنها
  • واجهة بحث بنتائج فورية (أقل من 50 مللي ثانية)
  • فلاتر متعددة الأوجه لتنقيح النتائج
  • تمييز مصطلحات البحث المطابقة
  • نظام ترتيب حسب الصلة أو التاريخ أو الشعبية
  • تحمّل الأخطاء الإملائية المدمج في Meilisearch

لماذا Meilisearch؟

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

المزايا الرئيسية:

  • فائق السرعة: استجابات في أقل من 50 مللي ثانية، حتى مع ملايين المستندات
  • تحمّل الأخطاء الإملائية: يفهم أخطاء الكتابة تلقائيًا
  • فلاتر وأوجه: بحث مُنقّح بدون إعدادات معقدة
  • مفتوح المصدر: قابل للاستضافة الذاتية، بدون اعتماد على خدمات سحابية
  • واجهة RESTful بسيطة: تكامل سهل مع أي إطار عمل

الخطوة 1: نشر Meilisearch باستخدام Docker

لنبدأ بإعداد Meilisearch محليًا باستخدام Docker Compose.

أنشئ ملف docker-compose.yml في جذر مشروعك:

version: "3.8"
 
services:
  meilisearch:
    image: getmeili/meilisearch:v1.12
    container_name: meilisearch
    ports:
      - "7700:7700"
    environment:
      - MEILI_MASTER_KEY=مفتاحك_السري_هنا
      - MEILI_ENV=development
      - MEILI_DB_PATH=/meili_data
    volumes:
      - meilisearch_data:/meili_data
    restart: unless-stopped
 
volumes:
  meilisearch_data:

شغّل الحاوية:

docker compose up -d

تحقق من أن Meilisearch يعمل:

curl http://localhost:7700/health
# {"status":"available"}

يمكنك أيضًا الوصول إلى لوحة التحكم المدمجة على http://localhost:7700 في متصفحك.

الخطوة 2: تهيئة مشروع Next.js

أنشئ مشروع Next.js جديد مع TypeScript:

npx create-next-app@latest meilisearch-app --typescript --tailwind --app --src-dir
cd meilisearch-app

ثبّت الحزم المطلوبة:

pnpm add meilisearch react-instantsearch @meilisearch/instant-meilisearch
  • meilisearch: العميل الرسمي لـ JavaScript للتعامل مع الخادم
  • react-instantsearch: مكونات React لبناء واجهات البحث
  • @meilisearch/instant-meilisearch: محوّل يربط Meilisearch بـ InstantSearch

الخطوة 3: إعداد متغيرات البيئة

أنشئ ملف .env.local في جذر المشروع:

MEILISEARCH_HOST=http://localhost:7700
MEILISEARCH_ADMIN_KEY=مفتاحك_السري_هنا
NEXT_PUBLIC_MEILISEARCH_HOST=http://localhost:7700
NEXT_PUBLIC_MEILISEARCH_SEARCH_KEY=

يُستخدم MEILISEARCH_ADMIN_KEY في جانب الخادم لفهرسة البيانات. سيتم إنشاء NEXT_PUBLIC_MEILISEARCH_SEARCH_KEY في الخطوة التالية — وهو مقتصر على البحث فقط ويمكن كشفه بأمان في جانب العميل.

الخطوة 4: إنشاء عميل Meilisearch

أنشئ ملف src/lib/meilisearch.ts:

import { MeiliSearch } from "meilisearch";
 
// عميل المشرف (جانب الخادم فقط)
export const meiliAdmin = new MeiliSearch({
  host: process.env.MEILISEARCH_HOST!,
  apiKey: process.env.MEILISEARCH_ADMIN_KEY!,
});
 
// دالة للحصول على مفتاح البحث أو إنشائه
export async function getSearchKey(): Promise<string> {
  const keys = await meiliAdmin.getKeys();
  const searchKey = keys.results.find(
    (key) =>
      key.actions.includes("search") && key.actions.length === 1
  );
 
  if (searchKey) {
    return searchKey.key;
  }
 
  // إنشاء مفتاح مقتصر على البحث
  const newKey = await meiliAdmin.createKey({
    description: "مفتاح البحث العام",
    actions: ["search"],
    indexes: ["*"],
    expiresAt: null,
  });
 
  return newKey.key;
}

الخطوة 5: تعريف مخطط البيانات

في هذا الدليل، سننشئ فهرسًا لمقالات المدونة. أنشئ src/lib/types.ts:

export interface Article {
  id: string;
  title: string;
  summary: string;
  content: string;
  author: string;
  category: string;
  tags: string[];
  publishedAt: string;
  readingTime: number;
  views: number;
}

الخطوة 6: إنشاء سكريبت التعبئة

أنشئ src/lib/seed.ts لتغذية Meilisearch ببيانات تجريبية:

import { meiliAdmin } from "./meilisearch";
import type { Article } from "./types";
 
const sampleArticles: Article[] = [
  {
    id: "1",
    title: "مقدمة في TypeScript 5.5",
    summary:
      "اكتشف الميزات الجديدة في TypeScript 5.5 وكيف تحسّن سير عملك.",
    content:
      "يقدّم TypeScript 5.5 عدة ميزات ثورية...",
    author: "Sarah Chen",
    category: "TypeScript",
    tags: ["typescript", "javascript", "web"],
    publishedAt: "2026-03-15",
    readingTime: 8,
    views: 2450,
  },
  {
    id: "2",
    title: "بناء واجهات REST مع Hono و Bun",
    summary:
      "دليل عملي لإنشاء واجهات برمجة سريعة مع إطار Hono على Bun.",
    content:
      "Hono هو إطار ويب خفيف للغاية مصمم لدوال الحافة...",
    author: "Marc Dubois",
    category: "Backend",
    tags: ["hono", "bun", "api", "rest"],
    publishedAt: "2026-03-10",
    readingTime: 12,
    views: 1890,
  },
  {
    id: "3",
    title: "Next.js 15: الدليل الشامل لـ PPR",
    summary:
      "كل ما تحتاج معرفته عن العرض المسبق الجزئي في Next.js 15.",
    content:
      "يجمع العرض المسبق الجزئي بين أفضل ما في SSR و SSG...",
    author: "Anis Marrouchi",
    category: "Frontend",
    tags: ["nextjs", "react", "ssr", "performance"],
    publishedAt: "2026-02-28",
    readingTime: 15,
    views: 3200,
  },
  {
    id: "4",
    title: "Docker Compose للمطورين",
    summary:
      "أتقن Docker Compose لتنسيق بيئات التطوير الخاصة بك.",
    content:
      "يبسّط Docker Compose إدارة الحاويات المتعددة...",
    author: "Fatma Ben Ali",
    category: "DevOps",
    tags: ["docker", "devops", "containers"],
    publishedAt: "2026-03-20",
    readingTime: 10,
    views: 1540,
  },
  {
    id: "5",
    title: "المصادقة الحديثة مع Passkeys",
    summary:
      "طبّق مصادقة بدون كلمة مرور مع WebAuthn و Passkeys.",
    content:
      "تمثّل Passkeys مستقبل المصادقة على الويب...",
    author: "Karim Mansour",
    category: "Security",
    tags: ["auth", "security", "passkeys", "webauthn"],
    publishedAt: "2026-03-25",
    readingTime: 14,
    views: 4100,
  },
];
 
async function seedMeilisearch() {
  console.log("إعداد فهرس المقالات...");
 
  // إنشاء أو تحديث الفهرس
  const index = meiliAdmin.index("articles");
 
  // إعداد السمات القابلة للتصفية والترتيب
  await index.updateSettings({
    filterableAttributes: [
      "category",
      "tags",
      "author",
      "readingTime",
    ],
    sortableAttributes: [
      "publishedAt",
      "views",
      "readingTime",
    ],
    searchableAttributes: [
      "title",
      "summary",
      "content",
      "author",
      "tags",
    ],
    // السمات المعروضة في النتائج (استبعاد المحتوى الكامل)
    displayedAttributes: [
      "id",
      "title",
      "summary",
      "author",
      "category",
      "tags",
      "publishedAt",
      "readingTime",
      "views",
    ],
  });
 
  console.log("فهرسة المقالات...");
 
  // إضافة المستندات
  const response = await index.addDocuments(sampleArticles);
  console.log("تم إنشاء مهمة الفهرسة:", response.taskUid);
 
  // انتظار اكتمال الفهرسة
  await meiliAdmin.waitForTask(response.taskUid);
  console.log("اكتملت الفهرسة بنجاح!");
 
  // التحقق
  const stats = await index.getStats();
  console.log(`تم فهرسة ${stats.numberOfDocuments} مستند`);
}
 
seedMeilisearch().catch(console.error);

أضف سكريبت في package.json:

{
  "scripts": {
    "seed": "npx tsx src/lib/seed.ts"
  }
}

نفّذه:

pnpm seed

الخطوة 7: إنشاء مسار API للبحث

أنشئ src/app/api/search/route.ts للبحث من جانب الخادم:

import { meiliAdmin } from "@/lib/meilisearch";
import { NextRequest, NextResponse } from "next/server";
 
export async function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams;
  const query = searchParams.get("q") || "";
  const category = searchParams.get("category") || null;
  const sort = searchParams.get("sort") || null;
  const page = parseInt(searchParams.get("page") || "1");
  const limit = parseInt(searchParams.get("limit") || "10");
 
  try {
    const index = meiliAdmin.index("articles");
 
    const filters: string[] = [];
    if (category) {
      filters.push(`category = "${category}"`);
    }
 
    const results = await index.search(query, {
      filter: filters.length > 0 ? filters.join(" AND ") : undefined,
      sort: sort ? [sort] : undefined,
      limit,
      offset: (page - 1) * limit,
      attributesToHighlight: ["title", "summary"],
      highlightPreTag: '<mark class="bg-yellow-200">',
      highlightPostTag: "</mark>",
      attributesToCrop: ["summary"],
      cropLength: 150,
    });
 
    return NextResponse.json({
      hits: results.hits,
      query: results.query,
      processingTimeMs: results.processingTimeMs,
      totalHits: results.estimatedTotalHits,
      page,
      totalPages: Math.ceil(
        (results.estimatedTotalHits || 0) / limit
      ),
    });
  } catch (error) {
    console.error("خطأ في البحث:", error);
    return NextResponse.json(
      { error: "فشل البحث" },
      { status: 500 }
    );
  }
}

الخطوة 8: بناء مكوّن البحث مع InstantSearch

الآن لننشئ واجهة بحث غنية باستخدام React InstantSearch. أنشئ src/components/Search.tsx:

"use client";
 
import { instantMeiliSearch } from "@meilisearch/instant-meilisearch";
import {
  InstantSearch,
  SearchBox,
  Hits,
  RefinementList,
  Pagination,
  Stats,
  Highlight,
  SortBy,
  ClearRefinements,
  Configure,
} from "react-instantsearch";
 
const { searchClient } = instantMeiliSearch(
  process.env.NEXT_PUBLIC_MEILISEARCH_HOST!,
  process.env.NEXT_PUBLIC_MEILISEARCH_SEARCH_KEY!
);
 
function ArticleHit({ hit }: { hit: any }) {
  return (
    <article className="rounded-lg border border-gray-200 p-6 transition-shadow hover:shadow-md">
      <div className="mb-2 flex items-center gap-2">
        <span className="rounded-full bg-blue-100 px-3 py-1 text-xs font-medium text-blue-800">
          {hit.category}
        </span>
        <span className="text-sm text-gray-500">
          {hit.readingTime} دقيقة قراءة
        </span>
      </div>
 
      <h3 className="mb-2 text-xl font-semibold text-gray-900">
        <Highlight attribute="title" hit={hit} />
      </h3>
 
      <p className="mb-3 text-gray-600">
        <Highlight attribute="summary" hit={hit} />
      </p>
 
      <div className="flex items-center justify-between">
        <span className="text-sm text-gray-500">
          بواسطة {hit.author}
        </span>
        <div className="flex gap-2">
          {hit.tags?.slice(0, 3).map((tag: string) => (
            <span
              key={tag}
              className="rounded bg-gray-100 px-2 py-1 text-xs text-gray-600"
            >
              {tag}
            </span>
          ))}
        </div>
      </div>
    </article>
  );
}
 
export default function Search() {
  return (
    <InstantSearch
      indexName="articles"
      searchClient={searchClient}
    >
      <Configure hitsPerPage={10} />
 
      <div className="mx-auto max-w-6xl p-6">
        <h1 className="mb-8 text-3xl font-bold text-gray-900">
          البحث في المقالات
        </h1>
 
        {/* شريط البحث */}
        <div className="mb-6">
          <SearchBox
            placeholder="اكتب بحثك..."
            classNames={{
              root: "relative",
              form: "relative",
              input:
                "w-full rounded-lg border border-gray-300 px-4 py-3 pl-10 text-lg focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-200",
              submit: "absolute left-3 top-1/2 -translate-y-1/2",
              reset: "absolute right-3 top-1/2 -translate-y-1/2",
            }}
          />
        </div>
 
        {/* الإحصائيات */}
        <div className="mb-4">
          <Stats
            translations={{
              rootElementText({ nbHits, processingTimeMS }) {
                return `${nbHits} نتيجة في ${processingTimeMS} مللي ثانية`;
              },
            }}
          />
        </div>
 
        <div className="flex gap-8">
          {/* فلاتر جانبية */}
          <aside className="w-64 flex-shrink-0">
            <div className="mb-6">
              <h3 className="mb-3 font-semibold text-gray-700">
                ترتيب حسب
              </h3>
              <SortBy
                items={[
                  {
                    label: "الصلة",
                    value: "articles",
                  },
                  {
                    label: "الأحدث",
                    value: "articles:publishedAt:desc",
                  },
                  {
                    label: "الأكثر شعبية",
                    value: "articles:views:desc",
                  },
                ]}
                classNames={{
                  select:
                    "w-full rounded border border-gray-300 px-3 py-2",
                }}
              />
            </div>
 
            <div className="mb-6">
              <h3 className="mb-3 font-semibold text-gray-700">
                الفئة
              </h3>
              <RefinementList
                attribute="category"
                classNames={{
                  list: "space-y-2",
                  label: "flex items-center gap-2 cursor-pointer",
                  checkbox:
                    "rounded border-gray-300 text-blue-600",
                  labelText: "text-sm text-gray-600",
                  count:
                    "text-xs text-gray-400 bg-gray-100 rounded-full px-2",
                }}
              />
            </div>
 
            <div className="mb-6">
              <h3 className="mb-3 font-semibold text-gray-700">
                الوسوم
              </h3>
              <RefinementList
                attribute="tags"
                limit={10}
                showMore
                classNames={{
                  list: "space-y-2",
                  label: "flex items-center gap-2 cursor-pointer",
                  checkbox:
                    "rounded border-gray-300 text-blue-600",
                  labelText: "text-sm text-gray-600",
                  count:
                    "text-xs text-gray-400 bg-gray-100 rounded-full px-2",
                }}
              />
            </div>
 
            <ClearRefinements
              translations={{
                resetButtonText: "مسح جميع الفلاتر",
              }}
              classNames={{
                button:
                  "text-sm text-blue-600 hover:text-blue-800 underline",
              }}
            />
          </aside>
 
          {/* النتائج */}
          <main className="flex-1">
            <Hits
              hitComponent={ArticleHit}
              classNames={{
                list: "space-y-4",
              }}
            />
 
            <div className="mt-8">
              <Pagination
                classNames={{
                  list: "flex gap-2 justify-center",
                  item: "rounded border border-gray-300 px-3 py-2 hover:bg-gray-50",
                  selectedItem:
                    "rounded bg-blue-600 px-3 py-2 text-white",
                }}
              />
            </div>
          </main>
        </div>
      </div>
    </InstantSearch>
  );
}

الخطوة 9: الدمج في الصفحة الرئيسية

أنشئ src/app/page.tsx:

import Search from "@/components/Search";
 
export default function HomePage() {
  return (
    <main className="min-h-screen bg-white">
      <Search />
    </main>
  );
}

الخطوة 10: الفهرسة التلقائية مع Route Handlers

في الاستخدام الفعلي، ستحتاج لفهرسة المحتوى الجديد تلقائيًا. أنشئ src/app/api/index/route.ts:

import { meiliAdmin } from "@/lib/meilisearch";
import { NextRequest, NextResponse } from "next/server";
import type { Article } from "@/lib/types";
 
// Webhook لفهرسة مقال جديد
export async function POST(request: NextRequest) {
  // التحقق من رمز المصادقة
  const authHeader = request.headers.get("authorization");
  if (authHeader !== `Bearer ${process.env.INDEX_API_SECRET}`) {
    return NextResponse.json(
      { error: "غير مصرّح" },
      { status: 401 }
    );
  }
 
  try {
    const article: Article = await request.json();
    const index = meiliAdmin.index("articles");
 
    // addDocuments تقوم بعملية upsert: إنشاء أو تحديث
    const task = await index.addDocuments([article]);
    await meiliAdmin.waitForTask(task.taskUid);
 
    return NextResponse.json({
      success: true,
      taskUid: task.taskUid,
    });
  } catch (error) {
    console.error("خطأ في الفهرسة:", error);
    return NextResponse.json(
      { error: "فشلت الفهرسة" },
      { status: 500 }
    );
  }
}
 
// حذف مقال من الفهرس
export async function DELETE(request: NextRequest) {
  const authHeader = request.headers.get("authorization");
  if (authHeader !== `Bearer ${process.env.INDEX_API_SECRET}`) {
    return NextResponse.json(
      { error: "غير مصرّح" },
      { status: 401 }
    );
  }
 
  const { id } = await request.json();
  const index = meiliAdmin.index("articles");
 
  const task = await index.deleteDocument(id);
  await meiliAdmin.waitForTask(task.taskUid);
 
  return NextResponse.json({ success: true });
}

الخطوة 11: بحث مخصص مع تأخير

للتحكم الأدق، يمكنك إنشاء hook مخصص. أنشئ src/hooks/useSearch.ts:

"use client";
 
import { useState, useEffect } from "react";
 
interface SearchResult {
  hits: any[];
  query: string;
  processingTimeMs: number;
  totalHits: number;
  page: number;
  totalPages: number;
}
 
export function useSearch(debounceMs = 300) {
  const [query, setQuery] = useState("");
  const [results, setResults] = useState<SearchResult | null>(
    null
  );
  const [isLoading, setIsLoading] = useState(false);
  const [debouncedQuery, setDebouncedQuery] = useState("");
 
  // تأخير الاستعلام
  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedQuery(query);
    }, debounceMs);
 
    return () => clearTimeout(timer);
  }, [query, debounceMs]);
 
  // تنفيذ البحث
  useEffect(() => {
    if (!debouncedQuery.trim()) {
      setResults(null);
      return;
    }
 
    const controller = new AbortController();
 
    async function search() {
      setIsLoading(true);
      try {
        const response = await fetch(
          `/api/search?q=${encodeURIComponent(debouncedQuery)}`,
          { signal: controller.signal }
        );
        const data = await response.json();
        setResults(data);
      } catch (error) {
        if (
          error instanceof Error &&
          error.name !== "AbortError"
        ) {
          console.error("خطأ في البحث:", error);
        }
      } finally {
        setIsLoading(false);
      }
    }
 
    search();
 
    return () => controller.abort();
  }, [debouncedQuery]);
 
  return {
    query,
    setQuery,
    results,
    isLoading,
  };
}

يمكن استخدام هذا الـ hook لبناء واجهة بحث مخصصة بالكامل دون الاعتماد على InstantSearch.

الخطوة 12: التحسين للإنتاج

إعداد المرادفات

يدعم Meilisearch المرادفات لتحسين الملاءمة:

const index = meiliAdmin.index("articles");
 
await index.updateSettings({
  synonyms: {
    js: ["javascript"],
    ts: ["typescript"],
    react: ["reactjs"],
    vue: ["vuejs"],
    api: ["واجهة برمجة التطبيقات"],
    db: ["قاعدة بيانات", "database"],
  },
});

إعداد كلمات التوقف

كلمات التوقف يتم تجاهلها أثناء البحث لتحسين الملاءمة:

await index.updateSettings({
  stopWords: [
    "في",
    "من",
    "على",
    "إلى",
    "مع",
    "عن",
    "هو",
    "هي",
    "هذا",
    "هذه",
    "التي",
    "الذي",
    "أن",
    "كان",
    "كانت",
    "بين",
    "أو",
    "و",
  ],
});

إعداد الترتيب

خصّص قواعد الترتيب حسب احتياجاتك:

await index.updateSettings({
  rankingRules: [
    "words",
    "typo",
    "proximity",
    "attribute",
    "sort",
    "exactness",
    "views:desc", // تفضيل المقالات الشائعة
  ],
});

الخطوة 13: تأمين النشر

للإنتاج، أنشئ docker-compose.prod.yml:

version: "3.8"
 
services:
  meilisearch:
    image: getmeili/meilisearch:v1.12
    container_name: meilisearch-prod
    ports:
      - "127.0.0.1:7700:7700"
    environment:
      - MEILI_MASTER_KEY=${MEILI_MASTER_KEY}
      - MEILI_ENV=production
      - MEILI_DB_PATH=/meili_data
      - MEILI_MAX_INDEXING_MEMORY=512Mb
      - MEILI_HTTP_PAYLOAD_SIZE_LIMIT=100Mb
    volumes:
      - meilisearch_data:/meili_data
    restart: always
    deploy:
      resources:
        limits:
          memory: 1G
 
volumes:
  meilisearch_data:

نقاط مهمة للإنتاج:

  • اكشف فقط على localhost ‏(127.0.0.1:7700) واستخدم وكيل عكسي (Nginx/Caddy)
  • استخدم MEILI_ENV=production لتفعيل حماية الأمان
  • أنشئ مفتاح رئيسي قوي: openssl rand -base64 32
  • حدّد الذاكرة مع MEILI_MAX_INDEXING_MEMORY
  • انسخ احتياطيًا بانتظام وحدة تخزين Docker

إعداد Nginx

server {
    server_name search.yourdomain.com;
 
    location / {
        proxy_pass http://127.0.0.1:7700;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
 
    # SSL via Let's Encrypt
    listen 443 ssl;
    ssl_certificate /etc/letsencrypt/live/search.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/search.yourdomain.com/privkey.pem;
}

الخطوة 14: اختبار التنفيذ

شغّل تطبيقك واختبر الميزات:

# الطرفية 1: Meilisearch
docker compose up -d
 
# الطرفية 2: فهرسة البيانات
pnpm seed
 
# الطرفية 3: تشغيل Next.js
pnpm dev

افتح http://localhost:3000 واختبر:

  1. البحث الفوري: اكتب مصطلحًا وراقب النتائج تظهر في الوقت الفعلي
  2. تحمّل الأخطاء: اكتب "typesript" (بخطأ إملائي) — Meilisearch يجد نتائج "TypeScript" رغم ذلك
  3. الفلاتر: انقر على فئة لتصفية النتائج
  4. الترتيب: بدّل بين الصلة والتاريخ والشعبية
  5. التمييز: مصطلحات البحث مُميّزة بالأصفر في النتائج

استكشاف الأخطاء

Meilisearch لا يبدأ

تحقق من أن المنفذ 7700 متاح:

lsof -i :7700

إذا كانت خدمة أخرى تستخدم هذا المنفذ، عدّل التعيين في docker-compose.yml.

النتائج لا تظهر

تحقق من أن البيانات مفهرسة بشكل صحيح:

curl http://localhost:7700/indexes/articles/stats \
  -H "Authorization: Bearer مفتاحك_السري_هنا"

أخطاء CORS من جانب العميل

يسمح Meilisearch بجميع الأصول افتراضيًا في وضع التطوير. في الإنتاج، اضبط الوكيل العكسي للتعامل مع رؤوس CORS.

البحث بطيء

تحقق من سمات searchableAttributes — فهرسة حقول كبيرة كثيرة (مثل content) قد تبطئ الاستجابات. اقتصر على الحقول الأساسية.

الخطوات التالية

الآن بعد أن أصبح محرك البحث جاهزًا، إليك طرق لتطويره أكثر:

  • فهارس متعددة: أنشئ فهارس منفصلة لأنواع مختلفة من المحتوى (مقالات، منتجات، مستخدمين) وابحث فيها جميعًا في وقت واحد
  • البحث الجغرافي: يدعم Meilisearch البحث الجغرافي — مفيد للأدلة أو الأسواق الإلكترونية
  • التحليلات: تتبع المصطلحات الأكثر بحثًا لتحسين محتواك
  • البحث الموحّد: اجمع عدة فهارس في استعلام واحد مع ميزة البحث المتعدد
  • تكامل CI/CD: أتمت إعادة الفهرسة مع كل نشر

الخلاصة

لقد بنيت تطبيق بحث فوري متكامل مع Meilisearch و Next.js. في أقل من 50 مللي ثانية، يحصل مستخدموك على نتائج ملائمة مع تحمّل الأخطاء الإملائية وفلاتر متعددة الأوجه وترتيب مخصص.

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

الكود المصدري الكامل لهذا الدليل جاهز للتكييف مع حالة استخدامك — سواء كانت مدونة أو توثيق تقني أو منصة تجارة إلكترونية.


هل تريد قراءة المزيد من الدروس التعليمية؟ تحقق من أحدث درس تعليمي لدينا على ElysiaJS + Bun: بناء واجهة برمجة تطبيقات REST آمنة الأنواع من طرف إلى طرف.

ناقش مشروعك معنا

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

دعنا نجد أفضل الحلول لاحتياجاتك.

مقالات ذات صلة