יישום חיפוש פאקטי עבור יישומי ווב

כאשר לקוח בוחר מוצר בקטלוג, כל לחיצה על פילטר חייבת להציג מיד את מספר התוצאות העדכני—אחרת, הוא הולך לאיבוד ועוזב. אנו מיישמים חיפוש פנים (faceted search) שהופך סינון רב-רמות למהיר ואינטואיטיבי עבור המשתמש. הצוות שלנו מטפל בכל המחזור, מבחירת המנוע ועד לפריסה ותמיכה, ומבטיח פתרון אמין שגדל עם העסק שלכם.

פיתוח ותחזוקה של כל סוגי האתרים:

אתרי מידע או יישומי אינטרנט
אתרי תדמית, דפי נחיתה, אתרי חברה, קטלוגים מקוונים, חידונים, אתרי קידום, בלוגים, מקורות חדשות, פורטלי מידע, פורומים, אגרגטורים
אתרי מסחר אלקטרוני או יישומי אינטרנט
חנויות מקוונות, פורטלי B2B, שווקים, בורסות מקוונות, אתרי קאשבק, בורסות, פלטפורמות דרופשיפינג, מנתחי מוצרים
יישומי אינטרנט לניהול תהליכים עסקיים
מערכות CRM, מערכות ERP, פורטלים ארגוניים, מערכות ניהול ייצור, מנתחי מידע
אתרי שירות אלקטרוני או יישומי אינטרנט
פלטפורמות מודעות, בתי ספר מקוונים, בתי קולנוע מקוונים, בוני אתרים, פורטלים לשירותים אלקטרוניים, פלטפורמות אירוח וידאו, פורטלים נושאיים

אלה רק חלק מהסוגים הטכניים של אתרים שאנו עובדים איתם, ולכל אחד מהם יכולים להיות מאפיינים ופונקציונליות ספציפיים משלו, וכן ניתן להתאים אותם לצרכים ולמטרות הספציפיים של הלקוח.

השירותים שאנו מציעים
מציג 1 מתוך 1כל 2062 השירותים
יישום חיפוש פאקטי עבור יישומי ווב
מורכב
~3-5 ימים

הכישורים שלנו:

שאלות נפוצות

העבודות האחרונות

  • פיתוח אפליקציית ווב עבור FEEDME
    פיתוח אפליקציית ווב עבור FEEDME
    1359
  • פיתוח חנות מקוונת לחברת FURNORO
    פיתוח חנות מקוונת לחברת FURNORO
    1330
  • פיתוח אפליקציית ווב עבור Enviok
    פיתוח אפליקציית ווב עבור Enviok
    1077
  • פיתוח CRM עבור Chasseurs
    פיתוח CRM עבור Chasseurs
    1113
  • פיתוח אתר עבור SBH Partners
    פיתוח אתר עבור SBH Partners
    1201
  • פיתוח אתר עבור Red Pear
    פיתוח אתר עבור Red Pear
    619

משתמש מגיע לחנות מקוונת, בוחר בקטגוריית 'מחשבים ניידים', ורוצה לסנן דגמים עם 16 GB RAM, מחיר בטווח מסוים ודירוג של 4.5. כל לחיצה על פילטר צריכה להראות כמה מוצרים תואמים לתנאים. אם המונים לא מתעדכנים, המשתמש מתבלבל ועוזב. חיפוש פאקטלי פותר זאת: הוא מחשב מיידית את מספר התוצאות עבור כל ממד (פאקט) תוך התחשבות בכל הפילטרים הפעילים. אנו מיישמים מנגנונים כאלה עבור קטלוגים בכל גודל, מ-1,000 עד 500,000 מוצרים.

מדוע אגרגציות ב-Elasticsearch מהירות יותר מ-SQL?

Elasticsearch מאחסן נתונים באינדקס הפוך ומבצע אגרגציות ברמת הסגמנט ללא סריקות מלאות. עבור PostgreSQL עם jsonb ואינדקסי GIN, שאילתות מרובות-פאקטים מורכבות עם פילטרים על פני שדות מרובים עלולות להוביל לבעיות N+1. Elasticsearch מעבד 10–50 פאקטים ב-10–50 אלפיות השנייה על קטלוג של 50,000 מוצרים. Typesense מציג חביון נמוך אף יותר—עד 5 אלפיות השנייה—הודות ל-API פשוט. בחירת המנוע משפיעה ישירות על חוויית המשתמש ועלות התשתית.

