كل بحث يكتبه موظف في Google أو Bing يخرج من شبكة مؤسستك ومعه عنوان IP وCookies الحساب وسجل البحث السابق، فإذا كان الفريق يبحث عن اسم عميل أو رسالة خطأ من نظام داخلي أو اسم مشروع لم يعلن، فهذه المعلومات تصل إلى شركة إعلانات قبل أن تصل إلى أي شخص آخر. والحل البسيط أن تطلب من الفريق استخدام وضع التصفح الخاص، وهذا لا يغير شيئاً في الواقع، لأن المحرك ما زال يرى العنوان ونص البحث. أما SearXNG فهو محرك بحث وصفي Metasearch Engine تشغله على سيرفرك، يرسل البحث إلى عدة محركات باسمه هو، ثم يدمج النتائج ويعرضها للموظف.
وسوف نناقش في هذا المقال ما يلي:
- كيف يعمل البحث الوصفي، وما الذي يحميه SearXNG فعلاً وما الذي لا يحميه.
- الرخصة AGPL وماذا تعني لمؤسسة تشغله داخلياً.
- تثبيته بـ Docker Compose خلف nginx، واختيار المحركات Engines، والمحدد Limiter مع Valkey، ووكيل الصور Image Proxy.
- واجهة JSON لتطبيقاتك الداخلية، ولماذا رفضت أول طلباتنا برمز 429.
- ماذا يحدث عندما تحظرك المحركات، وكيف تجعل النسخة لفريقك فقط خلف الدخول الموحد SSO.
كيف يعمل محرك البحث الوصفي؟
SearXNG لا يملك فهرساً خاصاً بالويب، ولا يزحف على المواقع كما يفعل Google، وإنما هو وسيط: يستقبل سؤالك، ويرسله إلى المحركات التي اخترتها في نفس اللحظة، ثم يجمع الإجابات ويحذف الروابط المكررة ويرتبها، فالرابط الذي تعيده عدة محركات يرتفع في الترتيب. وهذا يختلف تماماً عن محرك البحث الذي تبنيه على بياناتك كما في مقال كيف يعمل البحث، فهناك أنت تملك الفهرس، وهنا تملك الطريق إلى فهارس الآخرين. والصورة التالية تبين ذلك:

وبحسب صفحة لماذا تستخدم نسخة خاصة في التوثيق، فإن SearXNG لا يرسل Cookies المستخدم إلى المحركات، وينشئ بصمة متصفح عشوائية Browser Profile لكل طلب، ولا يعرض إعلانات أو أدوات تتبع، ويخفي الصفحة التي جئت منها عن المواقع التي تفتحها من النتائج. ولكن هل هذا يعني أن البحث أصبح خاصاً بالكامل؟ لا، والتفصيل كما يلي:
- المحرك ما زال يرى نص البحث: إذا بحث الموظف عن اسم مشروع سري، فـ DuckDuckGo وBrave يستلمان هذا النص، ولكنهما لا يعرفان أي موظف كتبه.
- عنوان السيرفر واحد للجميع: كل بحث الفريق يخرج من عنوان IP واحد، فهو يخفي الأفراد، ولكنه يكشف أن هذه المؤسسة تبحث عن هذه المواضيع، ويمكن توجيه الطلبات عبر Proxy أو Tor كما يذكر التوثيق إذا كان هذا مهماً لك.
- من يدير السيرفر يرى كل شيء: سجلات nginx فيها نص البحث في الرابط، لذلك قلل ما تسجله، أو استخدم طريقة POST، أو احذف السجلات بعد مدة قصيرة كما في ما لا يدخل السجل.
الرخصة: AGPL-3.0
ملف LICENSE في مستودع SearXNG هو GNU Affero General Public License الإصدار 3، والمستودع نفسه أنشئ في أبريل 2021 كتفرع من مشروع searx الأقدم بنفس الرخصة. وAGPL تعني أنك إذا عدلت الكود وقدمته عبر الشبكة لمستخدمين، فعليك أن تتيح لهم الكود المعدل، أما تشغيله كما هو مع ملف إعدادات خاص بك فلا يلزمك بشيء، وقد شرحنا الحالة بالتفصيل في قسم تشغيل برنامج AGPL داخل شركتك. وملف settings.yml إعدادات وليس تعديلاً على الكود، ولكن إذا غيرت قالب الواجهة أو كتبت محركاً خاصاً داخل الكود، فاحتفظ بالتعديلات في مستودع يمكن للفريق الوصول إليه.
متى يناسبك ومتى لا يناسبك؟
يناسبك إذا أردت أن يبحث فريقك في الويب دون أن يرتبط كل بحث بحساب شخصي، أو أردت محرك بحث واحداً يجمع الويب مع مصادر داخلية مثل Gitea أو ويكي داخلي من خلال محركات SearXNG الخاصة بها، أو احتجت واجهة JSON تستدعيها أدوات داخلية دون مفتاح API تجاري. ولا يناسبك إذا كنت تتوقع جودة Google نفسها في كل الأسئلة، لأنك تعتمد على ما تعيده المحركات المجانية دون حساب، وبعضها يحظر الطلبات الآلية كما سنرى، ولا يناسبك إذا لم يكن عندك من يتابع السيرفر عند تغير المحركات، لأن المحركات تغير صفحاتها والمشروع يصدر إصدارات متكررة لمواكبتها.
التجربة: SearXNG خلف nginx مع Valkey
الإصدارات: SearXNG بالوسم 2026.10.9-f4822b3fc، وValkey 9.0.6، وnginx 1.31.6، والمنفذ الوحيد المفتوح هو منفذ nginx على 127.0.0.1:18080، وأسماء ال Containers تبدأ بـ lab-searx-. والمشروع لا يستخدم أرقام إصدارات تقليدية، وإنما وسماً بتاريخ البناء ورقم ال Commit، فثبت الوسم كاملاً ولا تستخدم latest، والسبب أن تحديثاً تلقائياً قد يغير سلوك المحركات في يوم عمل. والصورة التالية تبين ما نبنيه للفريق:

ملف Docker Compose وnginx
# SearXNG lab: nginx in front (sets X-Forwarded-For / X-Real-IP for the limiter),
# the SearXNG app and Valkey. Only nginx publishes a port, on 127.0.0.1.
# Run: docker compose -p lab-searx up -d
name: lab-searx
services:
proxy:
image: nginx:1.31.6-alpine
container_name: lab-searx-proxy
depends_on: [searxng]
volumes:
- ./nginx/searxng.conf:/etc/nginx/conf.d/default.conf:ro
ports:
- "127.0.0.1:18080:80"
networks: [searx]
searxng:
image: searxng/searxng:2026.10.9-f4822b3fc
container_name: lab-searx-app
depends_on: [valkey]
environment:
- SEARXNG_BASE_URL=http://127.0.0.1:18080/
- SEARXNG_SECRET=${SEARXNG_SECRET}
volumes:
- ./searxng/settings.yml:/etc/searxng/settings.yml:ro
- ./searxng/limiter.toml:/etc/searxng/limiter.toml:ro
mem_limit: 512m
networks: [searx]
valkey:
image: valkey/valkey:9.0.6-alpine
container_name: lab-searx-valkey
command: valkey-server --save "" --appendonly no --loglevel warning
networks: [searx]
networks:
searx:
ipam:
config:
- subnet: 10.213.77.0/24server {
listen 80;
location / {
proxy_pass http://lab-searx-app:8080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
}
}في الإعداد أعلاه لاحظ التالي:
- SearXNG نفسه لا ينشر أي منفذ، وكل الطلبات تمر من nginx الذي يضع
X-Forwarded-ForوX-Real-IP، وهذا شرط يذكره توثيق المحدد Limiter صراحة، لأن المحدد يعد الطلبات لكل عنوان، فإذا لم يعرف العنوان الحقيقي عامل كل الطلبات كأنها من مصدر واحد. - الشبكة بنطاق ثابت
10.213.77.0/24حتى نكتب في إعداد المحدد من هو ال Proxy الموثوق. وكان أول اختيار لنا172.30.77.0/24فرفضه Docker برسالةPool overlaps with other one on this address spaceلأن شبكة أخرى على نفس الجهاز تستخدمه، فاختر نطاقاً لا يتعارض مع شبكاتك. - Valkey بلا حفظ على القرص، لأن ما فيه عدادات مؤقتة للمحدد، وقد شرحنا Valkey وقصة رخصته في دليل Redis وValkey.
- المفتاح السري
SEARXNG_SECRETمن ملف.env، وأنشئه بالأمرopenssl rand -hex 32، ولا تترك القيمة الافتراضيةultrasecretkeyالتي في أمثلة التوثيق.
ملف الإعدادات: المحركات والصيغ والمحدد
بدل نسخ ملف الإعدادات الافتراضي كاملاً وفيه 3504 أسطر، نبدأ من الافتراضي بـ use_default_settings ونغير ما نحتاجه فقط، كما يشرح توثيق settings.yml:
# Start from the defaults that ship with SearXNG and change only what we need.
use_default_settings:
engines:
keep_only:
- duckduckgo
- brave
- mojeek
- wikipedia
- wikidata
- github
- stackoverflow
- duckduckgo images
general:
instance_name: "بحث الفريق"
search:
safe_search: 1
autocomplete: ""
default_lang: "auto"
formats:
- html
- json
server:
# secret_key comes from SEARXNG_SECRET in the environment
limiter: true
public_instance: false
image_proxy: true
method: "GET"
valkey:
url: valkey://lab-searx-valkey:6379/0
ui:
default_locale: "ar"
outgoing:
request_timeout: 8.0
max_request_timeout: 15.0
engines:
- name: wikipedia
weight: 1.5
- name: github
disabled: false[botdetection]
# trust X-Forwarded-For / X-Real-IP only when the request comes from our nginx
trusted_proxies = ["10.213.77.0/24"]
[botdetection.ip_lists]
# The internal server that calls the JSON API. In the lab it is the Docker gateway,
# which is how requests from the host reach nginx.
pass_ip = ["10.213.77.1/32"]في الإعداد أعلاه لاحظ التالي:
keep_onlyيبقي ثمانية محركات فقط من 352 محركاً معرفاً في ملف الإعدادات الافتراضي، والسبب أن كل محرك إضافي يعني طلباً إضافياً يخرج من سيرفرك مع كل بحث، وفرصة إضافية للحظر.formatsفيهاjsonبجانبhtml، وبدونها ترجع واجهة البحث برمجياً الرمز 403 بحسب توثيق Search API.image_proxy: trueيجعل صور النتائج تمر عبر سيرفرك، فلا يحمل متصفح الموظف الصورة مباشرة من موقع غريب يرى عنوانه، والثمن استهلاك ذاكرة وشبكة السيرفر كما يذكر توثيق server.pass_ipيعفي عنواناً معيناً من فحص البرامج الآلية، وسوف نرى بعد قليل لماذا احتجناه.
واجهة JSON: لماذا رفضت أول الطلبات؟
بعد التشغيل استدعينا واجهة JSON بـ curl كما يفعل أي تطبيق داخلي:
docker compose -p lab-searx up -d
curl -s -o /dev/null -w "%{http_code}\n" "http://127.0.0.1:18080/search?q=valkey+license&format=json"429الرمز 429 يعني Too Many Requests، ولم نرسل إلا طلباً واحداً. وفي أول تشغيل قبل أن نضع nginx أمامه كانت هذه الرسالة في سجل SearXNG:
ERROR:searx.botdetection: X-Forwarded-For nor X-Real-IP header is set!والسبب أن المحدد مصمم لمنع البرامج الآلية من استخدام نسختك، لأن طلباتها سوف تخرج باسم سيرفرك فتحظره المحركات، وcurl بالنسبة له برنامج آلي حتى لو أرسلنا ترويسة متصفح، فقد أرسلنا -A "Mozilla/5.0 ..." مع ترويسات اللغة وبقي الرد 429. والحل الصحيح ليس تعطيل المحدد، وإنما أن تضيف عنوان التطبيق الداخلي الذي يستدعي ال API إلى pass_ip، وفي التجربة هو عنوان بوابة Docker 10.213.77.1 الذي تصل منه طلبات الجهاز إلى nginx كما يظهر في سجله:
10.213.77.1 - - [11/Oct/2026:17:28:12 +0000] "GET /search?q=valkey+license&format=json HTTP/1.1" 429 17 "-" "curl/8.12.1" "-"وبعد إضافة العنوان وإعادة التشغيل:
HTTP 200 in 14.314657s
query: valkey license | results: 0 | unresponsive: [['brave', 'انتهت المهلة'], ['brave', 'انتهت المهلة'], ['duckduckgo', 'انتهت المهلة'], ['duckduckgo', 'انتهت المهلة'], ['wikidata', 'انتهت المهلة'], ['wikidata', 'انتهت المهلة'], ['wikipedia', 'انتهت المهلة'], ['wikipedia', 'انتهت المهلة']]نجح الطلب ولكن بلا نتائج، وكل المحركات انتهت مهلتها، والسبب أن المهلة كانت request_timeout: 3.0 ثوان، والاتصال من هذا الجهاز إلى Brave كان يأخذ قرابة 5 ثوان كما قسناه من داخل ال Container. فرفعنا المهلة إلى 8 ثوان:
HTTP 200 in 8.129898s
query: valkey license | results: 0 | unresponsive: [['brave', 'انتهت المهلة'], ['duckduckgo', 'أسئلة التحقق'], ['wikidata', 'انتهت المهلة']]عندما تحظرك المحركات
لاحظ في المخرج الأخير duckduckgo مع «أسئلة التحقق»، أي أن DuckDuckGo طلب CAPTCHA، وهذه ليست مشكلة في إعدادنا، وإنما طبيعة البحث الوصفي: كل طلبات الفريق تخرج من عنوان واحد ودون Cookies، فتبدو للمحرك كبرنامج آلي. وفي الطلبات التالية ظهر brave مع «الكثير من الطلبات» ثم «معلق: انتهت المهلة»، وهذا ملخص الأخطاء من صفحة /stats/errors في نفس الجلسة:
brave Timeout, SearxEngineTooManyRequestsException
duckduckgo SearxEngineCaptchaException
duckduckgo images Timeout
wikidata Timeoutوعندما يحدث ذلك يعلق SearXNG المحرك Suspend مدة محددة حتى لا يزيد الحظر، والمدد الافتراضية في توثيق قسم search هي 86400 ثانية أي يوم كامل عند SearxEngineAccessDenied وSearxEngineCaptcha، و3600 ثانية عند SearxEngineTooManyRequests. وفي المقابل أجابت المحركات الأخرى في نفس الدقائق:
# categories=it
query: valkey license | results: 1
- Crestway-AI-LLC/flint-cache | https://github.com/Crestway-AI-LLC/flint-cache | ['github']
# general (duckduckgo answered this time, brave was rate limited)
query: valkey license | results: 10 | unresponsive: [['brave', 'الكثير من الطلبات']]
- Valkey Documentation · License | https://valkey.io/topics/license/ | ['duckduckgo'] | score 1.0
- Valkey | https://valkey.io/ | ['duckduckgo'] | score 0.5
- GitHub - valkey-io/valkey: A flexible distributed key-value | https://github.com/valkey-io/valkey | ['duckduckgo'] | score 0.33
# engines=wikipedia, language=ar
query: رخصة جنو العمومية | infoboxes: 1
infobox: رخصة جنو العمومية | wikipedia | رخصة جنو العمومية رخصة برمجيات حرة مستخدمة على نحو واسع، كتبها أصلا ريتشارد ستولمن لمشروع جنو...والدرس العملي من هذه المخرجات:
- فعل عدة محركات عامة وليس واحداً، حتى إذا علق محرك أجاب غيره، كما حدث مع DuckDuckGo وBrave بالتناوب.
- لا تفتح النسخة للإنترنت، فالنسخة العامة يكتشفها من يجمع النسخ المفتوحة ويرسل إليها برامجه الآلية، فتحظر المحركات عنوانك في أيام.
- المحركات المتخصصة مثل Wikipedia وGitHub وStack Overflow أقل حظراً لأنها واجهات برمجية مقصودة، فاجعل لها وزناً أعلى بالخيار
weightالذي يشرحه توثيق engines. - راقب
/statsو/stats/errors، فإذا ظهر محرك بأخطاء CAPTCHA كل يوم فعطله بدل أن يبطئ كل بحث حتى تنتهي مهلته.
وهذه صفحة النتائج بالواجهة العربية لبحث «رخصة Valkey»، والنتائج جاءت من DuckDuckGo بعد أن رفع تعليقه:

ولاحظ أن «رخصة» بالعربية جاءت معها نتائج عن رخص البلدية، لأن DuckDuckGo لا يعرف أنك تقصد رخصة البرمجيات، وهذا يذكرنا بأن SearXNG لا يحسن جودة البحث، وإنما يغير من يرى البحث. وفي نفس الصفحة كانت صور النتائج تمر عبر الرابط /image_proxy?url=https%3A%2F%2Ftse1.mm.bing.net%2F...، أي أن المتصفح لا يتصل بخوادم الصور مباشرة.
نسخة لفريقك خلف الدخول الموحد
SearXNG لا يحتوي نظام حسابات، فكل من يصل إلى الرابط يستطيع البحث، والحل أن تضع الحماية في ال Reverse Proxy أمامه:
- ضعه خلف Forward Auth مع authentik أو أي مزود هوية يدعم OpenID Connect، فلا يصل إلى صفحة البحث إلا من سجل الدخول بحساب المؤسسة، وتطبق نفس سياسات المصادقة الثنائية وإيقاف الحساب عند ترك العمل.
- استثن من الدخول الموحد مسار ال API لتطبيق داخلي محدد بالعنوان فقط، وأضف نفس العنوان إلى
pass_ip، ولا تستثن المسار للجميع. - اجعل nginx يمرر
X-Forwarded-Forكما في التجربة، وأضف عنوان ال Reverse Proxy إلىtrusted_proxies، وهي نفس فكرة عنوان الزائر الحقيقي خلف ال Reverse Proxy. - اترك
public_instance: false، لأن تفعيله يضيف فحوصاً مصممة للنسخ العامة المفتوحة للجميع.
التشغيل اليومي
- التحديث: المحركات تغير صفحاتها، والمشروع يصدر Docker Image جديدة كل بضعة أيام، لذلك حدث الوسم المثبت كل أسبوعين أو عند تعطل محرك تعتمد عليه، بعد أن تقرأ ما تغير.
- الموارد: SearXNG نفسه خفيف، والحد في التجربة 512 ميجابايت، أما وكيل الصور فيستهلك شبكة السيرفر بقدر ما يتصفح الفريق من صور.
- النسخ الاحتياطي: لا توجد بيانات تستحق النسخ إلا
settings.ymlوlimiter.tomlوملف Compose، فاحفظها في Git مع إخفاء المفتاح السري. - المراقبة: نقطة
/healthzترجع 200 عندما يعمل التطبيق، ولكنها لا تخبرك أن كل المحركات محظورة، لذلك أضف فحصاً دورياً لبحث معروف بصيغة JSON يتأكد أن عدد النتائج أكبر من صفر.
أخطاء شائعة
- تعطيل المحدد لأن ال API ترجع 429: هذا يفتح سيرفرك لكل برنامج آلي، والحل
pass_ipلعنوان التطبيق المعروف. - تشغيله دون Reverse Proxy يمرر العنوان الحقيقي: المحدد يرى كل الطلبات من مصدر واحد فيحظر الجميع أو لا يحظر أحداً.
- تفعيل عشرات المحركات: كل بحث يصبح عشرات الطلبات الخارجية، والصفحة تنتظر أبطأ محرك حتى تنتهي المهلة.
- مهلة قصيرة جداً: رأينا أن 3 ثوان لم تكف على هذا الاتصال، فقس زمن الوصول من سيرفرك إلى المحركات قبل أن تحدد المهلة.
- استخدام الوسم
latest: قد يغير سلوك المحركات أو الإعدادات دون أن تعرف متى تغير. - ترك المفتاح السري الافتراضي: يستخدم في التوقيع، فإذا كان معروفاً أمكن تزوير ما يعتمد عليه.
تدريب عملي
- أضف محرك Gitea أو أي مصدر داخلي يدعمه SearXNG إلى
engines، ثم ابحث عن اسم مستودع داخلي وتأكد أن نتيجته تظهر بجانب نتائج الويب. - اكتب سكربتاً يستدعي
/search?format=jsonكل خمس دقائق ببحث ثابت، ويرسل تنبيهاً إذا كان عدد النتائج صفراً ثلاث مرات متتالية.
حذف المختبر
docker compose -p lab-searx down -v
rm .env
# optional
docker rmi searxng/searxng:2026.10.9-f4822b3fc valkey/valkey:9.0.6-alpineالخلاصة
وصلنا لنهاية الموضوع، وأهم ما فيه:
- SearXNG محرك بحث وصفي يسأل المحركات باسمه ودون Cookies المستخدم، فيخفي الموظف عن المحرك، ولكن المحرك ما زال يرى نص البحث وعنوان السيرفر.
- رخصته AGPL-3.0، وتشغيله كما هو بإعداداتك لا يلزمك بشيء، والتعديل على الكود يلزمك بإتاحته لمستخدميه.
- ضعه خلف Reverse Proxy يمرر العنوان الحقيقي، ومعه المحدد وValkey، وأضف تطبيقاتك الداخلية إلى
pass_ipبدل تعطيل المحدد. - الحظر من المحركات جزء طبيعي من البحث الوصفي، فاختر عدة محركات، وراقب
/stats/errors، ولا تفتح النسخة للإنترنت. - اجعل النسخة للفريق فقط خلف الدخول الموحد، وثبت وسم الإصدار وحدثه بانتظام.
دليلكيف تحمي أي تطبيق ويب بصفحة دخول authentik عبر Forward Authكيف تضع صفحة دخول authentik مع المصادقة الثنائية أمام أي أداة داخلية دون أن تعدل في التطبيق نفسه.
دليلRedis وبديله المفتوح Valkey: التخزين المؤقت والجلساتكيف تستخدم Redis للجلسات والعدادات دون أن تمتلئ الذاكرة، وماذا تعني رخصته الجديدة، ومتى يكفيك Valkey بدلاً منه.
دليلكيف يرى تطبيقك عنوان الزائر الحقيقي خلف ال Reverse Proxyإذا كان تطبيقك يرى عنوان ال Proxy بدلاً من الزائر أو يولد روابط http، فهذا هو السبب والإعداد الصحيح لإطار عملك.
دليلما هي البرمجيات مفتوحة المصدر؟ القصة وأشهر الرخصكيف بدأت البرمجيات الحرة والمفتوحة، وما الفرق بين الحر والمجاني، وكيف تعرف ما تسمح به رخصة أي مشروع قبل أن تبني عليه.سجل التحديثات
- أكتوبر 2026: كتابة الدليل، وتجربة SearXNG بالوسم 2026.10.9-f4822b3fc خلف nginx 1.31.6 مع Valkey 9.0.6، بطلبات حقيقية إلى المحركات أظهرت المحدد والمهلات والحظر كما هي.