Безпечний і зручний пошук в mysql

Коротка довідка по реалізації пошуку: Обробка рядки, вирізання службових символів, складання запиту до бази, логіка, посторінковий висновок, релевантність.

Частина 1: Загальні відомості

Насамперед треба порізати ручками рядок.

$ Search = substr ($ search, 0, 64);

64 символу користувачеві буде досить для пошуку. Тепер розпеченим залізом випалимо все "ненормальні" символи.

$ Search = preg_replace ( "/ [^ \ w \ x7F- \ xFF \ s] /", "", $ search);

За ідеєю, не можна давати користувачеві можливості шукати по занадто коротким словами - крім усього іншого, це сильно завантажує сервер. Отже, дозволимо шукати тільки зі слів, які довше двох букв (якщо обмеження більше, треба замінити "" на "").

$ Good = trim (preg_replace ( "/ \ s (\ S) \ s /", "", ereg_replace ( "+", "", "$ search")));

А після заміни поганих слів - треба стиснути подвійні пробіли (вони були зроблені спеціально для коректного пошуку коротких слів).

$ Good = ereg_eplace ( "+", "", $ good);

Припустимо, ми хочемо надати користувачеві можливість вибирати логіку пошуку - шукати все слова або тільки одне з декількох. Якщо ви хочете зробити як в Яндексі - два амперсанта означають "І" (слово1слово2слово3) або якось ще, то я не порадник. Шаманство з рядками на невеликому сайті imho не виправдовує витраченого часу. Тому форму для пошуку малюємо так:

А в пошуковому скрипті зайвий раз перевіряємо, що користувач ввів:

Як буде використовуватися логіка - нижче.

Непогано буде відразу інформувати користувача, скільки він знайшов рядків таблиці. Для цього робиться додатковий запит до бази:

$ Query = "SELECT id FROM table WHERE field LIKE '%". str_replace ( "", "% 'OR field LIKE'%", $ good). "% '";

Для статистики по окремим словами можна зробити наступне:

Посторінковий вивід результатів

Ну, коли у нас є макет для пошуку і кількість рядків результату пошуку, зробити посторінковий пошук - справа нехитра. Перевіряємо змінну $ page (незгірш від 0, не більше $ results_amount / $ rows_in_page) .В запит, який підраховує кількість рядків (дивись вище), пишемо потрібні нам поля та поля для сортування. А потім дописуємо

(Синтаксис: LIMIT <кол-во строк> або LIMIT <кол-во строк отступа>, <кол-во строк>)

В результаті виконання подібного запиту ми отримаємо саме ті самі рядки, які треба виводити на страніце.Для навігації можна або малювати посилання на наступну і попередню сторінки, або, що складніше, робити панель навігації на кілька сторінок.

Щоб підсвічувати світлом або жирним шрифтом шукані слова в тексті, треба зробити всього лише наступне:

Прогалини (а вони у нас між словами стоять поодинці, і ніде подвійний пробіл не зустрічається, до того ж з кінців рядка ми їх теж вирізали) досить замінити на вертикальну риску - роздільник варіантів в регулярних виразах. "Погані" слова ми не підсвічується, тому що в основі їх не шукаємо :). У коді, який виводить текст пишемо:

Після написання випуску я кинувся, було, писати і собі "підсвічування". Не тут то було! У мене в тексті зустрічаються теги HTML, тому довелося багато подумати. Вийшла ось така річ (рядок зі словами для підсвічування є):

Доводиться дивитися, немає в тезі це слово. Однак тут постає проблема ресурсоємності такої заміни (мій K6-266 над текстом в 5 кілобайт думав цілих сім секунд). Сумно.

Застосовуючи такі прийоми, можна, по-перше, обмежити свободу дій користувача і не дати йому а) дізнатися програмну структуру сайту б) викликати перевантаження сервера (наприклад, відправивши мегабайт тексту, що складається зі слів довжиною в три букви (фраза вийшла двозначна, але переписувати Не буду :), щоб скрипт 250 тисяч разів лазив в базу) в) побачити повідомлення про помилку в результаті попадання в рядок спецсимволов мови запитів. По-друге, деякий зручність для користувача - посторінковий вивід і підсвічування.

Пам'ятається в статті "Безпечний і зручний пошук" була така фраза

