全文检索引擎-ManticoreSearch

  • 记于:2025-09-28 下午
  • 地点:浙江省·温州市·家里
  • 天气:晴天

背景#

因个人开源的[商城项目]需要用到全文检索功能,主流的Elasticsearch太占资源,经过搜索比对,选择了轻量级的ManticoreSearch作为全文检索引擎。

简单介绍 (by AI)#

ManticoreSearch是一个开源、高性能的全文检索引擎,源自Sphinx Search。它能够帮助开发者在海量文本数据中快速查找相关内容,同时支持实时索引和SQL查询,让数据写入后即可被立即搜索。

特点包括:

  • 支持中文、英文等多语言全文搜索
  • 高并发、低延迟,适合网站、日志和内容平台
  • 使用MySQL协议,学习成本低、集成简单
  • 可进行布尔搜索、短语搜索和相关性排序

简单来说,ManticoreSearch就像一个专门为文本搜索优化的数据库,可以让你的应用或网站实现快速、精准的搜索功能。

安装#

支持多种方式安装,见官方文档[安装]部分。

MacOS下安装:

1
% brew install manticoresoftware/tap/manticoresearch manticoresoftware/tap/manticore-extra

以brew服务的方式启动Manticore:

1
% brew services start manticoresearch

支持MySQL客户端连接:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
% mysql -h0 -P9306
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 146
Server version: 0.0.0 4734be156a@250610 from tarball

Copyright (c) 2000, 2023, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| Manticore |
+--------------------+
2 rows in set (0.00 sec)

mysql> show tables;
+---------+------+
| Table | Type |
+---------+------+
| product | rt |
+---------+------+
1 row in set (0.00 sec)

Manticore Search使用MySQL协议实现了一个SQL接口,允许任何MySQL库或连接器以及许多MySQL客户端连接到ManticoreSearch,并像对待MySQL服务器一样使用它,而不是Manticore。

然而,SQL方言不同,并且只实现了MySQL中可用的SQL命令或函数的子集。此外,还有专门针对ManticoreSearch的子句和函数,例如用于全文搜索的MATCH()子句。

Manticore Search不支持服务器端预处理语句,但可以使用客户端预处理语句。需要注意的是,Manticore实现了多值(MVA)数据类型,这在MySQL或实现预处理语句的库中没有等价项。在这些情况下,MVA值必须在原始查询中构造。

某些MySQL客户端/连接器需要用户/密码和/或数据库名称的值。由于Manticore Search没有数据库的概念,也没有实现用户访问控制,这些值可以随意设置,因为Manticore会简单地忽略它们。

具体见官方文档[连接服务器]部分。

集成#

ManticoreSearch支持多种编程语言的客户端连接,同时也支持Java客户端连接。
客户端Github地址:https://github.com/manticoresoftware/manticoresearch-java

但是由于我的项目使用的还是Java8,而该客户端的近几个版本已经升级了Java版本,(忘记是11还是17了);
我fork了官方该仓库,改回了Java8,并发布到了maven中央仓库;
Github地址:https://github.com/yeshimin/manticoresearch-java-8
Maven地址:https://mvnrepository.com/artifact/com.yeshimin/manticoresearch-java-8

只需要使用这个非官方的maven依赖,其他使用方式和官方的一样,无需任何改动。
版本使用8.1.1,对应官方的8.1.0。

1
2
3
4
5
6
<!-- manticoresearch 全文检索 -->
<dependency>
<groupId>com.yeshimin</groupId>
<artifactId>manticoresearch-java-8</artifactId>
<version>8.1.1</version>
</dependency>

使用示例#

表示例#

1
2
3
4
5
6
7
8
9
CREATE TABLE product (
id bigint,
product_name text,
sku_names text,
sku_specs text,
sales bigint,
sku_min_price float,
sku_max_price float
) charset_table='non_cjk' ngram_len='1' ngram_chars='cjk' morphology='stem_en' ;

配置解释:

  • charset_table=’non_cjk’:表示该表的字符集为非中文字符集,适用于英文等语言的全文检索。
  • ngram_len=’1’:表示使用1-gram分词方式,即将文本拆分为单个字符进行索引和搜索。
  • ngram_chars=’cjk’:表示对中文、日文、韩文字符进行n-gram分词处理。
  • morphology=’stem_en’:表示对英文单词进行词干提取处理,以提高搜索的匹配度。

这样配置可以支持中英文的全文检索。

代码示例#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
package com.yeshimin.yeahboot.merchant.service;