בפרויקט אחד עבור קמעונאי מוצרי אלקטרוניקה עם 200,000 מוצרים, עברנו מ-PostgreSQL ל-Elasticsearch, והפחתנו את זמן תגובת הפילטרים מ-800 אלפיות השנייה ל-15 אלפיות השנייה.

כיצד לארגן כראוי סכמת URL עבור פילטרים?

מצב הפאקטים צריך להישמר בפרמטרי URL. זה מאפשר שיתוף קישורים ושומר על היסטוריית הדפדפן. דוגמה לסכמה: /catalog?category=laptops&brand=apple,samsung&price=50000-150000&page=2. המודל המוקלד ופונקציות ה-parse/serialize מוצגים להלן.

type FacetState = {
  category?: string;
  brand?: string[];
  price?: { min: number; max: number };
  rating?: number[];
  inStock?: boolean;
  page: number;
  sort: 'relevance' | 'price_asc' | 'price_desc' | 'rating';
};

function parseFacetState(searchParams: URLSearchParams): FacetState {
  const price = searchParams.get('price');
  const [priceMin, priceMax] = price ? price.split('-').map(Number) : [undefined, undefined];
  return {
    category: searchParams.get('category') ?? undefined,
    brand: searchParams.get('brand')?.split(',').filter(Boolean),
    price: priceMin && priceMax ? { min: priceMin, max: priceMax } : undefined,
    rating: searchParams.get('rating')?.split(',').map(Number),
    inStock: searchParams.get('inStock') === 'true',
    page: Number(searchParams.get('page') ?? 1),
    sort: (searchParams.get('sort') as FacetState['sort']) ?? 'relevance',
  };
}

function serializeFacetState(state: FacetState): URLSearchParams {
  const params = new URLSearchParams();
  if (state.category) params.set('category', state.category);
  if (state.brand?.length) params.set('brand', state.brand.join(','));
  if (state.price) params.set('price', `${state.price.min}-${state.price.max}`);
  if (state.rating?.length) params.set('rating', state.rating.join(','));
  if (state.inStock) params.set('inStock', 'true');
  if (state.page > 1) params.set('page', String(state.page));
  if (state.sort !== 'relevance') params.set('sort', state.sort);
  return params;
}

כיצד לבחור מנוע וארכיטקטורה לחיפוש פאקטלי?

השאלה המרכזית היא היכן לבצע אגרגציה. Elasticsearch / OpenSearch היא הבחירה הנכונה עבור אלפי פריטים ומעלה. אגרגציות רצות בצד המנוע; אין צורך בשאילתות SQL. PostgreSQL עם jsonb + אינדקסי GIN עובד עבור עשרות אלפי פריטים אם Elasticsearch הוא מוגזם. Typesense / Meilisearch הן חלופות מתארחות עצמית עם תמיכה מובנית בפאקטים, פשוטות יותר לתפעול מ-Elasticsearch. בצד הלקוח, גישה מבוססת-לקוח מתאימה רק למערכי נתונים קטנים (עד 10,000 רשומות) הנטענים במלואם בדפדפן, תוך שימוש בספריות כמו Fuse.js או Lunr.js.

