Advertisement
riff

Search

Mar 31st, 2013
206
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
PHP 37.89 KB | None | 0 0
  1. <?php
  2.  
  3. class DB
  4. {
  5.     /**
  6.      * @var \mysqli
  7.      */
  8.     static public $handle = false;
  9.  
  10.     static public function connect($db, $login, $passw)
  11.     {
  12.         if (self::$handle = new mysqli('localhost', $login, $passw))
  13.         {
  14.             self::$handle->select_db($db);
  15.             self::$handle->set_charset('UTF8');
  16.             return true;
  17.         }
  18.         return false;
  19.     }
  20.  
  21.     /**
  22.      * Выполняет запрос к БД.
  23.      *
  24.      * @param string $sql
  25.      * @return \mysqli_result|bool
  26.      */
  27.     public static function query($sql)
  28.     {
  29.         return self::$handle->query($sql);
  30.     }
  31.  
  32.     /**
  33.      * Возвращает строку в стиле mysql
  34.      *
  35.      * @param string $value
  36.      * @param boolean $q
  37.      * @return string
  38.      */
  39.     public static function str($value, $q = true)
  40.     {
  41.         if ($value === null)
  42.             return 'null';
  43.         else
  44.             return ($q ? '"' : '').self::$handle->real_escape_string($value).($q ? '"' : '');
  45.     }
  46.  
  47.     /**
  48.      * Выполняет запрос к БД и получает одну запись
  49.      *
  50.      * @param string $sql
  51.      * @return bool|array
  52.      */
  53.     public static function get($sql)
  54.     {
  55.         if (!$query = DB::query($sql))
  56.             return false;
  57.  
  58.         return $query->fetch_assoc();
  59.     }
  60. }
  61.  
  62. if (!function_exists('stem')) {
  63.     function stem($word, $stemmer) {
  64.         /** @var \Lingua_Stem_Ru $stemmer */
  65.         return $stemmer->stem_word($word);
  66.     }
  67. }
  68.  
  69. class ISearch
  70. {
  71.     public static $RUSSIAN;
  72.  
  73.     const tbl_search= 'search';
  74.     const tbl_category= 'search_category';
  75.     const tbl_pages = 'search_pages';
  76.     const tbl_page = 'search_page';
  77.     const tbl_results = 'search_results';
  78.     const tbl_words = 'search_words';
  79.     const tbl_stopwords = 'search_stopwords';
  80.  
  81.     const id = 'id';
  82.     const active = 'active';
  83.     const position = 'position';
  84.     const title = 'title';
  85.     const path = 'path';
  86.  
  87.     public static $storage_engine = 'MyISAM'; //'InnoDB';
  88.  
  89.     /**
  90.      * язык подключаемого стеммера (например 'Ru').
  91.      * соответствующий класс (Lingua_Stem_Ru) должен находиться в файле "search.stem-ru.php"
  92.      *
  93.      * или константа подключаемого стеммера (например STEM_RUSSIAN_UNICODE),
  94.      * если у вас установлено расширение php_stem
  95.      *
  96.      * @var string
  97.      */
  98.     public static $stemmer;
  99.     private static $stemmers = [];
  100.  
  101.     public static $words_mask = false;
  102.  
  103.     public static $categories = array();
  104.  
  105.     private static function html2text($text)
  106.     {
  107.         $text = preg_replace(array(
  108.             '~<script[^>]*?>.*?<\/script>~si', // Strip out javascript
  109.             '~<style[^>]*?>.*?<\/style>~si', // Strip style tags properly
  110.             '~<[/\!]*?[^<>]*?>~si', // Strip out HTML tags
  111.             '~<![\s\S]*?--[ \t\n\r]*>~', // Strip multi-line comments including CDATA
  112.             '~{{.+?}}~',
  113.         ), ' ', $text);
  114.         $text = htmlspecialchars_decode($text);
  115.         $text = preg_replace('~\W~u', ' ', $text); //удаляем все символы кроме букв и цифр
  116.         $text = preg_replace('~\x20\w\x20~u', ' ', $text); //удаляем все одиночные буквы
  117.         $text = preg_replace('~\x20{2,}~', ' ', $text);
  118.         $text = trim($text);
  119.  
  120.         return $text;
  121.     }
  122.  
  123.     /**
  124.      * Добавить страницу к поиску
  125.      * @param string $category группа, к которой принадлежит страница
  126.      * @param int $page_id id страницы в вашем каталоге
  127.      * @param bool $active только активные будут участвовать в поиске
  128.      * @param string $title заголовок страницы в поиске
  129.      * @param string $path путь к странице в результатах поиска
  130.      * @param string $text
  131.      * @return bool
  132.      */
  133.     public static function add($category, $page_id, $active, $title, $path, $text)
  134.     {
  135.         $text = self::html2text($text); //очищаем текст от html, !@#$^:<>......
  136.         if (empty($text)) return false;
  137. //      $text = explode(' ', $text); //строку в массив
  138. //      $text = array_unique($text); //убираем все повторения слов
  139. //      if (empty($text)) return false;
  140.  
  141.         //добавляем информацию о новой странице
  142.         if (DB::query(strtr('
  143.             INSERT IGNORE INTO :tbl_page SET
  144.                 category_id = :category_id,
  145.                 page_id = :page_id,
  146.                 active = :active,
  147.                 path = :path,
  148.                 title = :title
  149.             ', array(
  150.                 ':tbl_page' => self::tbl_page,
  151.                 ':category_id' => self::$categories[$category],
  152.                 ':page_id' => $page_id,
  153.                 ':active'=>$active,
  154.                 ':path' => DB::str($path),
  155.                 ':title' => DB::str($title),
  156.             ))))
  157.         {
  158.             $search_page_id = DB::$handle->insert_id;
  159.         }
  160.         else
  161.         {
  162.             //если не удалось добавить, то, скорей всего, страница уже существует,
  163.             //получаем её id
  164.             if (!$search_page_id = DB::get(strtr('
  165.                 SELECT id FROM :page
  166.                 WHERE category_id = :category_id
  167.                     AND page_id = :page_id
  168.             ', array(
  169.                 ':page'=>self::tbl_page,
  170.                 ':category_id'=>self::$categories[$category],
  171.                 ':page_id'=>$page_id,
  172.             ))))
  173.                 return false;
  174.  
  175.             $search_page_id = $search_page_id['id'];
  176.         }
  177.  
  178.         $text = explode(' ', $text);
  179.  
  180.         //если стеммер включён
  181.         if ($stemmer = self::$stemmer)
  182.         {
  183.             //если у вас установлено php extension php_stem, то этот блок вам не нужен
  184.             if (is_string($stemmer))
  185.             {
  186.                 if (isset(self::$stemmers[$stemmer]))
  187.                 {
  188.                     $stemmer = self::$stemmers[self::$stemmer];
  189.                 }
  190.                 else
  191.                 {
  192.                     $stemmer = self::getStemmer();
  193.                     //закешируем созданный stemmer, чтобы не создавать новые
  194.                     //в случае многократного вызова функции
  195.                     self::$stemmers[self::$stemmer] = $stemmer;
  196.                 }
  197.             }
  198.  
  199.             //надо разобрать каждое слово
  200.             foreach ($text as &$word)
  201.             {
  202.                 $word = stem($word, $stemmer);
  203.                 unset($word);
  204.             }
  205.         }
  206.  
  207.         DB::query(strtr('
  208.             CREATE TEMPORARY TABLE IF NOT EXISTS temp_words (
  209.                 main_id MEDIUMINT UNSIGNED DEFAULT "0",
  210.                 word VARCHAR(25),
  211.                 UNIQUE KEY word (word)
  212.             ) ENGINE=:engine DEFAULT CHARSET=utf8
  213.         ', array(':engine'=>self::$storage_engine)));
  214.  
  215.         //заполняем временную таблицу слов словами из текста
  216.         DB::query(strtr('
  217.             INSERT IGNORE INTO temp_words (word)
  218.             VALUES :words
  219.         ', array(':words'=>'("'.implode('"),("', $text).'")')));
  220.  
  221.         //убрать из списка новых слов стоп-слова
  222. //      DB::query(strtr('
  223. //          DELETE temp_words FROM temp_words
  224. //          INNER JOIN :tbl_stopwords ON (temp_words.word = :tbl_stopwords.word)
  225. //      ', array(
  226. //          ':tbl_stopwords'=>self::tbl_stopwords,
  227. //      )));
  228.  
  229.         //передаём слова в основную таблицу, т.к. в основной таблице
  230.         //уникальный индекс, то попадут в неё только новые слова.
  231.         DB::query(strtr('
  232.             INSERT INTO :tbl_words (word)
  233.             SELECT word FROM temp_words
  234.             ON DUPLICATE KEY UPDATE count = count + 1
  235.         ', array(':tbl_words'=>self::tbl_words)));
  236.  
  237.         DB::query(strtr('
  238.             UPDATE :tbl_words SET
  239.             main_id = id
  240.             WHERE NOT main_id
  241.         ', array(':tbl_words'=>self::tbl_words)));
  242.  
  243.         //забираем из основной таблицы идентификаторы слов или их синонимов
  244.         DB::query(strtr('
  245.             UPDATE temp_words
  246.             INNER JOIN :tbl_words words ON temp_words.word = words.word
  247.             SET temp_words.main_id = words.main_id
  248.         ', array(':tbl_words'=>self::tbl_words)));
  249.  
  250.         //добавляем в таблицу список идентификаторов слов встретившихся в тексте
  251.         DB::query(strtr('
  252.             INSERT IGNORE INTO :tbl_pages (page_id, word_id)
  253.             SELECT :page_id, main_id FROM temp_words
  254.         ', array(
  255.             ':tbl_pages'=>self::tbl_pages,
  256.             ':page_id' => $search_page_id,
  257.         )));
  258.  
  259.         //очищаем временную таблицу слов
  260.         DB::query('TRUNCATE TABLE temp_words');
  261.  
  262.         return true;
  263.     }
  264.  
  265.     /**
  266.      * Обновить страницу в поиске
  267.      * @param string $category группа, к которой принадлежит страница
  268.      * @param int $page_id id страницы в вашем каталоге
  269.      * @param bool $active только активные будут участвовать в поиске
  270.      * @param string|null $title заголовок страницы в поиске
  271.      * @param string|null $path путь к странице в результатах поиска
  272.      * @param string $text
  273.      * @return bool
  274.      */
  275.     public static function update($category, $page_id, $active, $title, $path, $text)
  276.     {
  277.         if (!$search_page_id = DB::get(strtr('
  278.             SELECT SQL_NO_CACHE id FROM :tbl_page
  279.             WHERE category_id = :category_id
  280.                 AND page_id = :page_id
  281.         ', array(
  282.             ':tbl_page'=>self::tbl_page,
  283.             ':category_id'=>self::$categories[$category],
  284.             ':page_id'=>$page_id,
  285.         ))))
  286.             return false;
  287.  
  288.         $search_page_id = $search_page_id['id'];
  289.  
  290.         DB::query(strtr('
  291.             UPDATE :tbl_page SET
  292.                 active = :active,
  293.                 path = :path,
  294.                 title = :title
  295.             WHERE
  296.                 category_id = :category_id
  297.                 AND page_id = :page_id
  298.             ', array(
  299.                 ':tbl_page'=>self::tbl_page,
  300.                 ':active'=>$active,
  301.                 ':path' => $path ? DB::str($path) : '`path`',
  302.                 ':title' => $title ? DB::str($title) : '`title`',
  303.                 ':category_id' => self::$categories[$category],
  304.                 ':page_id' => $page_id,
  305.         )));
  306.  
  307.         DB::query(strtr('
  308.             TRUNCATE TABLE :tbl_results
  309.         ', array(':tbl_results'=>self::tbl_results)));
  310.  
  311.         DB::query(strtr('
  312.             DELETE FROM :tbl_pages
  313.             WHERE page_id = :page_id
  314.         ', array(
  315.             ':tbl_pages'=>self::tbl_pages,
  316.             ':page_id' => $search_page_id,
  317.         )));
  318.  
  319.         return self::add($category, $page_id, $active, $title, $path, $text);
  320.     }
  321.  
  322.     /**
  323.      * Удалить страницу из поиска
  324.      * @param string $category группа, к которой принадлежит страница
  325.      * @param int $page_id id страницы в вашем каталоге
  326.      * @return bool
  327.      */
  328.     public static function delete($category, $page_id)
  329.     {
  330.         if (!$page_id = (int)$page_id)
  331.             return false;
  332.  
  333.         if (!$page_id = DB::get(strtr('
  334.             SELECT SQL_NO_CACHE page_id FROM :tbl_page
  335.             WHERE category_id = :category_id
  336.                 AND page_id = :page_id
  337.         ', array(
  338.             ':tbl_page'=>self::tbl_page,
  339.             ':category_id'=>self::$categories[$category],
  340.             ':page_id'=>$page_id,
  341.         ))))
  342.             return false;
  343.  
  344.         $page_id = $page_id['page_id'];
  345.  
  346.         DB::query(strtr('
  347.             DELETE FROM :tbl_results
  348.             WHERE page_id = :page_id
  349.         ', array(
  350.             ':tbl_results'=>self::tbl_results,
  351.             ':page_id'=>$page_id,
  352.         )));
  353.  
  354.         DB::query(strtr('
  355.             DELETE FROM :tbl_pages
  356.             WHERE page_id = :page_id
  357.         ', array(
  358.             ':tbl_pages'=>self::tbl_pages,
  359.             ':page_id'=>$page_id,
  360.         )));
  361.  
  362.         DB::query(strtr('
  363.             DELETE FROM :tbl_page
  364.             WHERE id = :page_id
  365.         ', array(
  366.             ':tbl_page'=>self::tbl_page,
  367.             ':page_id'=>$page_id,
  368.         )));
  369.  
  370.         return true;
  371.     }
  372.  
  373.     /**
  374.      * заполняем таблицу словами близкими по значению
  375.      * пример: synonym('подгузник', 'pampers')
  376.      *         synonym('pampers', 'памперс')
  377.      * в данном примере слова 'pampers' и 'памперс' будут приравнены к 'подгузник'
  378.      * и в случае поиска по одному из этих трёх слов, страница будет найдена, если в ней
  379.      * встречается любое и этих слов.
  380.      *
  381.      * так же можно добавить слова имеющие приблизительно одинаковое значение
  382.      * пример: synonym('телевизор', 'sony') ..ничего другого не придумал
  383.      *         synonym('конструктор', 'lego')
  384.      *
  385.      * слова в написанные в транскрипции
  386.      * пример: synonym('mercedes', 'мерседес')
  387.      *
  388.      * слова в различных падежах
  389.      * пример: synonym('цветок', 'цветком')
  390.      *         synonym('цветок', 'цветка')
  391.      *         synonym('цветок', 'цветах')
  392.      *         ........
  393.      *
  394.      * @param string $word
  395.      * @param string|null $synonym
  396.      * @return bool
  397.      */
  398.     public static function synonym($word, $synonym = null)
  399.     {
  400.         if ($word === $synonym) $synonym = null;
  401.  
  402.         //находим слово
  403.         if (!$word_id = DB::get(strtr('
  404.             SELECT SQL_NO_CACHE id, main_id FROM :tbl_words
  405.             WHERE word = :word
  406.         ', array(
  407.             ':tbl_words'=>self::tbl_words,
  408.             ':word'=>DB::str($word),
  409.         )))) {
  410.             //если не нашли, то пытаемся его добавить
  411.             if (DB::query(strtr('
  412.                 INSERT INTO :tbl_words SET
  413.                 word = :word
  414.             ', array(
  415.                 ':tbl_words'=>self::tbl_words,
  416.                 ':word'=>DB::str($word),
  417.             )))) {
  418.                 //если синоним не задан, уходим
  419.                 if (is_null($synonym))
  420.                     return true;
  421.  
  422.                 $word_id = new \stdClass;
  423.                 $word_id->{'id'} = DB::$handle->insert_id;
  424.                 $word_id->{'main_id'} = $word_id->{'id'};
  425.             }
  426.             else {
  427.                 //если не получилось добавить, уходим
  428.                 return false;
  429.             }
  430.         }
  431.  
  432.         //если $synonym === null значит $word больше не должно быть синонимом другого слова
  433.         if (is_null($synonym))
  434.         {
  435.             //отвязываем синоним от слова
  436.             DB::query(strtr('
  437.                 UPDATE :tbl_words SET
  438.                     main_id = id
  439.                 WHERE id = :id
  440.             ', array(
  441.                 ':tbl_words'=>self::tbl_words,
  442.                 ':id'=>$word_id['id'],
  443.             )));
  444.             return true;
  445.         }
  446.  
  447.         //если $synonym !== null значит $synonym надо привязать к $word
  448.  
  449.         //если "главное слово" это не синоним другого слова, то берём его id, иначе id более главного слова
  450.         $word_id = $word_id->{'id'} === $word_id->{'main_id'} ? $word_id->{'id'} : $word_id->{'main_id'};
  451.  
  452.         //находим слово, которое надо превратить в синоним
  453.         if ($synonym_id = DB::get(strtr('
  454.             SELECT SQL_NO_CACHE id FROM :tbl_words
  455.             WHERE word = :synonym
  456.         ', array(
  457.             ':tbl_words'=>self::tbl_words,
  458.             ':synonym'=>DB::str($synonym)
  459.         )))) {
  460.             $synonym_id = $synonym_id['id'];
  461.         }
  462.         else {
  463.             //если не нашли, то пытаемся его добавить
  464.             if (DB::query(strtr('
  465.                 INSERT INTO :tbl_words SET word = :word
  466.             ', array(
  467.                 ':tbl_words'=>self::tbl_words,
  468.                 ':word'=>DB::str($synonym),
  469.             )))) {
  470.                 $synonym_id = DB::$handle->insert_id;
  471.             }
  472.             else {
  473.                 //если не получилось добавить, уходим
  474.                 return false;
  475.             }
  476.         }
  477.  
  478.         //делаем его синонимом
  479.         DB::query(strtr('
  480.             UPDATE :tbl_words SET main_id = :word_id
  481.             WHERE id = :id
  482.         ', array(
  483.             ':tbl_words'=>self::tbl_words,
  484.             ':word_id'=>$word_id,
  485.             ':id'=>$synonym_id,
  486.         )));
  487.  
  488.         //все другие слова, которые были синонимами этого слова, присваиваем главному слову
  489.         DB::query(strtr('
  490.             UPDATE :tbl_words SET main_id = :word_id
  491.             WHERE main_id = :id
  492.         ', array(
  493.             ':tbl_words'=>self::tbl_words,
  494.             ':word_id'=>$word_id,
  495.             ':id'=>$synonym_id,
  496.         )));
  497.  
  498.         return true;
  499.     }
  500.  
  501.     const STOPWORD_CHECK = 0;
  502.     const STOPWORD_ADD = 1;
  503.     const STOPWORD_REMOVE = -1;
  504.     /**
  505.      * Работа со стоп-словами
  506.      * @param string $word
  507.      * @param int $flag STOPWORD_CHECK - проверить принадлежит ли слово к группе стоп-слов,
  508.      *                  STOPWORD_ADD - добавить слово в стоп-словарь,
  509.      *                  STOPWORD_REMOVE - удалить из словаря
  510.      * @return bool
  511.      */
  512.     public static function stopword($word, $flag = self::STOPWORD_ADD)
  513.     {
  514.         if ($flag === self::STOPWORD_CHECK) {
  515.             if ($count = DB::get(strtr('
  516.                 SELECT SQL_NO_CACHE COUNT(*) AS count FROM :tbl_stopwords
  517.                 WHERE word = :word
  518.             ', array(
  519.                 ':tbl_stopwords'=>self::tbl_stopwords,
  520.                 ':word'=>DB::str($word),
  521.             ))))
  522.             {
  523.                 return (int)$count['count'] > 0;
  524.             }
  525.             return false;
  526.         }
  527.  
  528.         if ($flag === self::STOPWORD_REMOVE) {
  529.             return DB::query(strtr('
  530.                 DELETE FROM :tbl_stopwords
  531.                 WHERE word = :word
  532.             ', array(
  533.                 ':tbl_stopwords'=>self::tbl_stopwords,
  534.                 ':word'=>DB::str($word),
  535.             )));
  536.         }
  537.  
  538.         if ($flag === self::STOPWORD_ADD) {
  539.             return DB::query(strtr('
  540.                 INSERT INTO :tbl_stopwords SET word = :word
  541.             ', array(
  542.                 ':tbl_stopwords'=>self::tbl_stopwords,
  543.                 ':word'=>DB::str($word),
  544.             )));
  545.         }
  546.  
  547.         return false;
  548.     }
  549.  
  550.     /**
  551.      * @param string $text
  552.      * @return array|bool
  553.      */
  554.     private static function internal_clean_query($text)
  555.     {
  556.         $text = preg_replace('~[^\w\-*]~u', ' ', $text); //удаляем все символы кроме букв и цифр
  557.         //if (preg_match_all('~(\w+[’*]\w*|\w*[’*]\w+|\w+|\s-)~u', $text, $out)) //удаляем все символы кроме букв и цифр и разрешённых символов
  558.         //  $text = implode(' ', $out[1]);
  559.         $text = preg_replace('~\x20\w{1,2}\x20~u', ' ', $text); //удаляем все одиночные буквы
  560.  
  561.         $text = trim($text);
  562.         if (empty($text)) return false;
  563.  
  564.         $search = array('inc'=>'', 'exc'=>'');
  565.         //если в запросе встречаются слова исключения
  566.         if (($pos = strpos($text, ' -')) !== false)
  567.         {
  568.             $search['inc'] = substr($text, 0, $pos);
  569.             $search['exc'] = substr($text, $pos+2);
  570.         }
  571.         else
  572.         {
  573.             $search['inc'] = $text;
  574.         }
  575.  
  576.         //если слово написано через дефис, разделяем его на два слова
  577.         $search['inc'] = str_replace('-', ' ', $search['inc']);
  578.         $search['exc'] = str_replace('-', ' ', $search['exc']);
  579.         //заменяем любое кол-во пробелов на один пробел
  580.         $search['inc'] = preg_replace('~\x20{2,}~', ' ', $search['inc']);
  581.         $search['exc'] = preg_replace('~\x20{2,}~', ' ', $search['exc']);
  582.         //убираем пробелы в начале и в конце
  583.         $search['inc'] = trim($search['inc']);
  584.         $search['exc'] = trim($search['exc']);
  585.  
  586.         //начинаем охоту за стоп-словами
  587.         //берём весь очищенный текст
  588.         $text = $search['inc'].(empty($search['exc']) ? '' : ' '.$search['exc']);
  589.         $text = explode(' ', $text);
  590.         //запрашиваем пересечение слов из запроса и из стоп-словаря
  591.         if ($query = DB::query(strtr('
  592.             SELECT word FROM :tbl_stopwords
  593.             WHERE word IN (:words)
  594.         ', array(
  595.             ':tbl_stopwords' => self::tbl_stopwords,
  596.             ':words' => '"'.implode('","', $text).'"',
  597.         )))) {
  598.             while ($word = $query->fetch_assoc())
  599.                 $stopwords[] = $word['word'];
  600.         }
  601.         //если пересечения найдены
  602.         if (!empty($stopwords)) {
  603.             //избавляемся от стоп-слов в запросе
  604.             foreach ($search as $key=>$words)
  605.             {
  606.                 $words = explode(' ', $words);
  607.                 $words = array_flip($words);
  608.                 foreach ($stopwords as $word)
  609.                 {
  610.                     if (isset($words[$word]))
  611.                         unset($words[$word]);
  612.                 }
  613.                 $words = array_flip($words);
  614.                 $words = implode(' ', $words);
  615.                 $search[$key] = $words;
  616.             }
  617.         }
  618.  
  619.         return $search;
  620.     }
  621.  
  622.     /**
  623.      * Возвращает запрос очищенный от лишних символов, стоп-слов, ...
  624.      * @param string $text
  625.      * @return string
  626.      */
  627.     public static function clean_query($text)
  628.     {
  629.         $search = self::internal_clean_query($text);
  630.         //очищенный от всего мусора запрос
  631.         return $search['inc'].(empty($search['exc']) ? '' : ' - '.$search['exc']);
  632.     }
  633.  
  634.     protected static function getStemmer()
  635.     {
  636.         include_once(strtr('search.stem-:lang.php', array(':lang'=>strtolower(self::$stemmer))));
  637.         $stemmer = 'Lingua_Stem_'.self::$stemmer;
  638.         return new $stemmer;
  639.     }
  640.  
  641.     private static function internal_search($text)
  642.     {
  643.         $search = self::internal_clean_query($text);
  644.         //очищенный от всего мусора запрос
  645.         $text = $search['inc'].(empty($search['exc']) ? '' : ' - '.$search['exc']);
  646.  
  647.         if ($stemmer = self::$stemmer)
  648.         {
  649.             if (is_string($stemmer))
  650.             {
  651.                 $stemmer = self::getStemmer();
  652.             }
  653.         }
  654.  
  655.         //проверяем были ли похожие запросы
  656.         if (!$search_id = DB::get(strtr('
  657.             SELECT id FROM :tbl_search
  658.             WHERE text=:text
  659.         ', array(
  660.             ':tbl_search'=>self::tbl_search,
  661.             ':text'=>DB::str($text),
  662.         ))))
  663.         {
  664.             if (DB::query(strtr('
  665.                 INSERT INTO :tbl_search SET
  666.                 count=1,
  667.                 text = :text
  668.             ', array(
  669.                 ':tbl_search'=>self::tbl_search,
  670.                 ':text'=>DB::str($text),
  671.             )))){
  672.                 $search_id = DB::$handle->insert_id;
  673.             }
  674.         }
  675.         else {
  676.             $search_id = $search_id['id'];
  677.  
  678.             //увеличиваем счётчик запросов
  679.             DB::query(strtr('
  680.                 UPDATE :tbl_search SET
  681.                     count = count+1
  682.                 WHERE id = :id
  683.                 ', array(
  684.                     ':tbl_search'=>self::tbl_search,
  685.                     ':id'=>$search_id,
  686.                 )));
  687.  
  688.             //проверяем были ли результаты подобного поиска
  689.             if ($count = DB::get(strtr('
  690.                 SELECT COUNT(*) AS count
  691.                 FROM :tbl_results
  692.                 WHERE search_id=:id
  693.             ', array(
  694.                 ':tbl_results'=>self::tbl_results,
  695.                 ':id'=>$search_id,
  696.             )))) {
  697.                 if ($count['count'])
  698.                     return $search_id;
  699.             };
  700.         }
  701.  
  702.         if (!$search_id) return 0;
  703.  
  704.         //таблица промежуточных результатов поиска
  705.         DB::query(strtr('
  706.             CREATE TEMPORARY TABLE search_tmp (
  707.                 page_id MEDIUMINT UNSIGNED NOT NULL,
  708.                 status TINYINT(2) UNSIGNED NOT NULL,
  709.                 UNIQUE KEY page_id (page_id)
  710.             ) ENGINE=:engine ROW_FORMAT=FIXED DEFAULT CHARSET=utf8;
  711.         ', array(':engine'=>self::$storage_engine)));
  712.  
  713.         $real_words = false;
  714.  
  715.         /**
  716.          * @param array $words
  717.          * @return array
  718.          */
  719.         $get_real_words = function($words) use($stemmer, &$real_words)
  720.         {
  721.             $real_words = [];
  722.             foreach ($words as $word)
  723.             {
  724.                 //если встретилось слово с маской и маска разрешена
  725.                 //прекращщаем операцию
  726.                 if ((strpos($word, '*') !== false) && self::$words_mask) {
  727.                     $real_words = false;
  728.                     break;
  729.                 }
  730.                 //если stemmer включен
  731.                 elseif ($stemmer)
  732.                 {
  733.                     $real_words[] = stem($word, $stemmer);
  734.                 }
  735.                 else {
  736.                     $real_words[] = $word;
  737.                 }
  738.             }
  739.  
  740.             if ($real_words)
  741.             {
  742.                 $words = array();
  743.                 if ($query = DB::query(strtr('
  744.                     SELECT word FROM search_words
  745.                     WHERE word IN (:words)
  746.                     ORDER BY count
  747.                 ', array(
  748.                     ':words'=>'"'.implode('","', $real_words).'"'
  749.                 )))) {
  750.                     $real_words = true;
  751.  
  752.                     while ($word = $query->fetch_assoc())
  753.                     {
  754.                         $words[] = "word = '{$word['word']}'";
  755.                     }
  756.                 }
  757.             }
  758.  
  759.             return $words;
  760.         };
  761.  
  762.         /**
  763.          * @param array $words
  764.          * @return array
  765.          */
  766.         $get_words = function($words) use($stemmer)
  767.         {
  768.             foreach ($words as &$word)
  769.             {
  770.                 if (empty($word)) {
  771.                     unset($word);
  772.                     continue;
  773.                 }
  774.  
  775.                 if ((strpos($word, '*') !== false) && self::$words_mask)
  776.                 {
  777.                     $word = str_replace('*', '%', $word);
  778.                     $word = "word LIKE '$word'";
  779.                 }
  780.                 elseif ($stemmer)
  781.                 {
  782.                     $word = stem($word, $stemmer);
  783.                     //если индексирование проводилось с учётом стеммера,
  784.                     //то ищем точное совпадение, иначе по части слова
  785.                     $word = "word = '$word'";
  786.                 }
  787.                 else
  788.                 {
  789.                     $word = "word = '$word'";
  790.                 }
  791.                 unset($word);
  792.             }
  793.         };
  794.  
  795.         $status = 0;
  796.         foreach ($search as $key => $words)
  797.         {
  798.             $words = explode(' ', $words);
  799.             $words = array_unique($words);
  800.  
  801.             //если находимся в ветке искомых слов,
  802.             //попытаемся отсортировать слова по частоте использования
  803.             if ($key === 'inc')
  804.             {
  805.                 $words = $get_real_words($words);
  806.             }
  807.  
  808.             //если на предыдущем шаге не удалось обработать слова
  809.             //обрабатываем их другим способом
  810.             if (!$real_words)
  811.             {
  812.                 $words = $get_words($words);
  813.             }
  814.  
  815.             foreach ($words as $word)
  816.             {
  817.                 if (empty($word)) continue;
  818.  
  819.                 if ($key === 'inc')
  820.                 {
  821.                     if ($status === 0) {
  822.                         DB::query(strtr('
  823.                             INSERT IGNORE INTO search_tmp
  824.                             SELECT page.id, 1 FROM :tbl_page AS page
  825.                             LEFT JOIN :tbl_pages AS pages ON (
  826.                                 page.id = pages.page_id
  827.                                 AND page.active
  828.                             )
  829.                             INNER JOIN :tbl_words AS words ON (
  830.                                 pages.word_id = words.main_id
  831.                                 AND :word
  832.                             )
  833.                         ', array(
  834.                             ':tbl_page' => self::tbl_page,
  835.                             ':tbl_pages' => self::tbl_pages,
  836.                             ':tbl_words' => self::tbl_words,
  837.                             ':word' => $word,
  838.                         )));
  839.                     }
  840.                     else {
  841.                         //в оставшихся спроках помечаем те, которые подходят под следующее условие
  842.                         DB::query(strtr('
  843.                             UPDATE search_tmp
  844.                             INNER JOIN :tbl_pages pages ON search_tmp.page_id = pages.page_id
  845.                             INNER JOIN :tbl_words words ON pages.word_id = words.main_id
  846.                             SET search_tmp.status = :status + 1
  847.                             WHERE search_tmp.status = :status
  848.                                 AND (:word)
  849.                         ', array(
  850.                             ':tbl_pages' => self::tbl_pages,
  851.                             ':tbl_words' => self::tbl_words,
  852.                             ':status' => $status,
  853.                             ':word' => $word,
  854.                         )));
  855.                     }
  856.  
  857.                     $status++;
  858.                 }
  859.                 else {
  860.                     DB::query(strtr('
  861.                         UPDATE search_tmp
  862.                         INNER JOIN :tbl_pages pages ON search_tmp.page_id = pages.page_id
  863.                         INNER JOIN :tbl_words words ON pages.word_id = words.main_id
  864.                         SET search_tmp.status = 0
  865.                         WHERE (:word)
  866.                     ', array(
  867.                         ':tbl_pages' => self::tbl_pages,
  868.                         ':tbl_words' => self::tbl_words,
  869.                         ':word' => $word,
  870.                     )));
  871.                 }
  872.             }
  873.         }
  874.  
  875.         DB::query('SET @pos=0');
  876.         DB::query(strtr('
  877.             INSERT INTO :tbl_results
  878.             SELECT :id, @pos:=@pos+1, search_tmp.page_id
  879.             FROM search_tmp
  880.             INNER JOIN :tbl_page page ON search_tmp.page_id = page.id
  881.             WHERE search_tmp.status = :status
  882.             ORDER BY page.category_id, page.title
  883.         ', array(
  884.             ':tbl_results' => self::tbl_results,
  885.             ':tbl_page' => self::tbl_page,
  886.             ':id' => $search_id,
  887.             ':status' => $status,
  888.         )));
  889.  
  890.         return $search_id;
  891.     }
  892.  
  893.     /**
  894.      * @param string $text
  895.      * @param int $page_num номер выводимой страницы поиска
  896.      * @param int $limit кол-во элементов на странице
  897.      * @param string $category группа страниц по которым осуществляется поиск
  898.      * @return array ['id'=>'', 'active'=>'', 'category_id'=>'', 'page_id'=>'', 'path'=>'', 'title'=>'']
  899.      */
  900.     public static function search($text, $page_num = 0, $limit = 25, $category = '*')
  901.     {
  902.         $pages = array();
  903.  
  904.         if (!$search_id = self::internal_search($text))
  905.             return $pages;
  906.  
  907.         if ($query = DB::query(strtr('
  908.             SELECT page.*, results.position
  909.             FROM :tbl_results results
  910.             LEFT JOIN :tbl_page page ON (
  911.                 results.page_id = page.id
  912.                 AND page.active
  913.             )
  914.             WHERE (:category OR page.category_id = :category_id)
  915.                 AND results.search_id = :search_id
  916.                 AND (results.position > :from AND results.position <= :to)
  917.             ORDER BY results.position
  918.         ', array(
  919.             ':tbl_page'=>self::tbl_page,
  920.             ':tbl_results'=>self::tbl_results,
  921.             ':category'=>$category === '*' ? 1 : 0,
  922.             ':category_id'=>$category === '*' ? 0 : self::$categories[$category],
  923.             ':search_id'=>$search_id,
  924.             ':from'=>$page_num * $limit,
  925.             ':to'=>$page_num * $limit + $limit,
  926.         )))) {
  927.             $pages = $query->fetch_all(MYSQLI_ASSOC);
  928.         }
  929.  
  930.         return $pages;
  931.     }
  932. }
  933.  
  934. /////////////////////////////////////////////////////////////////////////////////////
  935. //для подключения к базе не забудьте вписать корректные параметры
  936. DB::connect('my_db', 'root', '');
  937. /////////////////////////////////////////////////////////////////////////////////////
  938.  
  939. ISearch::$RUSSIAN = defined('STEM_RUSSIAN_UNICODE') ? STEM_RUSSIAN_UNICODE : 'Ru';
  940.  
  941. //при первом обращении к скрипту загружаются группы (что такое группы написано ниже)
  942. if ($query = DB::query(strtr('SELECT * FROM :category',
  943.     array(':category'=>ISearch::tbl_category))))
  944. {
  945.     while ($category = $query->fetch_assoc())
  946.     {
  947.         ISearch::$categories[$category['category']] = $category['id'];
  948.     }
  949. }
  950.  
  951. /*
  952. -- Обратите внимание на ENGINE=MyISAM, и выберите подходящую (MyISAM или InnoDB)
  953. -- Также в ISearch::$storage_engine выберите нужный вариант (нужно для временных таблиц)
  954.  
  955. CREATE TABLE IF NOT EXISTS `search` (
  956.   `id` mediumint(8) unsigned NOT NULL AUTO_INCREMENT,
  957.   `count` mediumint(8) unsigned NOT NULL DEFAULT '0',
  958.   `text` varchar(255) NOT NULL,
  959.   PRIMARY KEY (`id`)
  960. ) ENGINE=MyISAM DEFAULT CHARSET=utf8 ROW_FORMAT=FIXED;
  961.  
  962. -- --------------------------------------------------------
  963.  
  964. CREATE TABLE IF NOT EXISTS `search_stopwords` (
  965.   `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  966.   `word` varchar(25) NOT NULL,
  967.   PRIMARY KEY (`id`),
  968.   UNIQUE KEY `word` (`word`)
  969. ) ENGINE=MyISAM DEFAULT CHARSET=utf8 ROW_FORMAT=FIXED;
  970.  
  971. -- --------------------------------------------------------
  972.  
  973. CREATE TABLE IF NOT EXISTS `search_page` (
  974.   `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  975.   `active` tinyint(1) unsigned NOT NULL DEFAULT '0',
  976.   `category_id` smallint(5) unsigned NOT NULL,
  977.   `page_id` int(10) unsigned NOT NULL,
  978.   `path` varchar(255) DEFAULT NULL,
  979.   `title` varchar(255) DEFAULT NULL,
  980.   PRIMARY KEY (`id`),
  981.   UNIQUE KEY `category_id` (`category_id`,`page_id`)
  982. ) ENGINE=MyISAM DEFAULT CHARSET=utf8 ROW_FORMAT=FIXED;
  983.  
  984. -- --------------------------------------------------------
  985.  
  986. CREATE TABLE IF NOT EXISTS `search_pages` (
  987.   `page_id` int(10) unsigned NOT NULL,
  988.   `word_id` mediumint(8) unsigned NOT NULL,
  989.   UNIQUE KEY `page_id_word_id` (`page_id`,`word_id`)
  990. ) ENGINE=MyISAM DEFAULT CHARSET=utf8;
  991.  
  992. -- --------------------------------------------------------
  993.  
  994. CREATE TABLE IF NOT EXISTS `search_results` (
  995.   `search_id` mediumint(8) unsigned NOT NULL,
  996.   `position` mediumint(8) unsigned NOT NULL DEFAULT '0',
  997.   `page_id` int(10) unsigned NOT NULL
  998. ) ENGINE=MyISAM DEFAULT CHARSET=utf8;
  999.  
  1000. -- --------------------------------------------------------
  1001.  
  1002. CREATE TABLE IF NOT EXISTS `search_words` (
  1003.   `id` mediumint(8) unsigned NOT NULL AUTO_INCREMENT,
  1004.   `main_id` mediumint(8) unsigned NOT NULL DEFAULT '0',
  1005.   `count` mediumint(8) unsigned NOT NULL DEFAULT '1',
  1006.   `word` varchar(25) NOT NULL,
  1007.   PRIMARY KEY (`id`),
  1008.   UNIQUE KEY `word` (`word`),
  1009.   KEY `main_id` (`main_id`)
  1010. ) ENGINE=MyISAM DEFAULT CHARSET=utf8 ROW_FORMAT=FIXED;
  1011.  
  1012. -- --------------------------------------------------------
  1013.  
  1014. CREATE TABLE IF NOT EXISTS `search_category` (
  1015.   `id` smallint(5) unsigned NOT NULL AUTO_INCREMENT,
  1016.   `category` varchar(15) NOT NULL,
  1017.   PRIMARY KEY (`id`)
  1018. ) ENGINE=MyISAM DEFAULT CHARSET=utf8 ROW_FORMAT=FIXED;
  1019.  
  1020. INSERT INTO `search_category` (`id`, `category`) VALUES
  1021. (1, 'page'),
  1022. (2, 'catalog'),
  1023. (3, 'product'),
  1024. (4, 'news');
  1025.  
  1026. */
  1027.  
  1028. /* ----- ПЛАН ДЕЙСТВИЙ (пример есть далее) ------------------------------
  1029.  
  1030. //очищаем таблицы перед индексированием
  1031. DB::query(strtr('
  1032.     TRUNCATE TABLE :tbl_page
  1033. ', array(':tbl_page' => ISearch::tbl_page)));
  1034. DB::query(strtr('
  1035.     TRUNCATE TABLE :tbl_pages
  1036. ', array(':tbl_pages' => ISearch::tbl_pages)));
  1037. DB::query(strtr('
  1038.     TRUNCATE TABLE :tbl_results
  1039. ', array(':tbl_results' => ISearch::tbl_results)));
  1040.  
  1041. //таблицу слов можно не очищать перед индексированием, тогда
  1042. //не придётся её заново заполнять
  1043. //DB::query(strtr('
  1044. //  TRUNCATE TABLE :tbl_words
  1045. //', array(':tbl_words' => ISearch::tbl_words)));
  1046.  
  1047. //таблицу слов можно заполнить вручную
  1048. ISearch::synonym('слово');
  1049. ISearch::synonym(...);
  1050. ISearch::synonym(...);
  1051.  
  1052. //можно задать синонимы
  1053. ISearch::synonym('цветок', 'цветка');
  1054. ISearch::synonym('цветок', 'цветком');
  1055. ISearch::synonym('мерседес', 'mercedes');
  1056.  
  1057. //и приступаем непосредственно к индексированию
  1058. //select * from какой-то_таблицы
  1059. //foreach(по записям) {
  1060.     ISearch::add(
  1061.         'page', //группа страниц (см. ниже)
  1062.         $id, //идентификатор вашей страницы (см. ниже)
  1063.         true, //указываем активна ли в данный момент страница
  1064.               //если не активна, то она проиндексируется, но не будет отражена
  1065.               //в результатах поиска
  1066.         $title, //заголовок, который будет выведен в результатах поиска
  1067.         $path, //ссылка на страницу
  1068.         $text //любой текст для индекса, актуальный для этой страницы:
  1069.               //заголовок, описание страницы, товара, сообщений с форума...
  1070.     );
  1071. //} //end foreach
  1072.  
  1073. //select * from другой_таблицы
  1074. //foreach(по записям) {
  1075.     ISearch::add(
  1076.         'news',
  1077.         ....
  1078. //} //end foreach
  1079.  
  1080. //что такое "группа страниц" - это разделение вашего индекса на логические группы.
  1081. //можно конечно свалить весь индекс в кучу, но тогда не возможно будет ограничить
  1082. //какой либо группой товара, или ограничить поиск страницами форума, новостями, ...
  1083.  
  1084. //"идентификатор вашей страницы" - обычно у каждой страницы (новости, товара, ветки каталога)
  1085. //есть уникальный идентификатор, по нему же и разделяются страницы в индексе в рамках одной группы.
  1086.  
  1087. // и сам поиск
  1088. ISearch::search(
  1089.     'найди меня',
  1090.     0,
  1091.     25
  1092. );
  1093. */
  1094.  
  1095. /* -------------------- ТЕСТ ------------------------ */
  1096. //после того как вы создали таблицы и вписали корректные параметры подключения
  1097. //раскомментируйте первый шаг, проиндексируйте вашу таблицу (запустив скрипт http://localhost/search.php)
  1098. //закомментируйте 1шаг
  1099. //раскомментируйти шаг 2, и попробуйте что-нибудь найти (http://localhost/search.php?search=hello)
  1100.  
  1101. //1. шаг
  1102. //false - пропустить этот шаг (индексирование)
  1103. //true - выполнить
  1104. if (false) {
  1105.     set_time_limit(0); //индексация может занять много времени,
  1106.                        //если у вас большой базы
  1107.  
  1108.     DB::query(strtr('
  1109.         TRUNCATE TABLE :tbl_page
  1110.     ', array(':tbl_page' => ISearch::tbl_page)));
  1111.     DB::query(strtr('
  1112.         TRUNCATE TABLE :tbl_pages
  1113.     ', array(':tbl_pages' => ISearch::tbl_pages)));
  1114.     DB::query(strtr('
  1115.         TRUNCATE TABLE :tbl_results
  1116.     ', array(':tbl_results' => ISearch::tbl_results)));
  1117.  
  1118.     //добавляем слова "синонимы"
  1119.     ISearch::synonym('велосипед', 'велописед');
  1120.     ISearch::synonym('велосипед', 'велик');
  1121.     //можно добавить проскланённые слова
  1122.     //ISearch::synonym('велосипед', 'велосипедов');
  1123.     //ISearch::synonym('велосипед', 'велосипедами');
  1124.     //ISearch::synonym('велосипед', 'велосипеде');
  1125.  
  1126.     //добавляем слова которые не надо учитывать в поиске
  1127.     ISearch::stopword('все');
  1128.     ISearch::stopword('очень');
  1129.  
  1130.     //базу можно проиндексировать стеммером Портера
  1131.     //индексация, возможно, займёт больше времени, т.к. надо обработать каждое слово в тексте
  1132.     //можно не индексировать стеммером Портера, а включить стеммер только для поиска
  1133.     //для включения стеммера эти два параметра обязательны.
  1134.     //не забудьте скачать файл search.stem-ru.php ( http://pastebin.com/PvJL9d7F ), если
  1135.     //у вас не установлено extension php_stem
  1136.  
  1137.     ISearch::$stemmer = ISearch::$RUSSIAN; //указываем язык стеммера или
  1138.         //если оставить пустым - индексация будет проводиться обычным способом
  1139.  
  1140.     if ($query = DB::query('
  1141.         SELECT * FROM shop_products
  1142.         -- LiMiT 1000
  1143.     ')) {
  1144.         while ($rec = $query->fetch_assoc())
  1145.         {
  1146.             //'id', 'active', 'title', 'text' - это поля в вашей таблице
  1147.             ISearch::add(
  1148.                 'product',
  1149.                 $rec['id'],
  1150.                 $rec['active'],
  1151.                 $rec['title'], //заголовок, который будет выведен в результатах поиска
  1152.                 '/products/'.$rec['id'], //ссылка на страницу
  1153.                 $rec['title'].' '.$rec['text'] //индексируем по двум полям
  1154.             );
  1155.         }
  1156.     }
  1157.     return;
  1158. }
  1159.  
  1160. //2. шаг
  1161. //false - прогпустить этот шаг (поиск)
  1162. //true - выполнить
  1163. if (false)
  1164. {
  1165.     //если что база была проиндексирована стеммером Портера
  1166.     ISearch::$stemmer = ISearch::$RUSSIAN;
  1167.  
  1168.     $pages = ISearch::search($_GET['search'], 0, 150);
  1169.     ?>
  1170.         <ul>
  1171.             <?
  1172.                 foreach ($pages as $page)
  1173.                 {
  1174.                     ?>
  1175.                         <li><a href="<?=$page['path'] ?>"><?=htmlspecialchars($page['title']) ?></a>
  1176.                     <?
  1177.                 }
  1178.             ?>
  1179.         </ul>
  1180.     <?
  1181. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement