OpenCart Kereső Gyorsítás & Javítás Fulltext indexelés + relevancia alapú rendezés – Journal3 kompatibilis

OpenCart 2.x / 3.x MySQL FULLTEXT Boolean Mode keresés Journal3 AJAX kereső 100k+ termék

📖 Áttekintés

Az OpenCart alapértelmezett keresője LIKE '%keresőszó%' lekérdezéseket használ, amelyek nagy termékadatbázisnál rendkívül lassúak, és nem képesek relevanciaalapú sorrendet kialakítani. Ez a dokumentáció bemutatja, hogyan lehet MySQL FULLTEXT indexelés és Boolean Mode keresés segítségével drasztikusan javítani a keresési élményt és sebességet.

A módosítás egyetlen PHP fájlt érint (catalog/model/catalog/product.php), és adatbázis oldali index-létrehozással egészül ki. Nincs szükség bővítmény telepítésére vagy OpenCart core fájlok tömeges módosítására.

ℹ️
Journal3 kompatibilitás

Az AJAX alapú azonnali keresés (instant search) ugyanezen getProducts() függvényt hívja, így a fejlesztés ott is azonnal érvényes lesz.

🎯 Mire jó?

A fejlesztés az alábbi problémákat oldja meg:

🐌

Lassú keresés nagy katalógusban

A LIKE '%szó%' full table scan-t végez. Fulltext index esetén a MySQL indexelt struktúrán keres.

🔢

Cikkszám nem jelenik meg elöl

Az alapértelmezett rendezés nem veszi figyelembe, hogy a találat termékszám-, SKU- vagy EAN-egyezés-e.

✂️

Részszavas keresés hiánya

A módosított logika +szó* prefixet alkalmaz, így a „kábel" kifejezés megtalálja a „kábelköteg" szót is.

📊

Relevancia alapú sorrend

A termékek pontszámot kapnak: a cikkszám-egyezés 15×, a leírás-egyezés 5× súlyú. A lista a legjobb találattal kezdődik.

🔡

Mértékegység-szétválasztás

A „12mm" keresést automatikusan „12 mm"-ként dolgozza fel, elkerülve a tokenizálási hibákat.

🔙

Fallback rövid keresésekre

1–2 karakteres keresési kifejezés esetén az eredeti LIKE-alapú logika lép életbe automatikusan.

Várt eredmények

Gyorsabb keresés nagy adatbázisnál

🏷️

Cikkszám / SKU / EAN prioritás

🔍

Részszavas (prefix) találatok

📈

Relevancia alapú sorrend

🛒

Journal3 AJAX kereső is javul

📦

100k+ terméknél is stabil

🖼️
Képernyőfotó helye
Keresési találati oldal – relevancia alapú sorrend demonstrációja (előtte / utána összehasonlítás)

📋 Rendszerkövetelmények

Adatbázis

Feltétel Szükséges Megjegyzés
MySQL / MariaDB verzió MySQL 5.6+ / MariaDB 10.0+ Kötelező – InnoDB FULLTEXT támogatás
Tábla motor InnoDB Kötelező – MyISAM esetén más szintaxis
innodb_ft_min_token_size 2 vagy 3 Ajánlott – 3 az alapértelmezett, 2-re módosítható
MySQL hozzáférés ALTER TABLE jog Kötelező – index létrehozáshoz
my.cnf szerkesztési jog Root / sudo Opcionális – csak ha a token size-t módosítani kell

OpenCart

Feltétel Szükséges Megjegyzés
OpenCart verzió 2.x vagy 3.x Kötelező
Módosítandó fájl catalog/model/catalog/product.php Kötelező
Biztonsági mentés Fájl + adatbázis backup Kötelező – módosítás előtt
Journal3 téma Opcionális – kompatibilis, de nem szükséges
⚠️
Módosítás előtt mindig készíts biztonsági mentést!

Mind a product.php fájlból, mind az adatbázisból. A FULLTEXT index hozzáadása visszafordítható, de a kód módosítása igényel megfelelő állapotot.


🚀 Megvalósítás – lépések sorrendben

1. lépés – Fulltext indexek létrehozása

Az első és legfontosabb lépés az adatbázis szintű fulltext indexek felvétele. Ezt futtatsd az SQL konzolban (phpMyAdmin, MySQL Workbench, vagy CLI).