מנוע ביצועים (50k מוצרים) מורכבות פריסה תמיכה במונים
Elasticsearch 10–50 אלפיות השנייה לאגרגציה בינוני (קלאסטר, כוונון שאדים) כן באמצעות type FacetState = { category?: string; brand?: string[]; price?: { min: number; max: number }; rating?: number[]; inStock?: boolean; page: number; sort: 'relevance' | 'price_asc' | 'price_desc' | 'rating'; }; function parseFacetState(searchParams: URLSearchParams): FacetState { const price = searchParams.get('price'); const [priceMin, priceMax] = price ? price.split('-').map(Number) : [undefined, undefined]; return { category: searchParams.get('category') ?? undefined, brand: searchParams.get('brand')?.split(',').filter(Boolean), price: priceMin && priceMax ? { min: priceMin, max: priceMax } : undefined, rating: searchParams.get('rating')?.split(',').map(Number), inStock: searchParams.get('inStock') === 'true', page: Number(searchParams.get('page') ?? 1), sort: (searchParams.get('sort') as FacetState['sort']) ?? 'relevance', }; } function serializeFacetState(state: FacetState): URLSearchParams { const params = new URLSearchParams(); if (state.category) params.set('category', state.category); if (state.brand?.length) params.set('brand', state.brand.join(',')); if (state.price) params.set('price', `${state.price.min}-${state.price.max}`); if (state.rating?.length) params.set('rating', state.rating.join(',')); if (state.inStock) params.set('inStock', 'true'); if (state.page > 1) params.set('page', String(state.page)); if (state.sort !== 'relevance') params.set('sort', state.sort); return params; } + global
Typesense 2–10 אלפיות השנייה לשאילתה נמוך (בינארי יחיד) מובנה (filter)
PostgreSQL + jsonb 50–200 אלפיות השנייה נמוך (אם כבר קיים) דורש יישום ידני

שאלה נפוצה: מה ההבדל בין חיפוש פאקטלי לסינון רגיל? סינון רגיל פשוט מסתיר פריטים שאינם תואמים. חיפוש פאקטלי מציג בנוסף את מספר התוצאות עבור כל פילטר תוך התחשבות בפילטרים פעילים אחרים, ועוזר למשתמשים להעריך במהירות את ההשפעה של בחירותיהם. איזה מנוע הוא הטוב ביותר? עבור קטלוגים מעל 50,000 מוצרים, Elasticsearch או Typesense עדיפים, ומציעים חביון של 5–50 אלפיות השנייה. PostgreSQL עובד עבור נפחים קטנים יותר (עד 50,000) ופאקטים פשוטים. כיצד מעדכנים מונים כאשר פילטר נבחר מבלי לאבד ביצועים? השתמשו ב-post_filter ב-Elasticsearch או ב-facet_by המובנה של Typesense. זה מוציא את ההשפעה של הפילטר הנבחר על המונים של אותה קטגוריה. עבור תרחישים מורכבים, השתמשו באגרגציות גלובליות. כיצד יש לארגן URLs עבור חיפוש פאקטלי תוך התחשבות ב-SEO? שמרו את מצב הפילטרים בפרמטרי שאילתה. אינדקסו רק דפים ללא פילטרים ושילובים פופולריים. הוסיפו noindex לדפים עם מחיר, מיון או פילטרים מרובים. השתמשו ב-canonical לקטגוריית הבסיס. כמה זמן לוקח ליישם חיפוש פאקטלי? יישום פשוט על PostgreSQL עם 5 פאקטים ללא מונים אורך 3–5 ימים. פתרון מלא עם Elasticsearch או Typesense, אגרגציות, URL ו-SEO אורך 2–3 שבועות. הוספת סליידר מותאם אישית וחיפוש מיידי מוסיפה 3–5 ימים.

Elasticsearch לעומת Typesense: השוואת יישום פאקטים

Elasticsearch. דוגמת שאילתה עם סינון ואגרגציות:

POST /products/_search
{
  "query": {
    "bool": {
      "filter": [
        {
          "term": {
            "category": "laptops"
          }
        },
        {
          "range": {
            "price": {
              "gte": 50000,
              "lte": 150000
            }
          }
        }
      ]
    }
  },
  "aggs": {
    "brands": {
      "terms": {
        "field": "brand.keyword",
        "size": 20,
        "min_doc_count": 1
      }
    },
    "price_ranges": {
      "range": {
        "field": "price",
        "ranges": [
          {
            "key": "budget",
            "to": 50000
          },
          {
            "key": "mid",
            "from": 50000,
            "to": 100000
          },
          {
            "key": "premium",
            "from": 100000
          }
        ]
      }
    },
    "rating": {
      "terms": {
        "field": "rating",
        "size": 5
      }
    },
    "has_stock": {
      "filter": {
        "term": {
          "in_stock": true
        }
      },
      "aggs": {
        "count": {
          "value_count": {
            "field": "id"
          }
        }
      }
    }
  },
  "size": 20,
  "from": 0
}

