最佳实践-数据库的模糊搜索

一 常见场景

1.1 数据库表创建

首先创建一个学生表并添加姓名的普通索引。

CREATE TABLE `school`.`student`  (
  `id` bigint(10) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `age` int(3) NULL DEFAULT NULL,
  `class_number` bigint(10) NULL DEFAULT NULL,
  `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  `update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `name`(`name`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

并为其中填充几条数据:

INSERT INTO `school`.`student` (`id`, `name`, `age`, `class_number`, `create_time`, `update_time`) VALUES (1, '张三', 10, 3, '2023-07-20 09:31:29', '2023-07-20 09:31:34');
INSERT INTO `school`.`student` (`id`, `name`, `age`, `class_number`, `create_time`, `update_time`) VALUES (2, '李四', 11, 3, '2023-07-20 09:31:44', '2023-07-20 09:31:44');
INSERT INTO `school`.`student` (`id`, `name`, `age`, `class_number`, `create_time`, `update_time`) VALUES (3, '王五', 10, 3, '2023-07-20 09:32:19', '2023-07-20 09:32:19');
INSERT INTO `school`.`student` (`id`, `name`, `age`, `class_number`, `create_time`, `update_time`) VALUES (4, '盖伦', 9, 3, '2023-07-20 09:32:39', '2023-07-20 09:32:39');
INSERT INTO `school`.`student` (`id`, `name`, `age`, `class_number`, `create_time`, `update_time`) VALUES (5, '普朗克', 10, 3, '2023-07-20 09:32:56', '2023-07-20 09:32:56');
INSERT INTO `school`.`student` (`id`, `name`, `age`, `class_number`, `create_time`, `update_time`) VALUES (6, '卡特', 12, 3, '2023-07-20 09:34:31', '2023-07-20 09:34:31');

1.2 查询案例

假设现在有需要对name进行模糊查询,sql如下:

select * from student where name like '%王%';

1.3 案例分析

EXPLAIN看看执行计划:

从图中发现是没有走索引的。那怎么办?索引岂不是白建了?

1.4 解决方法

别慌,先给你支一招,如下图:

走最左侧原则,去掉左边的通配符,就可以发现能走索引了。

产品:我要的就是要全模糊,不要一边模糊搜索。

那该怎么办呢?

1.5 全文索引

这时候可以采用终极杀招,使用mysql 自带的全文索引。不懂全文索引的可以先去了解一下。简而言之能实现我们全模糊的要求还走索引,是一种空间换时间的典型。

create fulltext index name_fulltext on student(name) WITH PARSER ngram;;

首先建立一个name的全文索引。

接着采用全文索引提供的布尔查询(也可以在查询参数上加通配符*):

select * from student where match(name) against('朗' IN BOOLEAN MODE);
select * from student where MATCH ( name ) against ( '王' IN NATURAL LANGUAGE MODE );

发现,结果啥也没有。什么情况?

这个问题有很多原因,其中最常见的就是最小搜索长度导致的。

MySQL 中的全文索引,有两个变量,最小搜索长度和最大搜索长度,对于长度小于最小搜索长度和大于最大搜索长度的词语,都不会被索引。通俗点就是说,想对一个词语使用全文索引搜索,那么这个词语的长度必须在以上两个变量的区间内。

这两个的默认值可以使用以下命令查看

show variables like '%ft%';

可以看到这两个变量在 MyISAM 和 InnoDB 两种存储引擎下的变量名和默认值

// MyISAM
ft_min_word_len = 4;
ft_max_word_len = 84;

// InnoDB
innodb_ft_min_token_size = 3;
innodb_ft_max_token_size = 84;

可以看到最小搜索长度 MyISAM 引擎下默认是 4,InnoDB 引擎下是 3,也即,MySQL 的全文索引只会对长度大于等于 4 或者 3 的词语建立索引,而刚刚搜索的长度大于等于 4。

配置最小搜索长度

全文索引的相关参数都无法进行动态修改,必须通过修改 MySQL 的配置文件来完成。修改最小搜索长度的值为 1,首先打开 MySQL 的配置文件 /etc/my.cnf,在 [mysqld] 的下面追加以下内容

[mysqld]
innodb_ft_min_token_size = 1
ft_min_word_len = 1

可以使用以下命令查询配置文件路径
mysql --help|grep 'my.cnf'

然后重启 MySQL 服务器,并修复全文索引。
⚠️注意,修改完参数以后,一定要修复下索引,不然参数不会生效。

两种方式,一种是重建索引,一种是执行命令。

repair table student quick;

MySQL 的全文索引最开始仅支持英语,因为英语的词与词之间有空格,使用空格作为分词的分隔符是很方便的。亚洲文字,比如汉语、日语、汉语等,是没有空格的,这就造成了一定的限制。不过 MySQL 5.7.6 开始,引入了一个 ngram 全文分析器来解决这个问题,并且对 MyISAM 和 InnoDB 引擎都有效。

再次执行执行计划,可以看到现在是走索引了。

1.6 ElasticSearch

还有一个执行效率更高更快的办法,那就是引入ES。这里不过多介绍,大概方案就是,用text类型对需要模糊查询对词进行分词,其次是利用match查询进行模糊查询。要达到模糊的效果,首先是要对中英文采取不同对分词器,然后利用match的Operator.AND熟悉进行查询。

Operator.AND和Operator.OR是用于设置match查询的操作符的枚举类型。它们的区别在于如何处理多个查询词的匹配逻辑:
Operator.AND:使用AND操作符,表示所有的查询词都必须出现在匹配的文档中。只有当文档中同时包含所有的查询词时,才会被匹配到。
Operator.OR:使用OR操作符,表示只要文档中包含任何一个查询词,就会被匹配到。只要文档中包含至少一个查询词,就会被视为匹配。

具体案例为:

{
  "from": 0,
  "size": 20,
  "timeout": "60s",
  "query": {
    "bool": {
      "must": [
        {
          "exists": {
            "field": "yb_code",
            "boost": 1
          }
        },
        {
          "match": {
            "name": {
              "query": "高血压",
              "operator": "AND",
              "prefix_length": 0,
              "max_expansions": 50,
              "fuzzy_transpositions": true,
              "lenient": false,
              "zero_terms_query": "NONE",
              "auto_generate_synonyms_phrase_query": true,
              "boost": 1
            }
          }
        }
      ],
      "filter": [
        {
          "bool": {
            "must": [
              {
                "term": {
                  "category": {
                    "value": 1,
                    "boost": 1
                  }
                }
              }
            ],
            "adjust_pure_negative": true,
            "boost": 1
          }
        }
      ],
      "adjust_pure_negative": true,
      "boost": 1
    }
  },
  "sort": [
    {
      "name_length": {
        "order": "asc"
      }
    },
    {
      "id": {
        "order": "asc"
      }
    }
  ]
}

其中match可以用matchPhrase,但是效果没有match好,或者使用wildcardQuery这种通配符查询的方式也可以。

PS:
如果想看某个字段分词情况,可以用下面的请求:
GET https://{ip}:{port}/index/_doc/{id}/_termvectors?fields={field}

一条小咸鱼