我花了20个小时学习全文检索技术
根据Josh Kaufman先生的说法,如果正确学习,似乎可以在20小时内掌握新的技能。这次我参考了他的学习方法,尝试用20小时学习“全文搜索”这个技能,所以我想总结一下学习结果。请注意,本文更注重于学习笔记的方面。
首先
学习前的知识和技能 de yǔ
听说可以在Elasticsearch这个搜索引擎中实现全文搜索。因为有提供Docker镜像,所以我一直很感兴趣。
听说还有其他可以实现全文搜索的开源软件,但是我不知道具体的名称。(后来发现是Apache Solr)
据说这些软件在读取方面比MySQL等数据库要优秀。然而,据说写入方面不如MySQL等写入型数据库,因此似乎需要以与MySQL等写入型数据存储同步的方式来使用它们。
据说在MySQL等数据库中也可以实现类似的全文搜索,但我不太了解具体情况。
我从一开始就没有正确理解全文检索的机制。
学习目标
在学习全文搜索时,我们将技能分为知识和技术,并制定了以下学习目标。
-
- 全文検索について理解する
-
- Elasticsearch, Apache Solrのドキュメントを読んで概要を理解する
-
- ElasticsearchとApache Solrの比較ができる
-
- データベース検索との比較ができる
- 簡単なサンプルプロジェクトで動作させることができる
全文検索是指的是一个功能
全文搜索是一种技术,可以从多个文件和大量文章中高效地搜索与特定关键词相关的内容。
全文検索技術可以主要分为grep型和索引(索引)型两种,它们各自具有不同的特点。
本次使用的Elasticsearch和Apache Solr都是索引型搜索引擎,与grep型相比,它们可以实现对大量数据的高速搜索。相反,由于需要管理索引,因此在添加或更新数据时需要注意。
在创建索引的常见方法中,有形态分析和n-gram两种方法。
形态分析是一种将文本分割成有意义的最小单元的技术。据说它在日语全文搜索中被广泛使用。
n-gram相反,它以字符为单位进行分割。尽管可以实现自动完成功能,但由于以字符为单位分割的特性,会导致搜索噪声增加和索引大小膨胀的缺点。
无论是哪种方法,都各有优点和缺点,但也存在一种被称为混合方法的手法,它结合了形态分析和n-gram分析。然而,这种方法的一个缺点是由于结合使用而导致了较慢的运行速度。根据需求,需要适当选择使用不同的方法。
Elasticsearch是什么?
“Elasticsearch(Elasticsearch)是一款基于Apache Lucene开发的分析和搜索引擎。之前采用了Apache 2.0许可证,但现在已转为SSPL和Elastic License双重许可证。无论哪个许可证,都可以免费使用Elasticsearch的功能。同时也提供有偿订阅,购买后可以在免费功能的基础上享受更强大的安全支持和机器学习功能等。”
Elasticsearch支持多种编程语言,并提供官方客户端。例如,可以通过JavaScript中的@elastic/elasticsearch包来安装,可以使用npm或yarn来安装。您可以从以下文档中访问各编程语言的客户端文档。
Apache Solr是什么?
Apache Solr(索尔)是一个基于Apache Lucene开发的企业级搜索平台,类似于Elasticsearch。它是在Apache许可证下构建的。
Solr具有REST风格的API,还具备自动完成和拼写检查等高级功能。此外,它还捆绑了丰富的插件,可以进行针对PDF和Word等富内容的搜索。
我试着寻找Solr的官方客户端,但没有找到。我在JavaScript中找到了一个名为solr-client的包,但它似乎还没有完全适应最新版本的9,可能会有意想不到的行为。
样例代码
这次我们使用Docker搭建了Elasticsearch(以及Kibana)、Apache Solr和MySQL环境,并使用ts-node执行了示例代码。
这是我们这次使用的docker-compose.yml文件和为Elasticsearch准备的Dockerfile。
version: '3'
services:
# initializer
initializer:
image: alpine
container_name: solr-initializer
restart: "no"
entrypoint: |
/bin/sh -c "chown 8983:8983 /solr"
volumes:
- ./services/solr:/solr
# solr
solr:
depends_on:
- initializer
image: solr:9.2.0
ports:
- 8983:8983
volumes:
- ./services/solr:/var/solr
command:
- solr-precreate
- posts
# elastic
elastic:
build: ./services/elastic
ports:
- 9200:9200
environment:
- discovery.type=single-node
- xpack.security.enabled=false
- ES_JAVA_OPTS=-Xms400m -Xmx400m
ulimits:
memlock:
soft: -1
hard: -1
# kibana
kibana:
image: docker.elastic.co/kibana/kibana:8.7.0
ports:
- 5601:5601
environment:
- ELASTICSEARCH_HOSTS=http://elastic:9200
depends_on:
- elastic
# mysql
mysql:
image: mysql:8.0-debian
ports:
- 3306:3306
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: my_database
MYSQL_USER: user
MYSQL_PASSWORD: password
TZ: "Asia/Tokyo"
volumes:
- ./services/mysql/data:/var/lib/mysql
- ./services/mysql/sql
FROM docker.elastic.co/elasticsearch/elasticsearch:8.7.0
RUN elasticsearch-plugin install analysis-kuromoji
并且这次,我准备了以下作为全文搜索的字符串。坦白说,数量相当少呢…
[
"今日は何もないすばらしい一日でした",
"隣の客はよく柿食う客だ",
"ふとんが吹っ飛んだ",
"HTML/CSSは楽しい",
"これはテスト投稿です。キャプションはありません",
"今日はテストがあります"
]
在Elasticsearch中进行全文搜索。
对Elasticsearch的访问是通过上述的@elastic/elasticsearch进行的。
import { Client } from "@elastic/elasticsearch";
(async () => {
const client = new Client({
node: process.env.ELASTICSEARCH_URL || 'http://localhost:9200',
});
const index = "posts";
// インデックスが存在するか確認
const isExists = await client.indices.exists({
index
});
if (!isExists) {
// インデックスの作成
await client.indices.create({
index,
mappings: {
properties: {
id: { type: 'keyword' },
caption: { type: 'text' },
createdAt: { type: 'date' }
}
}
});
}
// 検索の実行
const result = client.search({
index,
body: {
min_score: 0.5,
query: {
match: {
caption: "今日は"
}
},
},
});
console.log(result); // [今日はテストがあります, 今日は何もないすばらしい一日でした]
})();
在以下部分,我们进行了索引的存在检查,如果索引不存在,则创建新的索引。需要注意的是,Elasticsearch支持无模式(schemaless),因此即使没有创建索引也可以进行搜索。
// インデックスが存在するか確認
const isExists = await client.indices.exists({
index
});
if (!isExists) {
// インデックスの作成
await client.indices.create({
index,
mappings: {
properties: {
id: { type: 'keyword' },
caption: { type: 'text' },
createdAt: { type: 'date' }
}
}
});
}
在下面的部分,我正在进行文档的搜索。
我已将body.min_score指定为0.5,这是用来设置获取最低匹配度的选项。如果不指定,将会连同包含“○○は”的句子一起获取。
// 検索の実行
const result = client.search({
index,
body: {
min_score: 0.5,
query: {
match: {
caption: "今日は"
}
},
},
});
最后,我在Kibana上尝试进行了相同的搜索。