💡
Mit indexelünk?

oc_product_description: a termék neve, leírása és tagek.  |  oc_product: cikkszám, SKU, UPC, EAN, JAN, ISBN, MPN azonosítók.

SQL
-- Termékleírás tábla: név, leírás, tag mezők indexelése
ALTER TABLE oc_product_description
ADD FULLTEXT ft_search_text (name, description, tag);

-- Termék tábla: azonosítók (cikkszám, SKU, EAN stb.) indexelése
ALTER TABLE oc_product
ADD FULLTEXT ft_search_codes (model, sku, upc, ean, jan, isbn, mpn);
⏱️
Futási idő

Nagy adatbázisnál (100k+ termék) az ALTER TABLE futása több percig is tarthat. Ezt lehetőleg alacsony forgalmú időszakban futtasd.

🖼️
Képernyőfotó helye
phpMyAdmin → SQL fül → az ALTER TABLE parancsok sikeres végrehajtása (zöld megerősítő üzenet)

2. lépés – MySQL konfiguráció ellenőrzése

Az InnoDB FULLTEXT motor alapból 3 karakter hosszú minimális token size-t alkalmaz. Ez azt jelenti, hogy 2 karakteres szavakra nem keres. Ellenőrizd az aktuális értéket:

SQL
SHOW VARIABLES LIKE 'innodb_ft_min_token_size';

Ha az érték 3 (alapértelmezett) – elfogadható

A rendszer így is megfelelően működik. A FULLTEXT + Boolean Mode keresés a 3+ karakteres szavakra hatékonyan dolgozik. Nincs szükség MySQL konfiguráció módosítására.

Ha 2-re szeretnéd módosítani (opcionális)

Ehhez szerver adminisztrátori hozzáférés szükséges:

my.cnf / my.ini
# [mysqld] szekcióba kell beilleszteni
innodb_ft_min_token_size = 2

MySQL újraindítása után az indexeket újra kell építeni:

SQL – Index újraépítés
ALTER TABLE oc_product_description DROP INDEX ft_search_text;
ALTER TABLE oc_product DROP INDEX ft_search_codes;

ALTER TABLE oc_product_description
ADD FULLTEXT ft_search_text (name, description, tag);

ALTER TABLE oc_product
ADD FULLTEXT ft_search_codes (model, sku, upc, ean, jan, isbn, mpn);
🖼️
Képernyőfotó helye
SHOW VARIABLES lekérdezés eredménye – innodb_ft_min_token_size aktuális értéke

3. lépés – PHP fájl módosítása

A keresési logika módosítása a getProducts() függvény teljes cseréjét jelenti a következő fájlban:

Fájl elérési útja
catalog/model/catalog/product.php
💾
Előbb mentsd el az eredeti fájlt!

Nevezd át product.php.bak-ra, vagy készíts belőle másolatot, mielőtt bármit módosítasz.

A módosítás logikája – mit és miért?

  • 1
    Boolean string előkészítés – A keresett szót tokenekre bontja, mértékegységeket szétválaszt (pl. „12mm" → „12 mm"), majd minden 3+ karakteres szóra +szó* formátumú FULLTEXT keresési kifejezést épít.
  • 2
    Relevancia számítás – Minden találathoz pontszámot számít: a leírás egyezése 5-szörös, a kódmező (cikkszám, EAN stb.) egyezése 15-szörös súlyú.
  • 3
    FULLTEXT keresés vagy LIKE fallback – Ha a boolean string nem üres (van elegendő hosszú szó), FULLTEXT-et használ. Ha nem (1–2 karakteres keresés), visszaesik az eredeti LIKE-alapú logikára.
  • 4
    Rendezés – Keresés esetén relevancia szerint csökkenő sorrendben adja vissza a termékeket. Egyéb esetekben az OpenCart eredeti rendezési logikája érvényes.
🖼️
Képernyőfotó helye
A product.php fájl megnyitva szövegszerkesztőben / FTP kliensben – getProducts() függvény helye jelölve

4. lépés – Cache törlése