import cn.hutool.core.util.StrUtil;
import com.manticoresearch.client.ApiClient;
import com.manticoresearch.client.ApiException;
import com.manticoresearch.client.api.IndexApi;
import com.manticoresearch.client.api.SearchApi;
import com.manticoresearch.client.model.*;
import com.yeshimin.yeahboot.common.common.consts.FulltextTableConsts;
import com.yeshimin.yeahboot.common.common.properties.ManticoreSearchProperties;
import com.yeshimin.yeahboot.data.domain.entity.ProductSpuEntity;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Slf4j
@Service
@RequiredArgsConstructor
public class FullTextSearchService {

private final ManticoreSearchProperties properties;

/**
* 同步商品索引
*/
public void syncProduct(ProductSpuEntity productSpu, List<String> skuNames, List<String> skuSpecs, boolean update) {
Map<String, Object> doc = new HashMap<>();
doc.put("id", productSpu.getId());
doc.put("product_name", productSpu.getName());
doc.put("sku_names", StrUtil.join(" ", skuNames));
doc.put("sku_specs", StrUtil.join(" ", skuSpecs));
doc.put("sales", productSpu.getSales());
doc.put("sku_min_price", productSpu.getMinPrice());
doc.put("sku_max_price", productSpu.getMaxPrice());

if (update) {
this.replace(FulltextTableConsts.PRODUCT, doc);
} else {
this.insert(FulltextTableConsts.PRODUCT, doc);
}
}

// ================================================================================

/**
* search
*/
public SearchResponse search(String tableName, String queryString) {
ApiClient client = this.newClient();

SearchQuery searchQuery = new SearchQuery();
searchQuery.setQueryString(queryString);

SearchRequest searchRequest = new SearchRequest();
searchRequest.table(tableName).query(searchQuery);

SearchApi searchApi = new SearchApi(client);

try {
SearchResponse response = searchApi.search(searchRequest);
log.debug("Search response: {}", response);
return response;
} catch (ApiException e) {
this.printError(e);
throw new RuntimeException(e);
}
}

/**
* insert
*/
public SuccessResponse insert(String tableName, Map<String, Object> doc) {
ApiClient client = this.newClient();

Long id = Long.parseLong(doc.remove("id").toString());

InsertDocumentRequest request = new InsertDocumentRequest();
request.table(tableName).id(id).doc(doc);

IndexApi indexApi = new IndexApi(client);
try {
SuccessResponse response = indexApi.insert(request);
log.debug("Insert response: {}", response);
return response;
} catch (ApiException e) {
this.printError(e);
throw new RuntimeException(e);
}
}

/**
* replace
*/
public SuccessResponse replace(String tableName, Map<String, Object> doc) {
ApiClient client = this.newClient();

Long id = Long.parseLong(doc.remove("id").toString());

InsertDocumentRequest request = new InsertDocumentRequest();
request.table(tableName).id(id).doc(doc);

IndexApi indexApi = new IndexApi(client);
try {
SuccessResponse response = indexApi.replace(request);
log.debug("Replace response: {}", response);
return response;
} catch (ApiException e) {
this.printError(e);
throw new RuntimeException(e);
}
}

/**
* update
*/
public UpdateResponse update(String tableName, Map<String, Object> doc) {
ApiClient client = this.newClient();

Long id = Long.parseLong(doc.remove("id").toString());

UpdateDocumentRequest updateRequest = new UpdateDocumentRequest();
updateRequest.table(tableName).id(id).doc(doc);

IndexApi indexApi = new IndexApi(client);
try {
UpdateResponse response = indexApi.update(updateRequest);
log.debug("Update response: {}", response);
return response;
} catch (ApiException e) {
this.printError(e);
throw new RuntimeException(e);
}
}

/**
* deleteIndex
*/
public DeleteResponse deleteIndex(String tableName, Long id) {
ApiClient client = this.newClient();

DeleteDocumentRequest deleteRequest = new DeleteDocumentRequest();
deleteRequest.table(tableName).id(id);

IndexApi indexApi = new IndexApi(client);
try {
DeleteResponse response = indexApi.delete(deleteRequest);
log.debug("Delete response: {}", response);
return response;
} catch (ApiException e) {
this.printError(e);
throw new RuntimeException(e);
}
}

// ================================================================================

private ApiClient newClient() {
ApiClient client = new ApiClient();
client.setBasePath(properties.getBasePath());
return client;
}

private void printError(ApiException e) {
log.error("Exception when calling Api function: {}, Status code: {}, Reason: {}, Response headers: {}",
e.getMessage(), e.getCode(), e.getResponseBody(), e.getResponseHeaders());
}
}

官方建议:Java客户端SDK未实现线程安全,建议每次请求都创建一个新的ApiClient实例。

参考资料#