تجربة اليوزر · فصل ٥

الغلط الإملائي والفلاتر

Fuzziness · query context · filter context
🎯 الهدف: تسمح بغلط إملائي محدود من غير ما تفتح البحث على أي كلمة، وتفصل درجة الصلة عن شرط نعم أو لأ.

ifone غير جملة تانية خالص

اليوزر كتب ifone وهو قاصد iphone: حرف ناقص. ده typo، و fuzzy search معمول عشانه. المسافة بين الكلمتين هي Levenshtein distance: كام إضافة أو حذف أو تبديل يخلّصوا الكلمة للتانية. هنا المسافة ١.

جملة زي “if no way” بدل iPhone مش typo في كلمة. دي كلمات تانية. الـ fuzzy مش هيحوّلها، ولو زوّدت المسافة المسموحة عشان تلحقها هترجع نتائج ملهاش علاقة. المرادفات والبحث الصوتي أدوات تانية، وسهلة تبوّظ الدقة.

الكويري وفيها سماحية الغلط

src/search-text.js
export function textQuery(q) {  return {    bool: {      should: [        { term: { sku: { value: q.toUpperCase(), boost: 20 } } },        {          multi_match: {            query: q,            fields: ["name^3", "description"],            fuzziness: "AUTO",            prefix_length: 1,          },        },      ],      minimum_should_match: 1,    },  };}

الـ SKU بره الـ fuzzy. لو اليوزر لصق كود، عايز تطابق كامل، ومش عايز كود قريب يطلّع منتج تاني. وحّد الـ SKU لـ uppercase وقت ما بتحفظه ووقت ما بتبحث، عشان apl-... و APL-... يبقوا نفس الـ keyword.

الـ fuzzy أغلى من التطابق العادي لأنه بيوسّع الكلمة لكذا احتمال. حطه على بحث الصفحة، مش على كل حرف في الاقتراحات. الاقتراحات فصل لوحدها.

Query context غير filter context

بعد ما لقينا نتائج قريبة، اليوزر يقول: Lenovo، رام ١٦، سعر من ١٠ لـ ٣٠ ألف، متوفر، تقييم ٤ وفوق.

Query context

سؤال: مين أقرب للنِيّة؟ بيحسب _score. مكانه must و should.

Filter context

سؤال: الشرط متحقق؟ Lenovo يا آه يا لأ. مفيش score، والنتيجة بتتـcache. مكانه filter.

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

src/filters.js
export function toNum(value) {  if (value == null || value === "") return null;  const n = Number(value);  return Number.isFinite(n) ? n : null;}export function productFilters({ brand, minPrice, maxPrice, minRating, inStock, ramGb }) {  const filters = [];  if (brand) filters.push({ term: { brand } });  if (ramGb != null) filters.push({ term: { ram_gb: ramGb } });  if (minRating != null) filters.push({ range: { rating: { gte: minRating } } });  if (inStock) filters.push({ range: { stock: { gt: 0 } } });  if (minPrice != null || maxPrice != null) {    const price = {};    if (minPrice != null) price.gte = minPrice;    if (maxPrice != null) price.lte = maxPrice;    filters.push({ range: { price } });  }  return filters;}
src/search-body.js
import { textQuery } from "./search-text.js";import { productFilters } from "./filters.js";export function searchBody(input) {  const q = (input.q ?? "").trim();  return {    query: {      bool: {        must: q ? [textQuery(q)] : [{ match_all: {} }],        filter: productFilters(input),      },    },  };}

من غير نص بحث، match_all مع الفلاتر دي صفحة قسم: “لابتوبات Lenovo المتوفرة”. لسه مش facets. الأعداد اللي جنب البراند محتاجة aggregations، وفيها فخ هنشوفه في الفصل الجاي.

🧪 Lab

ابعت نفس الجملة مرتين: ifone من غير fuzziness، وبعدين بيها. الأولى غالبًا فاضية، والتانية ترجع iPhone. بعدين ضيف filter براند تاني. ترتيب الـ _score بين نتايج نفس البراند مبيتغيرش بسبب الفلتر، والبراند الغلط يختفي كله.

✅ الخلاصة — Checklist

🧠 اختبر نفسك
١) ليه ifone تقدر ترجع iPhone بينما جملة بعيدة لأ؟
لأن الفرق مسافة تعديل صغيرة. السماح الكبير بيرجع نتائج مش مقصودة.
٢) الفلتر بيزوّد _score؟
لأ. يا المستند يعدّي الشرط يا لأ، ومن غير درجة صلة.
٣) صفحة قسم من غير خانة بحث بتستخدم إيه؟
match_all جوه must، والشروط جوه filter.