A módosítás életbe lépéséhez az OpenCart cache-t törölni kell:

  1. 1

    Bejelentkezés az Admin felületre

    Navigálj az Admin → Dashboard oldalra.

  2. 2

    Developer Settings megnyitása

    Admin panel jobb felső sarkában keresd a fogaskerék ikont → Developer Settings.

    🖼️
    Képernyőfotó helye
    Admin → Developer Settings panel – Theme Cache és SASS Cache opciók
  3. 3

    Theme Cache törlése

    Kattints a „Clear Theme Files" gombra.

  4. 4

    Modifications frissítése

    Admin → Extensions → Modifications → kattints a „Refresh" gombra (kék frissítés ikon jobb felül).

    🖼️
    Képernyőfotó helye
    Admin → Extensions → Modifications oldal – Refresh gomb kiemelve

5. lépés – Tesztelés

Ellenőrizd a következő eseteket a boltban:

Teszteset Várt viselkedés Státusz
3+ karakteres keresőszó FULLTEXT találatok, relevancia szerint rendezve Ellenőrizd
Cikkszámra keresés (pl. „ABC123") A pontos cikkszámú termék kerül az első helyre Ellenőrizd
Részszavas keresés (pl. „kábel") „kábelköteg", „kábeltartó" is megjelenik Ellenőrizd
Mértékegységes keresés (pl. „12mm") Megtalálja a „12 mm" és „12mm" leírású termékeket Ellenőrizd
1–2 karakteres keresés LIKE fallback működik, nem üres az eredmény Ellenőrizd
Journal3 AJAX (instant) kereső Keresés közben is relevancia alapú lista jelenik meg Ellenőrizd
🖼️
Képernyőfotó helye
Keresési találati oldal – cikkszámra keresve az adott termék az első helyen jelenik meg

📄 Teljes kód – getProducts()

Az alábbi a getProducts() függvény teljes, valóban beilleszthető kódja. A catalog/model/catalog/product.php fájlban az eredeti public function getProducts($data = array()) { ... } blokkot kell erre cserélni.

💾
Mentsd el az eredeti fájlt beillesztés előtt!

Nevezd át product.php.bak-ra, vagy készíts FTP/szerver oldali biztonsági másolatot.

PHP – catalog/model/catalog/product.php → getProducts()
public function getProducts($data = array()) {
    // Boolean kereső string előkészítése
    $boolean = '';
    if (!empty($data['filter_name'])) {
        $search = trim($data['filter_name']);
        $search = preg_replace('/(\d)(mm|cm|m|kg|g|db|pcs|w|a|v)/i', '$1 $2', $search);
        $words = explode(' ', preg_replace('/\s+/', ' ', $search));
        foreach ($words as $word) {
            $word = preg_replace('/[^a-zA-Z0-9áéíóöőúüűÁÉÍÓÖŐÚÜŰ]/u', '', $word);
            if (strlen($word) >= 3) {
                $boolean .= '+' . $word . '* ';
            }
        }
        $boolean = trim($boolean);
    }

    $sql = "SELECT p.product_id,
        (SELECT AVG(rating) AS total FROM " . DB_PREFIX . "review r1
            WHERE r1.product_id = p.product_id AND r1.status = '1'
            GROUP BY r1.product_id) AS rating,
        (SELECT price FROM " . DB_PREFIX . "product_discount pd2
            WHERE pd2.product_id = p.product_id
            AND pd2.customer_group_id = '" . (int)$this->config->get('config_customer_group_id') . "'
            AND pd2.quantity = '1'
            AND ((pd2.date_start = '0000-00-00' OR pd2.date_start < NOW())
            AND (pd2.date_end = '0000-00-00' OR pd2.date_end > NOW()))
            ORDER BY pd2.priority ASC, pd2.price ASC LIMIT 1) AS discount,
        (SELECT price FROM " . DB_PREFIX . "product_special ps
            WHERE ps.product_id = p.product_id
            AND ps.customer_group_id = '" . (int)$this->config->get('config_customer_group_id') . "'
            AND ((ps.date_start = '0000-00-00' OR ps.date_start < NOW())
            AND (ps.date_end = '0000-00-00' OR ps.date_end > NOW()))
            ORDER BY ps.priority ASC, ps.price ASC LIMIT 1) AS special";

    // Relevancia számítás hozzáadása, ha van keresési kifejezés
    if (!empty($boolean)) {
        $sql .= ", (
            MATCH(pd.name, pd.description, pd.tag)
                AGAINST('" . $boolean . "' IN BOOLEAN MODE) * 5
            +
            MATCH(p.model, p.sku, p.upc, p.ean, p.jan, p.isbn, p.mpn)
                AGAINST('" . $boolean . "' IN BOOLEAN MODE) * 15
        ) AS relevance";
    }

    if (!empty($data['filter_category_id'])) {
        if (!empty($data['filter_sub_category'])) {
            $sql .= " FROM " . DB_PREFIX . "category_path cp LEFT JOIN " . DB_PREFIX . "product_to_category p2c ON (cp.category_id = p2c.category_id)";
        } else {
            $sql .= " FROM " . DB_PREFIX . "product_to_category p2c";
        }
        if (!empty($data['filter_filter'])) {
            $sql .= " LEFT JOIN " . DB_PREFIX . "product_filter pf ON (p2c.product_id = pf.product_id) LEFT JOIN " . DB_PREFIX . "product p ON (pf.product_id = p.product_id)";
        } else {
            $sql .= " LEFT JOIN " . DB_PREFIX . "product p ON (p2c.product_id = p.product_id)";
        }
    } else {
        $sql .= " FROM " . DB_PREFIX . "product p";
    }

    $sql .= " LEFT JOIN " . DB_PREFIX . "product_description pd ON (p.product_id = pd.product_id)"
          . " LEFT JOIN " . DB_PREFIX . "product_to_store p2s ON (p.product_id = p2s.product_id)"
          . " WHERE pd.language_id = '" . (int)$this->config->get('config_language_id') . "'"
          . " AND p.status = '1' AND p.date_available <= NOW()"
          . " AND p2s.store_id = '" . (int)$this->config->get('config_store_id') . "'";

    if (!empty($data['filter_category_id'])) {
        if (!empty($data['filter_sub_category'])) {
            $sql .= " AND cp.path_id = '" . (int)$data['filter_category_id'] . "'";
        } else {
            $sql .= " AND p2c.category_id = '" . (int)$data['filter_category_id'] . "'";
        }
        if (!empty($data['filter_filter'])) {
            $implode = array();
            $filters = explode(',', $data['filter_filter']);
            foreach ($filters as $filter_id) {
                $implode[] = (int)$filter_id;
            }
            $sql .= " AND pf.filter_id IN (" . implode(',', $implode) . ")";
        }
    }

    // FULLTEXT keresés ha van elegendő hosszú szó, egyébként LIKE fallback
    if (!empty($boolean)) {
        $sql .= " AND (
            MATCH(pd.name, pd.description, pd.tag)
                AGAINST('" . $boolean . "' IN BOOLEAN MODE)
            OR
            MATCH(p.model, p.sku, p.upc, p.ean, p.jan, p.isbn, p.mpn)
                AGAINST('" . $boolean . "' IN BOOLEAN MODE)
        )";
    } elseif (!empty($data['filter_name']) || !empty($data['filter_tag'])) {
        // Fallback rövid kereséshez (1-2 karakter)
        $sql .= " AND (";
        if (!empty($data['filter_name'])) {
            $implode = array();
            $words = explode(' ', trim(preg_replace('/\s+/', ' ', $data['filter_name'])));
            foreach ($words as $word) {
                $word = $this->db->escape($word);
                $implode[] = "(pd.name LIKE '%$word%' OR pd.tag LIKE '%$word%')";
            }
            if ($implode) {
                $sql .= " " . implode(" AND ", $implode) . "";
            }
            if (!empty($data['filter_description'])) {
                $sql .= " OR pd.description LIKE '%" . $this->db->escape($data['filter_name']) . "%'";
            }
        }
        if (!empty($data['filter_name']) && !empty($data['filter_tag'])) {
            $sql .= " OR ";
        }
        if (!empty($data['filter_tag'])) {
            $implode = array();
            $words = explode(' ', trim(preg_replace('/\s+/', ' ', $data['filter_tag'])));
            foreach ($words as $word) {
                $implode[] = "pd.tag LIKE '%" . $this->db->escape($word) . "%'";
            }
            if ($implode) {
                $sql .= " " . implode(" AND ", $implode) . "";
            }
        }
        if (!empty($data['filter_name'])) {
            $sql .= " OR LCASE(p.model) = '"  . $this->db->escape(utf8_strtolower($data['filter_name'])) . "'";
            $sql .= " OR LCASE(p.sku)   = '"  . $this->db->escape(utf8_strtolower($data['filter_name'])) . "'";
            $sql .= " OR LCASE(p.upc)   = '"  . $this->db->escape(utf8_strtolower($data['filter_name'])) . "'";
            $sql .= " OR LCASE(p.ean)   = '"  . $this->db->escape(utf8_strtolower($data['filter_name'])) . "'";
            $sql .= " OR LCASE(p.jan)   = '"  . $this->db->escape(utf8_strtolower($data['filter_name'])) . "'";
            $sql .= " OR LCASE(p.isbn)  = '"  . $this->db->escape(utf8_strtolower($data['filter_name'])) . "'";
            $sql .= " OR LCASE(p.mpn)   = '"  . $this->db->escape(utf8_strtolower($data['filter_name'])) . "'";
        }
        $sql .= ")";
    }

    if (!empty($data['filter_manufacturer_id'])) {
        $sql .= " AND p.manufacturer_id = '" . (int)$data['filter_manufacturer_id'] . "'";
    }

    $sql .= " GROUP BY p.product_id";

    $sort_data = array(
        'pd.name',
        'p.model',
        'p.quantity',
        'p.price',
        'rating',
        'p.sort_order',
        'p.date_added'
    );

    // Relevancia alapú rendezés keresés esetén, egyébként az OpenCart eredeti logikája
    if (!empty($boolean)) {
        $sql .= " ORDER BY relevance DESC";
    } elseif (isset($data['sort']) && in_array($data['sort'], $sort_data)) {
        if ($data['sort'] == 'pd.name' || $data['sort'] == 'p.model') {
            $sql .= " ORDER BY LCASE(" . $data['sort'] . ")";
        } elseif ($data['sort'] == 'p.price') {
            $sql .= " ORDER BY (CASE WHEN special IS NOT NULL THEN special WHEN discount IS NOT NULL THEN discount ELSE p.price END)";
        } else {
            $sql .= " ORDER BY " . $data['sort'];
        }
    } else {
        $sql .= " ORDER BY p.sort_order";
    }

    if (isset($data['order']) && ($data['order'] == 'DESC')) {
        $sql .= " DESC, LCASE(pd.name) DESC";
    } else {
        $sql .= " ASC, LCASE(pd.name) ASC";
    }

    if (isset($data['start']) || isset($data['limit'])) {
        if ($data['start'] < 0) {
            $data['start'] = 0;
        }
        if ($data['limit'] < 1) {
            $data['limit'] = 20;
        }
        $sql .= " LIMIT " . (int)$data['start'] . "," . (int)$data['limit'];
    }

    $product_data = array();
    $query = $this->db->query($sql);
    foreach ($query->rows as $result) {
        $product_data[$result['product_id']] = $this->getProduct($result['product_id']);
    }
    return $product_data;
}
Ez a valódi, teljes kód – közvetlenül beilleszthető

A fenti „Másolás" gombbal vágólapra másolható, majd a product.php fájlban az eredeti getProducts() függvény helyére illeszthető be.

🔧 Hibaelhárítás

Nem jelennek meg találatok kereséskor

⚠️
Lehetséges ok: az indexek még nem épültek fel

Ellenőrizd az INFORMATION_SCHEMA.STATISTICS táblában, hogy az ft_search_text és ft_search_codes indexek valóban léteznek-e.

SQL – Index ellenőrzés
SELECT TABLE_NAME, INDEX_NAME, INDEX_TYPE
FROM INFORMATION_SCHEMA.STATISTICS
WHERE TABLE_SCHEMA = DATABASE()
  AND INDEX_NAME IN ('ft_search_text', 'ft_search_codes');

SQL szintaxishiba a módosítás után

Ellenőrizd, hogy a tábla prefix helyes-e. Az oc_ prefix az OpenCart alapértelmezése, de ez konfigurációfüggő. A DB_PREFIX konstans értéke a config.php fájlban ellenőrizhető.

A Journal3 AJAX kereső nem változott

Töröld a böngésző cache-t, és ellenőrizd, hogy a Modifications refresh lefutott-e (4. lépés). Ha a Journal3 saját modellt használ, annak is módosítani kell a keresési logikáját.

Lassabb lett a keresés az indexelés után

ℹ️
InnoDB FULLTEXT cache mérete

Nagy adatbázisnál érdemes a innodb_ft_cache_size és innodb_ft_total_cache_size értékeket növelni a my.cnf fájlban.