我成功地获得了所期待的搜索结果。
通过Apache Solr进行全文搜索。
因找不到Apache Solr官方客户端包,所以我使用了fetch()。
在docker-compose.yml文件的pre-create命令中,创建了一个名为”posts”的核心,所以我会在这个核心上进行搜索。
(async () => {
const url = process.env.SOLR_URL || "http://localhost:8983";
const response = await fetch(url + "/solr/posts/select?q=caption:今日は");
const result = await response.json();
console.log(result); // [今日はテストがあります, 今日は何もないすばらしい一日でした, これはテスト投稿です。キャプションはありません, HTML/CSSは楽しい, 隣の客はよく柿食う客だ]
})();
可以使用url/solr/核心名称/select来进行文档搜索。
因为不知道对应于Elasticsearch的min_score选项,所以还会获取到包含”○○”的句子。
最后,我在 Solr 管理界面上尝试进行了相同的搜索。

我很幸运地成功获取了我所期望的搜索结果。
在MySQL中进行全文搜索
以下是我們用於這個樣本的”posts”資料表的資料表定義。
CREATE TABLE posts (
id CHAR(36) NOT NULL,
caption TEXT NOT NULL,
created_at TIMESTAMP NOT NULL,
PRIMARY KEY (id),
FULLTEXT KEY `FT_CAPTION` (`caption`) WITH PARSER ngram
) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
我在标题列上创建了全文搜索索引。同时,我指定了可以处理日语的ngram解析器。这个ngram在最新的MySQL版本中可以直接使用,无需额外安装。
对上述表格进行全文检索。
在MySQL中,可以使用MATCH () … AGAINST语法进行全文检索。
本次执行的SQL语句如下。
SELECT * FROM posts WHERE MATCH (caption) AGAINST ('今日は' IN NATURAL LANGUAGE MODE);
在MATCH()部分指定了具有全文索引的标题列。AGAINST中指定了要搜索的字符串。
尝试真实操作一下
Elasticsearch和Kibana的内存使用量相当大,如果不在docker-compose.yml中添加与内存有关的配置,将会出现错误并无法运行。
而Apache Solr则没有出现这样的问题。
考虑到有丰富的文件内容、提供了官方客户端软件等,个人而言,我可能更喜欢Elasticsearch。
如果没有内存上的顾虑,我希望能将其与Apache Solr这样的工具配合使用。
关于与数据库的全文搜索进行比较,由于无法进行性能比较,所以无法很清楚地看出差异。也许增加数据量可以使其变得更明显。
总结
我花了20个小时学习全文搜索,并总结了学习结果。
通过了解全文搜索的概要和Elasticsearch等知识,并实际运用的经验,我积累了很好的经历。
这次我犯了一个错误,就是同时着手开始使用Elasticsearch和Apache Solr这两个搜索引擎。20小时的时间比我想象中的要短,我应该集中精力在其中一个上进行,并将另一个作为参考比较。
在下一次的20小时学习中,我计划选择对我感兴趣的技能,如Docker、Kubernetes、CI/CD工具,或者巩固基础技能,如网络和数据库。我会利用这次的反省经验来更好地学习。