Каков наилучший способ реализовать поиск по подстроке в SQL? - PullRequest
7 голосов
/ 23 июля 2010

У нас здесь простая проблема с SQL.В столбце varchar мы хотели найти строку в любом месте поля.Каков наилучший способ реализовать это для производительности?Очевидно, что индекс не поможет здесь, какие-то другие уловки?

Мы используем MySQL и имеем около 3 миллионов записей.Нам нужно выполнять множество таких запросов в секунду, поэтому мы действительно стараемся реализовать их с максимальной производительностью.

Самый простой способ сделать это на данный момент:

Select * from table where column like '%search%'

Я должен дополнительно указать, что столбец на самом деле является длинной строкой, такой как "sadfasdfwerwe", и я долженпоиск "asdf" в этом столбце. То есть они не являются предложениями и пытаются найти в них слово .Может ли здесь помочь полнотекстовый поиск?

Ответы [ 4 ]

15 голосов
/ 23 июля 2010

Проверьте мою презентацию Практический полнотекстовый поиск в MySQL .

Я сравнил:

Сегодня я бы использовал Apache Solr , который переводит Lucene в сервис с кучей дополнительных функций и инструментов.


Re ваш комментарий: Ага, хорошо, нет. Ни одна из возможностей полнотекстового поиска, которые я упомянул, не поможет, поскольку все они предполагают какие-то границы слов

Другим способом эффективного поиска произвольных подстрок является подход N-грамм . По сути, создайте индекс всех возможных последовательностей из N букв и укажите строки, где встречается каждая соответствующая последовательность. Обычно это делается с N = 3 или триграммой , потому что это точка компромисса между соответствием более длинным подстрокам и сохранением индекса в управляемом размере.

Я не знаю ни одной базы данных SQL, которая бы прозрачно поддерживала индексирование N-граммы, но вы можете настроить ее самостоятельно, используя инвертированный индекс :

create table trigrams (
  trigram char(3) primary key
);

create table trigram_matches (
  trigram char(3),
  document_id int,
  primary key (trigram, document_id),
  foreign key (trigram) references trigrams(trigram),
  foreign key (document_id) references mytable(document_id)
);

Теперь заполни это трудным путем:

insert into trigram_matches
  select t.trigram, d.document_id
  from trigrams t join mytable d
    on d.textcolumn like concat('%', t.trigram, '%');

Конечно, это займет много времени! Но как только это будет сделано, вы сможете искать намного быстрее:

select d.*
from mytable d join trigram_matches t
  on t.document_id = d.document_id
where t.trigram = 'abc'

Конечно, вы можете искать шаблоны длиннее трех символов, но инвертированный индекс все еще помогает сузить ваш поиск:

select d.*
from mytable d join trigram_matches t
  on t.document_id = d.document_id
where t.trigram = 'abc'
  and d.textcolumn like '%abcdef%';