היבט מרכזי בחיפוש פאקטלי: כאשר בוחרים פילטר מותג, המונים בפאקט 'מותג' צריכים להראות תוצאות ללא אותו פילטר (אחרת מותגים אחרים יראו אפס). זה נפתר באמצעות facet_by בשילוב עם אגרגציות POST /products/_search { "query": { "bool": { "filter": [ { "term": { "category": "laptops" } }, { "range": { "price": { "gte": 50000, "lte": 150000 } } } ] } }, "aggs": { "brands": { "terms": { "field": "brand.keyword", "size": 20, "min_doc_count": 1 } }, "price_ranges": { "range": { "field": "price", "ranges": [ { "key": "budget", "to": 50000 }, { "key": "mid", "from": 50000, "to": 100000 }, { "key": "premium", "from": 100000 } ] } }, "rating": { "terms": { "field": "rating", "size": 5 } }, "has_stock": { "filter": { "term": { "in_stock": true } }, "aggs": { "count": { "value_count": { "field": "id" } } } } }, "size": 20, "from": 0 } . ראו תיעוד Elasticsearch לפרטים.

Typesense. עבור פרויקטים שבהם Elasticsearch הוא מוגזם, Typesense מציע יתרונות ברורים: ההתקנה היא בינארי יחיד, וה-API אינטואיטיבי. דוגמת חיפוש עם פאקטים ב-TypeScript:

import Typesense from 'typesense';

const client = new Typesense.Client({
  nodes: [{ host: 'localhost', port: 8108, protocol: 'http' }],
  apiKey: 'xyz',
  connectionTimeoutSeconds: 2,
});

const results = await client.collections('products').documents().search({
  q: query || '*',
  query_by: 'name,description',
  filter_by: buildTypesenseFilter(state),
  facet_by: 'brand,category,rating',
  max_facet_values: 20,
  page: state.page,
  per_page: 20,
  sort_by: sortMap[state.sort],
});

function buildTypesenseFilter(state: FacetState): string {
  const filters: string[] = [];
  if (state.brand?.length) filters.push(`brand:=[${state.brand.join(',')}]`);
  if (state.price) filters.push(`price:>=${state.price.min} && price:<=${state.price.max}`);
  if (state.rating?.length) filters.push(`rating:=[${state.rating.join(',')}]`);
  if (state.inStock) filters.push('in_stock:=true');
  return filters.join(' && ');
}

Typesense מעדכן אוטומטית מונים כאשר פילטר נבחר, ללא post_filter. עבור קטלוגים מורכבים, זה מקצר את זמן הפיתוח.

רכיבי פאקטים ב-React

ה-hook post_filter מנהל את מצב הפילטרים ומסנכרן אותו עם ה-URL. הרכיב global מציג תיבות סימון עם מונים וכפתור 'הצג עוד' עבור רשימות ארוכות. דוגמת יישום:

import { useCallback, useMemo, useTransition } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { useDebouncedCallback } from 'use-debounce';

export function useFacetSearch() {
  const router = useRouter();
  const searchParams = useSearchParams();
  const [isPending, startTransition] = useTransition();

  const state = useMemo(
    () => parseFacetState(searchParams),
    [searchParams]
  );

  const updateFilter = useCallback(
    (updates: Partial<FacetState>) => {
      const newState = { ...state, ...updates, page: 1 };
      const params = serializeFacetState(newState);
      startTransition(() => {
        router.push(`?${params.toString()}`, { scroll: false });
      });
    },
    [state, router]
  );

  const debouncedPriceUpdate = useDebouncedCallback(
    (min: number, max: number) => updateFilter({ price: { min, max } }),
    400
  );

  return { state, updateFilter, debouncedPriceUpdate, isPending };
}

type FacetOption = {
  value: string;
  label: string;
  count: number;
};

interface CheckboxFacetProps {
  title: string;
  options: FacetOption[];
  selected: string[];
  onChange: (values: string[]) => void;
  showMore?: boolean;
}

