用Typesense和Spring Boot構(gòu)建快速通用搜索引擎
在今天的数字世界中,用户期望快速而准确的搜索体验。无论是在线商店中查找产品,还是在新闻网站上浏览文章,搜索引擎都是实现良好用户体验的基础。但是从零开始构建高性能搜索引擎可能会很复杂。幸运的是,借助工具如Typesense和Spring Boot,创建一个强大、可扩展且高度可配置的搜索引擎是完全可能的。
背景:传统数据库搜索及其不足在像 Typesense 这样的搜索引擎变得普及之前,开发人员高度依赖数据库来实现搜索功能。让我们来看看传统在应用程序内实现搜索的方法以及它们的局限性:
- 基本的数据库查询
搜索数据库最简单的方式是使用带有LIKE
操作符的标准 SQL 查询。例如,要通过名称查找产品,你可以使用如下 SQL 查询:SELECT * FROM products WHERE name LIKE '产品名称'
。
SELECT * FROM products WHERE name LIKE '%keyword%';
虽然这种方法简单,在处理大型数据集时也存在一些局限性。在执行带有通配符的 LIKE
查询时无法有效利用索引,因此速度会变得较慢。此外,这种方法在灵活性方面较差,只能进行精确匹配或少量模式匹配,无法获得更细致的结果,比如相似词或容错拼写错误。
-
存储过程和自定义函数
存储过程允许进行更复杂的搜索,例如结合多个列或添加基本排名。一些开发人员创建自定义函数,根据精确匹配、部分匹配或特定权重字段来对结果进行相关性排序。然而,这种方法需要深入了解数据库和SQL知识,并且扩展具有挑战性。存储过程通常不是为现代应用程序中的复杂搜索需求场景设计的,因此很难有效地实现诸如模糊匹配或拼写纠正等功能。 - 模糊搜索实现
在某些情况下,数据库提供了模糊搜索功能或扩展,允许更宽松的匹配。例如,PostgreSQL有pg_trgm
扩展,支持基于相似性的搜索。这可以改善用户体验,允许搜索词存在小的不匹配,这对用户打字错误时很有用。然而,虽然模糊搜索提高了准确性,但这并不能解决所有的性能挑战。使用模糊匹配搜索大型数据集仍然会变得缓慢且资源消耗大。此外,这种方法无法提供更高级的功能,例如同义词识别、分面搜索或个性化排名。
- 性能瓶颈问题:数据库是为存储和检索而优化的,而不是为了处理复杂查询的高速搜索。随着数据的增长,查询响应时间会显著延长,导致用户体验不佳。
- 有限的搜索功能:数据库本身无法处理复杂的搜索功能,例如拼写纠错、词形还原、同义词支持或个性化排序。要添加这些功能需要额外的工作和自定义编码,这些通常性能不佳且难以扩展。
- 高查询量难以扩展:数据库服务器不是为处理高查询量而设计的,这种高查询量通常由专用搜索引擎处理。将数据库服务器扩展以适应高强度的搜索负载可能会成本高昂且复杂,维护索引和存储过程的需求也可能会变得负担沉重。
- 复杂开发和维护:使用存储过程或自定义SQL函数实现搜索功能会增加代码库的维护和调试难度。开发人员还需要编写复杂的查询来处理基本的搜索功能,这些解决方案通常难以修改或随时间推移扩展。
这些缺点清楚地表明了为什么像 Typesense 这样的专用搜索引擎成为了首选。Typesense 和类似的工具专门用于搜索,配备了优化的算法、更快的响应时间和丰富的功能,这些功能很难仅通过数据库查询来实现。
TypeSense到底是什么?Typesense 是一个开源的内存搜索引擎,速度快且高效。它提供了简单易用的 API 和众多强大的功能,是想要在应用程序中添加搜索功能的开发者的一个很好的选择。
Typesense的关键特性- 超快速响应:Typesense 设计用于毫秒级响应时间。
- 拼写纠错:自动修正用户搜索查询中的拼写错误。
- 分面搜索:允许用户根据各种属性筛选搜索结果。
- API 简洁性:Typesense 具有简洁易用的 RESTful API,使得集成非常简单。
Typesense非常适合需要实时的、用户友好的搜索功能的应用程序。例如:在线购物网站、社交媒体平台等:
- 包括例如需要产品搜索和筛选功能的电子商务店铺
- 包括知识库或文档存储库
- 包括以搜索功能为主的SaaS产品
Spring Boot 提供了一个强大的框架,用于构建可扩展且易于维护的 Java 网站应用。当与 Typesense 结合使用时,Spring Boot 可以简化创建强大搜索引擎的过程,使您能够:
- 轻松管理依赖和配置,通过Spring生态系统
- 快速构建RESTful API以处理搜索请求
- 高效扩展,利用Spring Boot的生产就绪特性
在开始之前,请确保你准备好了以下事项。
- Java开发工具包 (JDK) — Java 11 或更高版本
- Maven 或,Gradle — 用于管理依赖项
- Typesense 服务 — 可以是托管的或自行托管的(下方有安装说明)
- Spring Boot 框架 — 用于构建应用逻辑和 API 层
- Docker 和 Docker Compose — 自行托管(下方有安装说明)
设置 Typesense
要在本地机器上运行 Typesense,您可以手动下载最新版本的 Typesense,或者使用 Docker 来安装它,这对许多开发人员来说更简单。以下是使用 Docker Compose 进行设置的:
Docker Compose 设置
使用 Docker Compose 可以简化设置过程,让你轻松地启动、停止和配置 Typesense 服务。在你的 docker-compose.yml 文件中添加以下服务:
services:
typesense:
image: typesense/typesense:0.27.1
restart: on-failure
ports:
- "8108:8108"
volumes:
- ./typesense-data:/data
command: '--data-dir /data --api-key=xyz --enable-cors' # 数据目录设置和API密钥配置
一旦添加了此配置,就可以通过运行命令来运行Typesense容器。
docker-compose up -d
运行并后台启动 Docker 容器(使用 docker-compose up -d
命令)。
此命令将下载 Typesense 镜像(如果还未下载),创建容器并在后台运行它。
配置 Typesense 以优化性能要想充分利用 Typesense 的功能,你可以参考以下配置设置。
- 设置 API 密钥:通过要求所有请求都必须提供 API 密钥来保护您的 Typesense 实例。请将
--api-key=xyz
选项中的xyz
替换为一个强大且唯一的密钥。 - 启用高可用性:对于生产环境,请考虑将 Typesense 设置为集群模式运行。可以通过配置让 Typesense 在多个节点上运行,以防止单点故障并分配搜索负载。
- 调整内存限制:根据数据集的大小,确保为 Typesense 容器分配足够的内存。您可以使用 Docker 来管理内存资源并监控其使用情况,以防止在数据量增加时性能下降。
有了这样的设置,你就可以用于本地开发的Typesense实例,高效地构建和测试搜索功能。
启动一个Spring Boot项目- 初始化一个新的Spring Boot项目,创建一个新的Spring Boot项目,该项目需要包含用于开发REST API的依赖项。
- 添加Typesense依赖项,比如在你的
pom.xml
或build.gradle
文件中添加依赖项,比如TypesenseClient
。
在你的 application.properties
文件里添加以下配置内容:
## Typesense 配置
typesense.apiKey=your_api_key(xyz) # 考虑更好的处理方式
typesense.host=localhost # 本地主机
typesense.port=8108
typesense.protocol=http
typesense.connection-timeout-seconds=30 # 连接超时秒数
这确保了Spring Boot能找到并访问Typesense实例并能访问它。
连接Typesense创建一个Config类来初始化TypeSense客户端:
#请自行添加lombok等其他依赖
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.typesense.api.Client;
import org.typesense.api.Configuration;
import org.typesense.resources.Node;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
@Slf4j
@org.springframework.context.annotation.Configuration
public class TypesenseConfig {
@Value("${typesense.api-key}")
private String apiKey;
@Value("${typesense.host}")
private String host;
@Value("${typesense.port}")
private String port;
@Value("${typesense.protocol}")
private String protocol;
@Value("${typesense.connection-timeout-seconds}")
private int connectionTimeoutSeconds;
@Bean
public Client typesenseClient() {
List<Node> nodes = new ArrayList<>();
nodes.add(new Node(protocol, host, port));
Configuration configuration = new Configuration(
nodes,
Duration.ofSeconds(connectionTimeoutSeconds),
apiKey
);
Client client = new Client(configuration);
try {
client.health.retrieve();
} catch (Exception e) {
log.error("无法连接到Typesense服务器", e);
}
return client;
}
}
建立数据模型
定义您希望可以搜索的实体。例如,如果您正在构建产品搜索,可以创建一个名为 Product
的实体。每个实体都将对应一个 Typesense 集合。
@Entity
public class 商品 {
private Long id;
private String name;
private String description;
private Double price;
}
public interface 基础模型 {
String getModelId();
String getSearchableContent();
}
实现通用搜索功能的实现
创建一个通用搜索服务
创建一个使用 Typesense 的服务,利用其灵活的通用搜索功能,使得你可以轻松处理多个模型。
#你可以不断改进代码实现
@Slf4j
@Service
public class GenericSearchService<T extends BaseModel> {
public static final String CONTENT = "content";
private final Client typesenseClient;
public GenericSearchService(Client typesenseClient) {
this.typesenseClient = typesenseClient;
}
public void indexDocuments(String collectionName, List<T> documents) {
try {
ensureCollectionExists(collectionName);
final List<Map<String, Object>> documentsToIndex = documentsToExtract(documents);
indexDocumentsInternal(collectionName, documents, documentsToIndex);
} catch (Exception e) {
log.error("在索引过程中遇到错误,集合名称:{},错误详情:{}",
collectionName, e.getMessage(), e);
}
}
private void indexDocumentsInternal(String collectionName, List<T> documents, List<Map<String, Object>> documentsToIndex) {
int successCount = 0;
for (Map<String, Object> document : documentsToIndex)
try {
typesenseClient.collections(collectionName)
.documents()
.create(document);
successCount++;
} catch (Exception e) {
log.error("索引文档时发生错误,文档ID:{},错误详情:{}", document.get("id"), e.getMessage());
}
log.info("成功索引集合 {} 中的 {}/{} 份文档",
collectionName, successCount, documents.size());
}
private static <T extends BaseModel> @NotNull List<Map<String, Object>> documentsToExtract(List<T> documents) {
List<Map<String, Object>> documentsToIndex = new ArrayList<>();
for (T document : documents) {
Map<String, Object> map = new HashMap<>();
map.put("id", document.getModelId());
map.put(CONTENT, document.getSearchableContent());
documentsToIndex.add(map);
}
return documentsToIndex;
}
private void deleteCollection(String collectionName) {
try {
typesenseClient.collections(collectionName).delete();
log.info("已删除集合:{}", collectionName);
} catch (Exception e) {
log.debug("集合 {} 未找到", collectionName);
}
}
public List<Map<String, Object>> search(String query, String collectionName) {
try {
if (query == null || query.trim().isEmpty()) {
log.warn("收到空的搜索查询");
return List.of();
}
SearchParameters searchParameters = new SearchParameters()
.q(query)
.queryBy(CONTENT)
.perPage(100); // 请根据需要调整此值
return typesenseClient.collections(collectionName)
.documents()
.search(searchParameters)
.getHits()
.stream()
.map(SearchResultHit::getDocument)
.toList();
} catch (Exception e) {
log.error("在集合 {} 中搜索时遇到错误,错误信息:{}",
collectionName, e.getMessage(), e);
return List.of();
}
}
private void createCollection(String collectionName) {
try {
List<Field> fields = new ArrayList<>();
fields.add(new Field()
.name("id")
.type(FieldTypes.STRING));
fields.add(new Field()
.name(CONTENT)
.type(FieldTypes.STRING));
CollectionSchema schema = new CollectionSchema()
.name(collectionName)
.fields(fields);
typesenseClient.collections().create(schema);
log.info("建立了新的集合:{}", collectionName);
} catch (Exception e) {
log.error("建立集合 {} 时出现问题,错误信息:{}",
collectionName, e.getMessage(), e);
}
}
private boolean collectionExists(String collectionName) {
try {
typesenseClient.collections(collectionName).retrieve();
return true;
} catch (Exception e) {
return false;
}
}
private void ensureCollectionExists(String collectionName) {
if (!collectionExists(collectionName)) {
createCollection(collectionName);
} else {
log.debug("集合 {} 已经存在", collectionName);
}
}
}
实现带有@Autowired注解的ProductService
,其中使用了GenericSearchService
现在,我们来创建 ProductService
类,自动装配 GenericSearchService
,并添加用于索引以及搜索产品的方法。注意:你也可以为其他模型,例如用户等,做类似的操作。
@Service
@Slf4j
/** 商品服务类 */
public class ProductService {
private final GenericSearchService<Product> searchService;
private final ProductRepository productRepository;
@Autowired
public ProductService(GenericSearchService<Product> searchService, ProductRepository productRepository) {
this.searchService = searchService;
this.productRepository = productRepository;
}
@Transactional
/** 搜索商品方法 */
public List<Product> searchProducts(String searchTerm) {
try {
List<Map<String, Object>> searchedContent = searchProductsInternal(searchTerm);
if (searchedContent.isEmpty()) {
return Collections.emptyList();
}
List<Long> productIds = searchedContent.stream()
.map(doc -> doc.get("id"))
.filter(Objects::nonNull)
.map(id -> {
try {
return Long.parseLong(id.toString());
} catch (NumberFormatException e) {
log.error("无效的ID格式: " + id);
return null;
}
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
return productIds.isEmpty() ? Collections.emptyList() : productRepository.findAllById(productIds);
} catch (Exception e) {
log.error("搜索商品时发生错误: " + e.getMessage());
return Collections.emptyList();
}
}
/** 索引商品方法 */
public void indexProducts(List<Product> products) {
searchService.indexDocuments("products_collection", products);
}
/** 内部搜索商品方法 */
private List<Map<String, Object>> searchProductsInternal(String searchTerm) {
if (searchTerm == null || searchTerm.trim().isEmpty()) {
return Collections.emptyList();
}
return searchService.search(searchTerm, "products_collection");
}
}
新建 ProductController
这个控制器将包含一个端点来根据查询词搜索产品,通过 ProductService 进行查询。
标记为@RestController的注解表示这个类是一个REST控制器。产品控制器(ProductController)是一个处理产品相关请求的控制器类。它依赖于产品服务(ProductService),该服务用于执行产品的具体业务逻辑。
@RestController
@RequestMapping("/api/products")
public class 产品控制器 {
private final 产品服务 productService;
@Autowired
public 产品控制器(产品服务 productService) {
this.productService = productService;
}
@GetMapping("/search")
public ResponseEntity<?> 搜索产品(@RequestParam String 查询参数) {
List<Product> products = productService.搜索产品(查询参数);
如果 (products.isEmpty()) {
如果产品列表为空,返回一个无内容的响应。
}
否则,返回一个包含产品列表的响应。
}
}
如何通过认证来确保搜索API的安全
利用Spring Security来给您的搜索API添加认证和授权功能,从而确保安全访问搜索API。
对搜索引擎进行测试和调试功能 单元测试和集成测试为各个组件编写单元测试,并编写集成测试以确保 Typesense 的连接和搜索功能能够正常工作。
Typesense 和 Spring Boot 调试小贴士如果遇到问题,请检查 API 密钥、连接 URL 和查询语法。Typesense 的 API 文档和日志文件对于故障排除很有帮助。
性能优化 缓存技巧让响应更快实现缓存机制可以减少Typesense处理频繁访问查询的负载,从而使您的搜索引擎更快。
优化 Typesense 中的索引和查询的性能(在 Typesense 中)通过调整索引参数并按需开启分片技术,优化您的 Typesense 实例环境的性能,以应对高查询量和大规模数据集。
因此,结论通过将Typesense的速度和灵活性与Spring Boot的强大功能相结合,你可以创建一个强大的搜索引擎,符合你应用程序的需求。不论是电商、知识管理还是SaaS应用,这种方案都为你提供了一个既可扩展又高效的搜索体验。
共同學(xué)習(xí),寫下你的評(píng)論
評(píng)論加載中...
作者其他優(yōu)質(zhì)文章