Частина 2. Коротко про релевантності

Для виведення результатів пошуку за релевантністю необхідно:

  • Необхідні поля VARCHAR, або будь-які з різновидів полів TEXT (SMALLTEXT, MEDIUMTEXT і т.п.) зробити ключами FULLTEXT:

ALTER TABLE table ADD FULLTEXT (field)

  • Далі - ще простіше:

    $ Query = "SELECT *, MATCH field AGAINST ( '$ searchwords') as relev FROM table ORDER BY relev DESC"

    Далі можна навішувати всякі LIMIT'и та інше для зручного виведення.

    • За замовчуванням встановлений пошук слів, що містять не менше 4 символів. Правиться установкою #define MIN_WORD_LEN 4 в исходнике ft_static.c, хоча на мій погляд правити це не потрібно.
    • Недоступні символи% в пошуковій фразі, слова в пошуковій фразі Парс з використанням списку разделетелей.
    • Список роздільників слів правиться в исходнике ft_static.c.
    • Необхідно мінімум десяток записів в таблиці для початку обчислення релевантності.
    • Не можна поле relev використовувати в Клаузе WHERE:

    SELECT *, MATCH field AGAINST ( '$ searchwords') as relev FROM table WHERE relev> 0 ORDER BY relev DESC

    SELECT *, MATCH field AGAINST ( '$ searchwords') as relev FROM table WHERE MATCH field AGAINST ( '$ searchwords')> 0 ORDER BY relev DESC

  • Швидкість досить висока - навіть в деяких випадках швидше like пошуку
  • Все вищесказане працює починаючи з версії MySQL 3.23.23
  • При створенні індексів FULLTEXT по декількох полях можливі 2 варіанти:

    У першому випадку можливий запит:

    SELECT *, MATCH field1, field2 AGAINST ( '$ searchwords') as relev FROM table ORDER BY relev DESC

    релевантність обчислюється у всіх полів відразу. У другому випадку такий запит видасть помилку. Тут обчислюємо релевантність наступним чином:

    SELECT *, MATCH field1 AGAINST ( '$ searchwords') + MATCH field2 AGAINST ( '$ searchwords') as relev FROM table ORDER BY relev DESC

    Другий варіант трохи складніше в запитах, однак, на мій погляд краще, тому що збільшується гнучкість пошуку - до кожного з полів можна задати, наприклад, коефіцієнт значущості і при підсумовуванні релевантні полів множити їх на цей коефіцієнт. Пошукова фраза буде "більше" шукатися в полях з великим коефіцієнтом. Наприклад, якщо ми робимо пошук по проіндексованих сторінок каталогу ресурсів, то поле імені сторінки зазвичай задають з великим коефіцієнтом, ніж поля мета-тегів описів або ключових слів.

    Частина 3: Вправи c релевантність

    Спочатку як додати FULLTEXT-індекс:

    Текстові індекси можна робити тільки в таблицях типу MyISAM. Тексти беруться з таблиці і скидаються в файл індексу, і зростає обсяг бази. З приводу запитів. Не можна поле relev використовувати в Клаузе WHERE:

    SELECT *, MATCH field AGAINST ( '$ searchwords') as relev FROM table WHERE relev> 0 ORDER BY relev DESC

    SELECT *, MATCH field AGAINST ( '$ searchwords') as relev FROM table WHERE MATCH field AGAINST ( '$ searchwords')> 0 ORDER BY relev DESC

    Обчислення поле, звичайно ж, не можна використовувати в WHERE за всіма правилами синтаксису, але можна використовувати в HAVING:

    SELECT *, MATCH field AGAINST ( '$ searchwords') as relev FROM table HAVING relev> 0 ORDER BY relev DESC

    Пошук через MATCH, як писав Олег, робиться тільки по слову цілком. Втім, за релевантністю можна тільки сортувати, а вибирати по LIKE (це, звичайно, позначиться на продуктивності, навіть не знаю, наскільки).

    Прибираємо умова "relev> 0", залишаємо сортування. Усе інше, як і раніше - рубаємо отриманий рядок і перетворюємо в запит з декількома операторами LIKE:

    SELECT *, MATCH field AGAINST ( '$ searchwords') AS relev FROM table WHERE field LIKE '% $ word1%' OR field LIKE '% $ word2%' ORDER BY relev DESC, datefield DESC

    Частина 4: Продолеженіе розпочатого

    MySQL пропонує в останніх версіях бази даних використовувати для повнотекстового пошуку індексацію FULLTEXT і конструкцію MATCH field AGAINST. Однак не на всіх серверах стоїть остання версія MySQL, і не всі хостинг-провайдери хочуть оновлювати софт з міркувань надійності системи.

    Свого часу я припускав, що пошук з сортуванням за релевантністю треба буде робити в кілька запитів, і, отже, краще зовсім не братися за це. Думки, що релевантність можна підраховувати в самому запиті віддалено мене відвідували, але я боявся навіть уявити таку конструкцію.

    Однак же, працівник однієї з сайтобудівна фірм Н-ска похвалився мені системою пошуку, яку вони застосовують на своїх сайтах. Я точно не запам'ятав запит, спробую так відтворити його:

    SELECT title, date_format (material_date, '% e.% C.% Y') AS date1, IF (text like '% word1 word2 word3%', 3 * 10, 0) + IF (text LIKE '% word1%', 9, 0) + IF (text LIKE '% word2%', 9, 0) + IF (text LIKE '% word3%', 9, 0) AS relevance FROM table WHERE text LIKE '% word1%' OR text LIKE ' % word2% 'OR text LIKE'% word3% 'ORDER BY relevance DESC, material_date DESC

    Жахливо виглядає, але працює навіть на старих версіях MySQL. Спробував порівняти швидкість роботи з ось таким запитом:

    SELECT title, date_format (material_date, '% e.% C.% Y') AS date1, MATCH text AGAINST ( 'word1 word2 word3') AS relevance FROM table WHERE text LIKE '% word1%' OR text LIKE '% word2% 'OR text LIKE'% word3% 'ORDER BY relevance DESC, material_date DESC

    В середньому швидкість універсального запиту в два рази менше, ніж використовує нові конструкції. Що цілком логічно - чим більше універсальність, тим більше ресурсомісткість.

    Спробуємо побудувати такий запит автоматично. Відрізаємо довгий рядок, а так само всі неправильні символи і короткі слова. Малюємо запит.

    $ Query = "SELECT title, date_format (material_date, '% e.% C.% Y') AS date1, IF (text like '%". $ Good_words. "%',". (Substr_count ($ good_words, "" ) + 1). "* 10, 0) + IF (text LIKE '%". str_replace ( "", "%', 9, 0) + IF (text LIKE '%", $ good_words). "%', 9, 0) AS relevance FROM table WHERE text LIKE '% ". str_replace ( "", "% 'OR text LIKE'%", $ good_words). "% 'ORDER BY relevance DESC, material_date DESC";

    Не дуже-то складно. Для надійності і захисту від флуду можна обмежити кількість слів у запиті.

    Деякі доповнення до колишніх публікацій

    Загальна кількість знайдених рядків в таблиці. Для виведення результатів пошуку, зрозуміло, треба користуватися оператором LIMIT (щоб не писати кожен раз формування цього параметра, користуйтеся готовими функціями). Якщо ніяких операцій угруповання в запиті не робиться, краще підрахувати кількість рядків відразу в запиті - COUNT (*), а не через функцію php mysql_num_rows (). Можете перевірити на великих таблицях. Якщо виробляються групові операції, робимо запит з COUNT (DISTINCT (<поле, по которому группируем>)), Але без GROUP BY.

    Підсвічування. Якщо в текстах не буває html-тегів, жити простіше

    $ Text = preg_replace ( "/ word1 | word2 | word3 / i", "\\ 0", $ Text);

    Якщо в тексті теги використовуються, то є три варіанти а) не робити підсвічування б) оскільки теги користувач не бачить (хіба що дуже цікавий користувач), то можна зробити поле індексу, в якому не буде тегів а символи [^ \ w \ x7F- \ xFF \ s] будуть замінені на прогалини (саме ці символи вирізаються з пошукового рядка на самому початку, так що пошук по них не проводиться). Пошук і підсвічування в такому випадку зробити саме за індексом. в) робити підсвічування тексту зі звичайного поля, попередньо вирізавши теги функцією srip_tags ().

    Повна версія пошукового коду, як завжди, в списку файлів.