export function CheckboxFacet({
  title,
  options,
  selected,
  onChange,
  showMore = false,
}: CheckboxFacetProps) {
  const [expanded, setExpanded] = useState(false);
  const visible = expanded || !showMore ? options : options.slice(0, 5);

  const toggle = (value: string) => {
    const next = selected.includes(value)
      ? selected.filter((v) => v !== value)
      : [...selected, value];
    onChange(next);
  };

  return (
    <div className="facet">
      <h3 className="facet__title">{title}</h3>
      <ul className="facet__options">
        {visible.map((opt) => (
          <li key={opt.value}>
            <label className={opt.count === 0 ? 'facet__option--disabled' : ''}>
              <input
                type="checkbox"
                checked={selected.includes(opt.value)}
                onChange={() => toggle(opt.value)}
                disabled={opt.count === 0}
              />
              <span>{opt.label}</span>
              <span className="facet__count">{opt.count}</span>
            </label>
          </li>
        ))}
      </ul>
      {showMore && options.length > 5 && (
        <button onClick={() => setExpanded(!expanded)}>
          {expanded ? 'Скрыть' : `Показать ещё ${options.length - 5}`}
        </button>
      )}
    </div>
  );
}

SEO לחיפוש פאקטלי

כתובות URL פאקטליות עם פילטרים יוצרות תוכן כפול. אסטרטגיה:

  • אינדקסו דפי קטגוריה ללא פילטרים ואת השילובים הפופולריים ביותר (מותג + קטגוריה).
  • noindex בדפים עם פילטרי מחיר, מיון, פילטרים מרובים.
  • canonical לדף קטגוריית הבסיס.
  • rel="nofollow" בקישורי עמודים מעבר לעמוד 3.
// В Next.js App Router
export async function generateMetadata({ searchParams }) {
  const state = parseFacetState(new URLSearchParams(searchParams));
  const hasComplexFilters = (state.brand?.length ?? 0) > 1 || state.price || state.page > 1;
  return {
    robots: hasComplexFilters ? 'noindex,follow' : 'index,follow',
  };
}

מה כולל העבודה

בצעו את הצעדים הבאים ליישום מוצלח של חיפוש פאקטלי:

  1. ניתוח דרישות ועיצוב סכמת פאקטים.
  2. הגדרת מנוע (Elasticsearch/Typesense) ואופטימיזציה.
  3. פיתוח API עם אגרגציות וסינון-לאחר.
  4. אינטגרציית צד לקוח (React/Next.js) עם סנכרון URL.
  5. בדיקות ביצועים (Core Web Vitals) וסילוק צווארי בקבוק.
  6. פריסה, תיעוד והדרכת צוות.
שלב תוצאה
ניתוח דרישות ועיצוב סכמת פאקטים מסמך המתאר פאקטים, סוגי פילטרים ולוגיקת מונים
הגדרת מנוע ואופטימיזציה (Elasticsearch/Typesense) קלאסטר מכוון עם שאדים ומיפויים אופטימליים
פיתוח API עם אגרגציות וסינון-לאחר נקודות קצה API לחיפוש וסינון עם ביצועים < 100 אלפיות השנייה
אינטגרציית צד לקוח (React/Next.js) רכיבי פילטרים מוכנים, סנכרון URL
בדיקות ביצועים (Core Web Vitals) וסילוק צווארי בקבוק דוח עם מדידות LCP, INP, TTFB
תיעוד והדרכת צוות README, הוראות פריסה, סקירת קוד
תמיכה לאחר השקה מוסכם בנפרד (החל מחודש)

לוחות זמנים

סוג פרויקט משך
פשוט (PostgreSQL, 5 פאקטים, ללא מונים) 3–5 ימים
מלא (Elasticsearch/Typesense, אגרגציות, URL, SEO) 2–3 שבועות
עם סליידר מותאם אישית, חיפוש מיידי, תפריט נייד +3–5 ימים

יישמנו חיפוש פאקטלי ביותר מ-15 פרויקטים של מסחר אלקטרוני. עם ניסיון של למעלה מ-15 שנים בחיפוש מסחר אלקטרוני, הצוות המוסמך שלנו מבטיח פתרון חזק. המתודולוגיה המוכחת שלנו מבטיחה אפס זמן השבתה במהלך ההעברה. החל מ-$1,500 עבור הגדרות בסיסיות, הפתרונות המפתח-בידי שלנו מספקים חיסכון משמעותי בעלויות בהשוואה לפיתוח פנימי. צרו קשר להערכת הפרויקט. קבלו ייעוץ עוד היום.