0 голосов
/ 07 февраля 2013
  1. Качество полнотекстового поиска mysql (для этой цели) низкое, если ваш язык не английский

  2. Поиск триграмм дает очень хорошие результаты для этой задачи

  3. postgreSQL имеет индекс триграмм , его легко использовать :)

  4. , но если вам нужно сделать это в mysql,Попробуйте это, улучшенная версия ответа Билла Карвина:

    - каждая триграмма сохраняется только один раз

    - простой класс php использует данные

    <?php
    
      /*
    
        # mysql table structure
        CREATE TABLE `trigram2content` (
    `trigram_id` int NOT NULL REFERENCES trigrams(id),
    `content_type_id` int(11) NOT NULL,
    `record_id` int(11) NOT NULL,
    PRIMARY KEY (`content_type_id`,`trigram_id`,`record_id`)
    );
    
    #each trigram is stored only once
    CREATE TABLE `trigrams` (
    `id` int not null auto_increment,
    `token` varchar(3) NOT NULL,
    PRIMARY KEY (id),
    UNIQUE token(token)
    ) DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
    
    
    SELECT count(*), record_id FROM trigrams t
    inner join trigram2content c ON t.id=c.trigram_id
    WHERE (
    t.token IN ('loc','ock','ck ','blo',' bl', ' bu', 'bur', 'urn')
    AND c.content_type_id = 0
    )
    GROUP by record_id
    ORDER BY count(*) DESC
    limit 20;
    
    
    */
    class trigram
    {
    
        private $dbLink;
    
        var $types = array(
            array(0, 'name'),
            array(1, 'city'));
    
    
        function trigram()
        {
          //connect to db
          $this->dbLink = mysql_connect("localhost", "username", "password");
          if ($this->dbLink) mysql_select_db("dbname");
          else mysql_error();
    
          mysql_query("SET NAMES utf8;", $this->dbLink);
        }
    
        function get_type_value($type_name){
          for($i=0; $i<count($this->types); $i++){
              if($this->types[$i][1] == $type_name)
                  return $this->types[$i][0];
          }
          return "";
        }
    
        function getNgrams($word, $n = 3) {
            $ngrams = array();
            $len = mb_strlen($word, 'utf-8');
            for($i = 0; $i < $len-($n-1); $i++) {
                $ngrams[] = mysql_real_escape_string(mb_substr($word, $i, $n, 'utf-8'), $this->dbLink);
            }
            return $ngrams;
        }
    
        /**
        input: array('hel', 'ell', 'llo', 'lo ', 'o B', ' Be', 'Bel', 'ell', 'llo', 'lo ', 'o  ')
        output: array(1,     2,     3,      4,      5,      6,      7,     2,   3,  4,      8)
        */
        private function getTrigramIds(&$t){
            $u = array_unique($t);
            $q = "SELECT * FROM trigrams WHERE token IN ('" . implode("', '", $u) . "')";
    
            $query = mysql_query($q, $this->dbLink);
            $n = mysql_num_rows($query);
    
            $ids = array(); //these trigrams are already in db, they have id
            $ok = array();
    
            for ($i=0; $i<$n; $i++)
            {
              $row = mysql_fetch_array($query, MYSQL_ASSOC);
              $ok []= $row['token'];
              $ids[ $row['token'] ] = $row['id'];
            }
            $diff = array_diff($u, $ok); //these trigrams are not yet in the db
            foreach($diff as $n){
                mysql_query("INSERT INTO trigrams (token) VALUES('$n')", $this->dbLink);
                $ids[$n]= mysql_insert_id();
            }
    
            //so many ids than items (if a trigram occurs more times in input, then it will occur more times in output as well)
            $result = array();
            foreach($t as $n){
                $result[]= $ids[$n];
            }
            return $result;
        }
    
        function insertData($id, $data, $type){
            $t = $this->getNgrams($data);
    
            $id = intval($id);
            $type = $this->get_type_value($type);
            $tIds = $this->getTrigramIds($t);
            $q = "INSERT INTO trigram2content (trigram_id, content_type_id, record_id) VALUES ";
            $rows = array();
            foreach($tIds as $n => $tid){
                $rows[]= "($tid, $type, $id)";
            }
            $q .= implode(", ", $rows);
            mysql_query($q, $this->dbLink);
        }
    
        function updateData($id, $data, $type){
            mysql_query("DELETE FROM trigram2content WHERE record_id=".intval($id)." AND content_type_id=".$this->get_type_value($type), $this->dbLink);
            $this->insertData($id, $data, $type);
        }
    
        function search($str, $type){
    
            $tri = $this->getNgrams($str);
            $max = count($tri);
            $q = "SELECT count(*), count(*)/$max as score, record_id FROM trigrams t inner join trigram2content c ON t.id=c.trigram_id
    WHERE (
    t.token IN ('" . implode("', '", $tri) . "')
    AND c.content_type_id = ".$this->get_type_value($type)."
    )
    GROUP by record_id
    HAVING score >= 0.6
    ORDER BY count(*) DESC
    limit 20;";
            $query = mysql_query($q, $this->dbLink);
            $n = mysql_num_rows($query);
    
            $result = array();
            for ($i=0; $i<$n; $i++)
            {
              $row = mysql_fetch_array($query, MYSQL_ASSOC);
              $result[] = $row;
            }
            return $result;
        }
    
    
    };
    

и использование:

 $t = new trigram();

 $t->insertData(1, "hello bello", "name");
 $t->insertData(2, "hellllo Mammmma mia", "name");

  print_r($t->search("helo", "name"));
0 голосов
/ 23 июля 2010

Во-первых, возможно, это проблема плохо спроектированной таблицы, в которой строка с разделителями хранится в одном поле вместо правильного проектирования для создания связанной таблицы. Если это так, вы должны исправить свой дизайн.

Если у вас есть поле с длинным описательным текстом (скажем, в поле примечаний) и поиск всегда выполняется по целому слову, вы можете выполнить полнотекстовый поиск.

Подумайте, можете ли вы требовать, чтобы ваши пользователи по крайней мере указывали вам первый символ того, что они ищут, если это обычное поле, такое как Last_name.

Подумайте о том, чтобы сначала выполнить поиск точного совпадения и выполнять подстановочный поиск только в том случае, если результаты не возвращаются. Это будет работать, если у вас есть пользователи, которые могут предоставить точные совпадения. Мы делали это однажды с поиском по названию аэропорта, и он возвращался очень быстро, если они вводили точное имя, и медленнее, если они этого не делали.

Если вы хотите искать только строки, которые не являются словами, которые могут быть где-то в тексте, вы в значительной степени застряли с плохой производительностью.

0 голосов
/ 23 июля 2010

Если вы хотите сопоставить целые слова, посмотрите на FULLTEXT индекс & MATCH() AGAINST().И, конечно же, возьмите на себя нагрузку на сервер базы данных: кешируйте результаты в течение необходимого времени для ваших конкретных потребностей.

...