
在当今信息爆炸的时代,用户对搜索体验的要求越来越高。从电商平台的商品搜索到内容管理系统的文档检索,高效、精准的搜索功能已成为企业核心竞争力之一。Apache Solr 作为一款成熟的开源搜索平台,凭借其强大的全文检索能力、分布式架构支持和丰富的功能特性,被 Netflix、eBay、Instagram 等众多巨头企业广泛采用。
本文将以实战为导向,从环境搭建到分布式部署,从基础查询到性能优化,全方位剖析 Solr 的核心技术与最佳实践。无论你是刚接触搜索引擎的新手,还是需要解决实际问题的开发工程师,都能从中获得可直接应用于生产环境的知识和代码。
Apache Solr 是一个基于 Lucene 的开源企业级搜索平台,它提供了全文检索、高亮显示、分面搜索、动态聚类、数据库集成等功能,支持分布式部署,具有高可靠性和可扩展性。
很多人会混淆 Solr 和 Lucene 的关系,简单来说:

Solr 的核心组件包括:

倒排索引是 Solr 实现高效搜索的核心数据结构,它将文档中的词语映射到包含该词语的文档列表。
举个例子,有以下文档:
倒排索引会存储:
这种结构使得 Solr 能在毫秒级时间内找到包含特定词语的所有文档。
在开始之前,确保你的系统满足以下要求:
我们将安装最新稳定版 Solr 9.4.0:
# 下载Solr
wget https://dlcdn.apache.org/solr/solr/9.4.0/solr-9.4.0.tgz
# 解压
tar -zxvf solr-9.4.0.tgz
# 进入Solr目录
cd solr-9.4.0
# 启动Solr(单机模式)
bin/solr start -p 8983 -force
# 验证启动是否成功
bin/solr status
成功启动后,访问 http://localhost:8983 即可看到 Solr 的管理界面。
Core 是 Solr 存储索引和配置的基本单元,创建一个名为 "product" 的 core 用于电商商品搜索:
# 创建core
bin/solr create -c product -force
# 查看已创建的core
bin/solr list -c
了解 Solr 的目录结构有助于更好地配置和管理 Solr:
solr-9.4.0/
├── bin/ # 可执行脚本
├── conf/ # 全局配置文件
├── contrib/ # 扩展模块
├── dist/ # 编译后的jar包
├── docs/ # 文档
├── example/ # 示例
├── server/ # 服务器相关文件
│ ├── solr/ # Solr主目录
│ │ ├── configsets/ # 配置集
│ │ └── product/ # 我们创建的product core
│ │ ├── conf/ # core配置文件
│ │ └── data/ # 索引数据
│ └── webapps/ # Web应用
└── licenses/ # 许可证
schema 定义了文档的结构,Solr 9 默认使用 managed-schema(可通过 API 动态修改),位于server/solr/product/conf/managed-schema。
我们为电商商品搜索定义以下字段:
<!-- 商品ID -->
<field name="id" type="string" indexed="true" stored="true" required="true" multiValued="false" />
<!-- 商品名称 -->
<field name="product_name" type="text_ik" indexed="true" stored="true" multiValued="false" />
<!-- 商品分类 -->
<field name="category" type="string" indexed="true" stored="true" multiValued="false" />
<!-- 商品价格 -->
<field name="price" type="pdouble" indexed="true" stored="true" multiValued="false" />
<!-- 商品描述 -->
<field name="description" type="text_ik" indexed="true" stored="true" multiValued="false" />
<!-- 商品标签 -->
<field name="tags" type="string" indexed="true" stored="true" multiValued="true" />
<!-- 商品图片URL -->
<field name="image_url" type="string" indexed="false" stored="true" multiValued="false" />
<!-- 商品库存 -->
<field name="stock" type="pint" indexed="true" stored="true" multiValued="false" />
<!-- 商品上架时间 -->
<field name="create_time" type="pdate" indexed="true" stored="true" multiValued="false" />
<!-- 商品销量 -->
<field name="sales" type="pint" indexed="true" stored="true" multiValued="false" />
<!-- 复制字段,用于跨字段搜索 -->
<field name="product_keywords" type="text_ik" indexed="true" stored="false" multiValued="true" />
<copyField source="product_name" dest="product_keywords"/>
<copyField source="description" dest="product_keywords"/>
<copyField source="category" dest="product_keywords"/>
Solr 默认的分词器对中文支持不好,我们需要集成 IK 分词器。最新版本的 IK 分词器可以从 GitHub 获取:
# 下载IK分词器
wget https://github.com/magese/ik-analyzer-solr/releases/download/9.0.0/ik-analyzer-9.0.0.jar
# 复制到Solr的lib目录
cp ik-analyzer-9.0.0.jar server/solr-webapp/webapp/WEB-INF/lib/
# 复制词典文件到配置目录
mkdir -p server/solr/product/conf/IKAnalyzer
cp IKAnalyzer.cfg.xml ext.dic stopword.dic server/solr/product/conf/IKAnalyzer/
在 managed-schema 中添加 IK 分词器配置:
<!-- IK分词器 -->
<fieldType name="text_ik" class="solr.TextField">
<analyzer type="index">
<tokenizer class="org.wltea.analyzer.lucene.IKTokenizerFactory" useSmart="false"/>
<filter class="solr.LowerCaseFilterFactory"/>
</analyzer>
<analyzer type="query">
<tokenizer class="org.wltea.analyzer.lucene.IKTokenizerFactory" useSmart="true"/>
<filter class="solr.LowerCaseFilterFactory"/>
</analyzer>
</fieldType>
重启 Solr 使配置生效:
bin/solr restart -p 8983 -force
可以通过 Solr 的 REST API 添加文档:
# 添加单个文档
curl -X POST -H "Content-Type: application/json" http://localhost:8983/solr/product/update -d '
{
"add": {
"doc": {
"id": "p001",
"product_name": "Apple iPhone 15 Pro",
"category": "手机",
"price": 9999.00,
"description": "iPhone 15 Pro搭载A17 Pro芯片,6.1英寸Super Retina XDR显示屏",
"tags": ["Apple", "iPhone", "5G"],
"image_url": "https://example.com/images/iphone15pro.jpg",
"stock": 100,
"create_time": "2023-09-15T08:00:00Z",
"sales": 500
}
}
}'
# 批量添加文档
curl -X POST -H "Content-Type: application/json" http://localhost:8983/solr/product/update -d '
{
"add": {
"docs": [
{
"id": "p002",
"product_name": "Samsung Galaxy S23 Ultra",
"category": "手机",
"price": 8999.00,
"description": "Galaxy S23 Ultra配备2亿像素摄像头,支持S Pen",
"tags": ["Samsung", "Galaxy", "5G"],
"image_url": "https://example.com/images/s23ultra.jpg",
"stock": 80,
"create_time": "2023-02-17T08:00:00Z",
"sales": 350
},
{
"id": "p003",
"product_name": "华为Mate 60 Pro",
"category": "手机",
"price": 6999.00,
"description": "华为Mate 60 Pro支持卫星通话,搭载麒麟9000s芯片",
"tags": ["华为", "Mate", "5G"],
"image_url": "https://example.com/images/mate60pro.jpg",
"stock": 120,
"create_time": "2023-08-29T08:00:00Z",
"sales": 600
}
]
}
}'
# 提交更改
curl http://localhost:8983/solr/product/update?commit=true
Solr 通过 id 字段识别文档,更新操作会覆盖原有文档:
# 更新文档(全量更新)
curl -X POST -H "Content-Type: application/json" http://localhost:8983/solr/product/update -d '
{
"add": {
"doc": {
"id": "p001",
"product_name": "Apple iPhone 15 Pro",
"category": "手机",
"price": 9799.00, # 价格调整
"description": "iPhone 15 Pro搭载A17 Pro芯片,6.1英寸Super Retina XDR显示屏",
"tags": ["Apple", "iPhone", "5G", "新品"], # 添加标签
"image_url": "https://example.com/images/iphone15pro.jpg",
"stock": 90, # 库存减少
"create_time": "2023-09-15T08:00:00Z",
"sales": 510 # 销量增加
}
}
}'
# 部分更新(只更新指定字段)
curl -X POST -H "Content-Type: application/json" http://localhost:8983/solr/product/update -d '
{
"add": {
"doc": {
"id": "p001",
"price": 9599.00, # 只更新价格
"stock": 85 # 只更新库存
},
"overwrite": true
}
}'
# 提交更改
curl http://localhost:8983/solr/product/update?commit=true
# 按ID删除
curl -X POST -H "Content-Type: application/json" http://localhost:8983/solr/product/update -d '
{
"delete": {
"id": "p002"
}
}'
# 按查询条件删除(删除价格大于10000的商品)
curl -X POST -H "Content-Type: application/json" http://localhost:8983/solr/product/update -d '
{
"delete": {
"query": "price:[10000 TO *]"
}
}'
# 提交更改
curl http://localhost:8983/solr/product/update?commit=true
# 清空所有文档
curl -X POST http://localhost:8983/solr/product/update -d '<delete><query>*:*</query></delete>'
# 提交更改
curl http://localhost:8983/solr/product/update?commit=true
# 搜索所有商品
curl "http://localhost:8983/solr/product/select?q=*:*&wt=json&indent=true"
# 搜索名称包含"华为"的商品
curl "http://localhost:8983/solr/product/select?q=product_name:华为&wt=json&indent=true"
# 搜索价格在5000到8000之间的商品
curl "http://localhost:8983/solr/product/select?q=price:[5000 TO 8000]&wt=json&indent=true"
# 搜索分类为"手机"且销量大于300的商品
curl "http://localhost:8983/solr/product/select?q=category:手机 AND sales:[301 TO *]&wt=json&indent=true"
# 按价格升序排列
curl "http://localhost:8983/solr/product/select?q=category:手机&sort=price asc&wt=json&indent=true"
# 按销量降序排列
curl "http://localhost:8983/solr/product/select?q=category:手机&sort=sales desc&wt=json&indent=true"
# 分页查询,每页2条,查询第1页
curl "http://localhost:8983/solr/product/select?q=*:*&start=0&rows=2&wt=json&indent=true"
# 分页查询,每页2条,查询第2页
curl "http://localhost:8983/solr/product/select?q=*:*&start=2&rows=2&wt=json&indent=true"
# 只返回id、product_name和price字段
curl "http://localhost:8983/solr/product/select?q=*:*&fl=id,product_name,price&wt=json&indent=true"
# 高亮显示搜索关键词
curl "http://localhost:8983/solr/product/select?q=product_name:华为&hl=true&hl.fl=product_name&hl.simple.pre=<em>&hl.simple.post=</em>&wt=json&indent=true"
分面搜索可以按分类统计结果数量,非常适合电商的筛选功能:
# 按category分面
curl "http://localhost:8983/solr/product/select?q=*:*&facet=true&facet.field=category&wt=json&indent=true"
# 按价格区间分面
curl "http://localhost:8983/solr/product/select?q=*:*&facet=true&facet.range=price&facet.range.start=0&facet.range.end=10000&facet.range.gap=2000&wt=json&indent=true"
我们将使用 Spring Boot 3.2.0 集成 Solr,实现一个电商商品搜索服务。
pom.xml 配置:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>solr-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>solr-demo</name>
<description>Solr实战示例</description>
<properties>
<java.version>17</java.version>
<solr.version>9.4.0</solr.version>
<mybatis-plus.version>3.5.5</mybatis-plus.version>
<fastjson2.version>2.0.32</fastjson2.version>
<lombok.version>1.18.30</lombok.version>
<swagger.version>3.0.0</swagger.version>
</properties>
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Solr客户端 -->
<dependency>
<groupId>org.apache.solr</groupId>
<artifactId>solr-solrj</artifactId>
<version>${solr.version}</version>
</dependency>
<!-- MyBatis Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<!-- FastJSON2 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastjson2.version}</version>
</dependency>
<!-- Swagger3 -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${swagger.version}</version>
</dependency>
<!-- 测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
application.yml:
spring:
application:
name: solr-demo
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/ecommerce?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: root
# Solr配置
solr:
host: http://localhost:8983/solr
core: product
# MyBatis Plus配置
mybatis-plus:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.example.solrdemo.entity
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
map-underscore-to-camel-case: true
# 日志配置
logging:
level:
com.example.solrdemo: debug
# 服务器配置
server:
port: 8080
# Swagger配置
springdoc:
api-docs:
path: /api-docs
swagger-ui:
path: /swagger-ui.html
operationsSorter: method
package com.example.solrdemo.config;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.impl.HttpSolrClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.solr.core.SolrTemplate;
/**
* Solr配置类
*
* @author ken
*/
@Configuration
public class SolrConfig {
@Value("${solr.host}")
private String solrHost;
@Value("${solr.core}")
private String solrCore;
/**
* 创建SolrClient实例
*
* @return SolrClient对象
*/
@Bean
public SolrClient solrClient() {
String baseUrl = solrHost + "/" + solrCore;
return new HttpSolrClient.Builder(baseUrl).build();
}
/**
* 创建SolrTemplate实例
*
* @param solrClient SolrClient对象
* @return SolrTemplate对象
*/
@Bean
public SolrTemplate solrTemplate(SolrClient solrClient) {
return new SolrTemplate(solrClient);
}
}
package com.example.solrdemo.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.apache.solr.client.solrj.beans.Field;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
/**
* 商品实体类
*
* @author ken
*/
@Data
@TableName("product")
@Schema(description = "商品实体类")
public class Product {
@TableId(type = IdType.INPUT)
@Field("id")
@Schema(description = "商品ID")
private String id;
@Field("product_name")
@Schema(description = "商品名称")
private String productName;
@Field("category")
@Schema(description = "商品分类")
private String category;
@Field("price")
@Schema(description = "商品价格")
private BigDecimal price;
@Field("description")
@Schema(description = "商品描述")
private String description;
@Field("tags")
@Schema(description = "商品标签")
private List<String> tags;
@Field("image_url")
@Schema(description = "商品图片URL")
private String imageUrl;
@Field("stock")
@Schema(description = "商品库存")
private Integer stock;
@Field("create_time")
@Schema(description = "商品上架时间")
private LocalDateTime createTime;
@Field("sales")
@Schema(description = "商品销量")
private Integer sales;
}
package com.example.solrdemo.entity.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 搜索请求参数
*
* @author ken
*/
@Data
@Schema(description = "搜索请求参数")
public class SearchRequest {
@Schema(description = "搜索关键词")
private String keyword;
@Schema(description = "商品分类")
private String category;
@Schema(description = "最低价格")
private Double minPrice;
@Schema(description = "最高价格")
private Double maxPrice;
@Schema(description = "排序字段")
private String sortField;
@Schema(description = "排序方式:asc/desc")
private String sortOrder;
@Schema(description = "页码,从1开始")
private Integer pageNum = 1;
@Schema(description = "每页条数")
private Integer pageSize = 10;
}
package com.example.solrdemo.entity.vo;
import com.example.solrdemo.entity.Product;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
import java.util.Map;
/**
* 搜索结果
*
* @author ken
*/
@Data
@Schema(description = "搜索结果")
public class SearchResult {
@Schema(description = "商品列表")
private List<Product> products;
@Schema(description = "总条数")
private Long total;
@Schema(description = "总页数")
private Integer totalPages;
@Schema(description = "当前页码")
private Integer pageNum;
@Schema(description = "分面结果")
private Map<String, Map<String, Long>> facetResults;
}
package com.example.solrdemo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.solrdemo.entity.Product;
import org.apache.ibatis.annotations.Mapper;
/**
* 商品数据库访问接口
*
* @author ken
*/
@Mapper
public interface ProductMapper extends BaseMapper<Product> {
}
package com.example.solrdemo.dao;
import com.example.solrdemo.entity.Product;
import com.example.solrdemo.entity.dto.SearchRequest;
import com.example.solrdemo.entity.vo.SearchResult;
import java.util.List;
/**
* Solr数据访问接口
*
* @author ken
*/
public interface SolrProductDao {
/**
* 添加商品到Solr
*
* @param product 商品对象
*/
void addProduct(Product product);
/**
* 批量添加商品到Solr
*
* @param products 商品列表
*/
void batchAddProducts(List<Product> products);
/**
* 更新Solr中的商品
*
* @param product 商品对象
*/
void updateProduct(Product product);
/**
* 根据ID删除Solr中的商品
*
* @param id 商品ID
*/
void deleteProductById(String id);
/**
* 根据查询条件删除Solr中的商品
*
* @param query 查询条件
*/
void deleteProductsByQuery(String query);
/**
* 搜索商品
*
* @param request 搜索请求参数
* @return 搜索结果
*/
SearchResult searchProducts(SearchRequest request);
/**
* 根据ID查询商品
*
* @param id 商品ID
* @return 商品对象
*/
Product getProductById(String id);
/**
* 提交Solr事务
*/
void commit();
}
Solr DAO 实现类:
package com.example.solrdemo.dao.impl;
import com.alibaba.fastjson2.JSON;
import com.example.solrdemo.dao.SolrProductDao;
import com.example.solrdemo.entity.Product;
import com.example.solrdemo.entity.dto.SearchRequest;
import com.example.solrdemo.entity.vo.SearchResult;
import lombok.extern.slf4j.Slf4j;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.response.FacetField;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrDocumentList;
import org.apache.solr.common.SolrInputDocument;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Repository;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Solr数据访问实现类
*
* @author ken
*/
@Slf4j
@Repository
public class SolrProductDaoImpl implements SolrProductDao {
private final SolrClient solrClient;
public SolrProductDaoImpl(SolrClient solrClient) {
this.solrClient = solrClient;
}
@Override
public void addProduct(Product product) {
try {
SolrInputDocument document = convertToSolrDocument(product);
solrClient.add(document);
log.info("添加商品到Solr成功,商品ID:{}", product.getId());
} catch (Exception e) {
log.error("添加商品到Solr失败,商品ID:{}", product.getId(), e);
throw new RuntimeException("添加商品到Solr失败", e);
}
}
@Override
public void batchAddProducts(List<Product> products) {
if (CollectionUtils.isEmpty(products)) {
log.warn("批量添加商品到Solr,商品列表为空");
return;
}
try {
List<SolrInputDocument> documents = new ArrayList<>(products.size());
for (Product product : products) {
documents.add(convertToSolrDocument(product));
}
solrClient.add(documents);
log.info("批量添加商品到Solr成功,数量:{}", products.size());
} catch (Exception e) {
log.error("批量添加商品到Solr失败", e);
throw new RuntimeException("批量添加商品到Solr失败", e);
}
}
@Override
public void updateProduct(Product product) {
try {
SolrInputDocument document = convertToSolrDocument(product);
solrClient.add(document);
log.info("更新Solr中的商品成功,商品ID:{}", product.getId());
} catch (Exception e) {
log.error("更新Solr中的商品失败,商品ID:{}", product.getId(), e);
throw new RuntimeException("更新Solr中的商品失败", e);
}
}
@Override
public void deleteProductById(String id) {
if (!StringUtils.hasText(id)) {
log.warn("删除Solr中的商品,商品ID为空");
return;
}
try {
solrClient.deleteById(id);
log.info("删除Solr中的商品成功,商品ID:{}", id);
} catch (Exception e) {
log.error("删除Solr中的商品失败,商品ID:{}", id, e);
throw new RuntimeException("删除Solr中的商品失败", e);
}
}
@Override
public void deleteProductsByQuery(String query) {
if (!StringUtils.hasText(query)) {
log.warn("删除Solr中的商品,查询条件为空");
return;
}
try {
solrClient.deleteByQuery(query);
log.info("根据查询条件删除Solr中的商品成功,查询条件:{}", query);
} catch (Exception e) {
log.error("根据查询条件删除Solr中的商品失败,查询条件:{}", query, e);
throw new RuntimeException("根据查询条件删除Solr中的商品失败", e);
}
}
@Override
public SearchResult searchProducts(SearchRequest request) {
log.info("搜索商品,请求参数:{}", JSON.toJSONString(request));
try {
SolrQuery solrQuery = buildSolrQuery(request);
QueryResponse response = solrClient.query(solrQuery);
return convertToSearchResult(response, request);
} catch (Exception e) {
log.error("搜索商品失败,请求参数:{}", JSON.toJSONString(request), e);
throw new RuntimeException("搜索商品失败", e);
}
}
@Override
public Product getProductById(String id) {
if (!StringUtils.hasText(id)) {
log.warn("根据ID查询Solr中的商品,商品ID为空");
return null;
}
try {
SolrDocument document = solrClient.getById(id);
if (ObjectUtils.isEmpty(document)) {
log.info("根据ID查询Solr中的商品,未找到商品,商品ID:{}", id);
return null;
}
return convertToProduct(document);
} catch (Exception e) {
log.error("根据ID查询Solr中的商品失败,商品ID:{}", id, e);
throw new RuntimeException("根据ID查询Solr中的商品失败", e);
}
}
@Override
public void commit() {
try {
solrClient.commit();
log.info("提交Solr事务成功");
} catch (Exception e) {
log.error("提交Solr事务失败", e);
throw new RuntimeException("提交Solr事务失败", e);
}
}
/**
* 将Product对象转换为SolrInputDocument
*
* @param product 商品对象
* @return SolrInputDocument对象
*/
private SolrInputDocument convertToSolrDocument(Product product) {
SolrInputDocument document = new SolrInputDocument();
document.setField("id", product.getId());
document.setField("product_name", product.getProductName());
document.setField("category", product.getCategory());
document.setField("price", product.getPrice());
document.setField("description", product.getDescription());
document.setField("tags", product.getTags());
document.setField("image_url", product.getImageUrl());
document.setField("stock", product.getStock());
document.setField("create_time", product.getCreateTime());
document.setField("sales", product.getSales());
return document;
}
/**
* 将SolrDocument转换为Product对象
*
* @param document Solr文档
* @return Product对象
*/
private Product convertToProduct(SolrDocument document) {
Product product = new Product();
product.setId((String) document.getFieldValue("id"));
product.setProductName((String) document.getFieldValue("product_name"));
product.setCategory((String) document.getFieldValue("category"));
product.setPrice((java.math.BigDecimal) document.getFieldValue("price"));
product.setDescription((String) document.getFieldValue("description"));
product.setTags((List<String>) document.getFieldValue("tags"));
product.setImageUrl((String) document.getFieldValue("image_url"));
product.setStock((Integer) document.getFieldValue("stock"));
product.setCreateTime((java.time.LocalDateTime) document.getFieldValue("create_time"));
product.setSales((Integer) document.getFieldValue("sales"));
return product;
}
/**
* 构建Solr查询对象
*
* @param request 搜索请求参数
* @return SolrQuery对象
*/
private SolrQuery buildSolrQuery(SearchRequest request) {
SolrQuery query = new SolrQuery();
// 构建查询条件
StringBuilder queryStr = new StringBuilder();
if (StringUtils.hasText(request.getKeyword())) {
queryStr.append("product_keywords:").append(request.getKeyword());
} else {
queryStr.append("*:*");
}
if (StringUtils.hasText(request.getCategory())) {
queryStr.append(" AND category:").append(request.getCategory());
}
if (request.getMinPrice() != null && request.getMaxPrice() != null) {
queryStr.append(" AND price:[").append(request.getMinPrice()).append(" TO ").append(request.getMaxPrice()).append("]");
} else if (request.getMinPrice() != null) {
queryStr.append(" AND price:[").append(request.getMinPrice()).append(" TO *]");
} else if (request.getMaxPrice() != null) {
queryStr.append(" AND price:[* TO ").append(request.getMaxPrice()).append("]");
}
query.setQuery(queryStr.toString());
// 设置排序
if (StringUtils.hasText(request.getSortField())) {
String sortOrder = StringUtils.hasText(request.getSortOrder()) ? request.getSortOrder() : "asc";
query.addSort(request.getSortField(), "desc".equalsIgnoreCase(sortOrder) ? SolrQuery.ORDER.desc : SolrQuery.ORDER.asc);
} else {
// 默认按相关性排序
query.addSort("score", SolrQuery.ORDER.desc);
}
// 设置分页
int start = (request.getPageNum() - 1) * request.getPageSize();
query.setStart(start);
query.setRows(request.getPageSize());
// 启用高亮
query.setHighlight(true);
query.addHighlightField("product_name");
query.addHighlightField("description");
query.setHighlightSimplePre("<em>");
query.setHighlightSimplePost("</em>");
// 启用分面
query.setFacet(true);
query.addFacetField("category");
query.setFacetMinCount(1);
query.setFacetLimit(10);
// 设置返回字段
query.setFields("id", "product_name", "category", "price", "description", "tags", "image_url", "stock", "create_time", "sales");
return query;
}
/**
* 将查询响应转换为搜索结果
*
* @param response 查询响应
* @param request 搜索请求参数
* @return 搜索结果
*/
private SearchResult convertToSearchResult(QueryResponse response, SearchRequest request) {
SearchResult result = new SearchResult();
// 处理商品列表
SolrDocumentList documents = response.getResults();
List<Product> products = new ArrayList<>();
for (SolrDocument document : documents) {
Product product = convertToProduct(document);
products.add(product);
}
result.setProducts(products);
// 处理分页信息
result.setTotal(documents.getNumFound());
long totalPages = (documents.getNumFound() + request.getPageSize() - 1) / request.getPageSize();
result.setTotalPages((int) totalPages);
result.setPageNum(request.getPageNum());
// 处理分面结果
Map<String, Map<String, Long>> facetResults = new HashMap<>();
List<FacetField> facetFields = response.getFacetFields();
if (!CollectionUtils.isEmpty(facetFields)) {
for (FacetField facetField : facetFields) {
String fieldName = facetField.getName();
Map<String, Long> fieldFacet = new HashMap<>();
List<FacetField.Count> counts = facetField.getValues();
if (!CollectionUtils.isEmpty(counts)) {
for (FacetField.Count count : counts) {
fieldFacet.put(count.getName(), count.getCount());
}
}
facetResults.put(fieldName, fieldFacet);
}
}
result.setFacetResults(facetResults);
return result;
}
}
package com.example.solrdemo.service;
import com.example.solrdemo.entity.Product;
import com.example.solrdemo.entity.dto.SearchRequest;
import com.example.solrdemo.entity.vo.SearchResult;
import java.util.List;
/**
* 商品搜索服务接口
*
* @author ken
*/
public interface ProductSearchService {
/**
* 将商品添加到索引
*
* @param product 商品对象
*/
void indexProduct(Product product);
/**
* 将商品列表批量添加到索引
*
* @param products 商品列表
*/
void batchIndexProducts(List<Product> products);
/**
* 更新商品索引
*
* @param product 商品对象
*/
void updateProductIndex(Product product);
/**
* 根据ID删除商品索引
*
* @param id 商品ID
*/
void deleteProductIndex(String id);
/**
* 从数据库同步商品到Solr
*/
void syncProductsFromDbToSolr();
/**
* 搜索商品
*
* @param request 搜索请求参数
* @return 搜索结果
*/
SearchResult searchProducts(SearchRequest request);
/**
* 根据ID查询商品
*
* @param id 商品ID
* @return 商品对象
*/
Product getProductById(String id);
}
服务实现类:
package com.example.solrdemo.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.example.solrdemo.dao.SolrProductDao;
import com.example.solrdemo.entity.Product;
import com.example.solrdemo.entity.dto.SearchRequest;
import com.example.solrdemo.entity.vo.SearchResult;
import com.example.solrdemo.mapper.ProductMapper;
import com.example.solrdemo.service.ProductSearchService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import java.util.List;
/**
* 商品搜索服务实现类
*
* @author ken
*/
@Slf4j
@Service
public class ProductSearchServiceImpl implements ProductSearchService {
private final SolrProductDao solrProductDao;
private final ProductMapper productMapper;
public ProductSearchServiceImpl(SolrProductDao solrProductDao, ProductMapper productMapper) {
this.solrProductDao = solrProductDao;
this.productMapper = productMapper;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void indexProduct(Product product) {
if (product == null) {
log.warn("添加商品索引,商品对象为空");
return;
}
solrProductDao.addProduct(product);
solrProductDao.commit();
}
@Override
@Transactional(rollbackFor = Exception.class)
public void batchIndexProducts(List<Product> products) {
if (CollectionUtils.isEmpty(products)) {
log.warn("批量添加商品索引,商品列表为空");
return;
}
solrProductDao.batchAddProducts(products);
solrProductDao.commit();
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateProductIndex(Product product) {
if (product == null) {
log.warn("更新商品索引,商品对象为空");
return;
}
solrProductDao.updateProduct(product);
solrProductDao.commit();
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteProductIndex(String id) {
solrProductDao.deleteProductById(id);
solrProductDao.commit();
}
@Override
@Transactional(rollbackFor = Exception.class)
public void syncProductsFromDbToSolr() {
log.info("开始从数据库同步商品到Solr");
// 先清空现有索引
solrProductDao.deleteProductsByQuery("*:*");
// 分批查询数据库并同步
int batchSize = 1000;
int pageNum = 1;
while (true) {
int offset = (pageNum - 1) * batchSize;
QueryWrapper<Product> queryWrapper = new QueryWrapper<>();
queryWrapper.last("LIMIT " + offset + ", " + batchSize);
List<Product> products = productMapper.selectList(queryWrapper);
if (CollectionUtils.isEmpty(products)) {
break;
}
solrProductDao.batchAddProducts(products);
log.info("同步商品到Solr,批次:{},数量:{}", pageNum, products.size());
pageNum++;
}
solrProductDao.commit();
log.info("从数据库同步商品到Solr完成");
}
@Override
public SearchResult searchProducts(SearchRequest request) {
return solrProductDao.searchProducts(request);
}
@Override
public Product getProductById(String id) {
return solrProductDao.getProductById(id);
}
}
package com.example.solrdemo.controller;
import com.example.solrdemo.entity.Product;
import com.example.solrdemo.entity.dto.SearchRequest;
import com.example.solrdemo.entity.vo.SearchResult;
import com.example.solrdemo.service.ProductSearchService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 商品搜索控制器
*
* @author ken
*/
@RestController
@RequestMapping("/api/product")
@Tag(name = "商品搜索接口", description = "提供商品的索引和搜索功能")
public class ProductSearchController {
private final ProductSearchService productSearchService;
public ProductSearchController(ProductSearchService productSearchService) {
this.productSearchService = productSearchService;
}
@PostMapping("/index")
@Operation(summary = "添加商品索引", description = "将单个商品添加到Solr索引")
public ResponseEntity<String> indexProduct(@RequestBody Product product) {
productSearchService.indexProduct(product);
return ResponseEntity.ok("商品索引添加成功");
}
@PostMapping("/index/batch")
@Operation(summary = "批量添加商品索引", description = "将多个商品批量添加到Solr索引")
public ResponseEntity<String> batchIndexProducts(@RequestBody List<Product> products) {
productSearchService.batchIndexProducts(products);
return ResponseEntity.ok("商品索引批量添加成功");
}
@PutMapping("/index")
@Operation(summary = "更新商品索引", description = "更新Solr中的商品索引")
public ResponseEntity<String> updateProductIndex(@RequestBody Product product) {
productSearchService.updateProductIndex(product);
return ResponseEntity.ok("商品索引更新成功");
}
@DeleteMapping("/index/{id}")
@Operation(summary = "删除商品索引", description = "根据ID删除Solr中的商品索引")
public ResponseEntity<String> deleteProductIndex(
@Parameter(description = "商品ID", required = true) @PathVariable String id) {
productSearchService.deleteProductIndex(id);
return ResponseEntity.ok("商品索引删除成功");
}
@PostMapping("/sync")
@Operation(summary = "同步商品数据", description = "从数据库同步商品数据到Solr")
public ResponseEntity<String> syncProductsFromDbToSolr() {
productSearchService.syncProductsFromDbToSolr();
return ResponseEntity.ok("商品数据同步成功");
}
@GetMapping("/search")
@Operation(summary = "搜索商品", description = "根据条件搜索商品")
public ResponseEntity<SearchResult> searchProducts(SearchRequest request) {
SearchResult result = productSearchService.searchProducts(request);
return ResponseEntity.ok(result);
}
@GetMapping("/{id}")
@Operation(summary = "查询商品详情", description = "根据ID查询商品详情")
public ResponseEntity<Product> getProductById(
@Parameter(description = "商品ID", required = true) @PathVariable String id) {
Product product = productSearchService.getProductById(id);
return ResponseEntity.ok(product);
}
}
package com.example.solrdemo;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.info.Info;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 应用启动类
*
* @author ken
*/
@SpringBootApplication
@MapperScan("com.example.solrdemo.mapper")
@OpenAPIDefinition(info = @Info(title = "商品搜索API", version = "1.0", description = "商品搜索服务接口文档"))
public class SolrDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SolrDemoApplication.class, args);
}
}
-- 创建电商数据库
CREATE DATABASE IF NOT EXISTS ecommerce CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 使用电商数据库
USE ecommerce;
-- 创建商品表
CREATE TABLE IF NOT EXISTS product (
id VARCHAR(32) NOT NULL COMMENT '商品ID',
product_name VARCHAR(255) NOT NULL COMMENT '商品名称',
category VARCHAR(100) NOT NULL COMMENT '商品分类',
price DECIMAL(10, 2) NOT NULL COMMENT '商品价格',
description TEXT COMMENT '商品描述',
tags VARCHAR(255) COMMENT '商品标签,逗号分隔',
image_url VARCHAR(255) COMMENT '商品图片URL',
stock INT NOT NULL DEFAULT 0 COMMENT '商品库存',
create_time DATETIME NOT NULL COMMENT '商品上架时间',
sales INT NOT NULL DEFAULT 0 COMMENT '商品销量',
PRIMARY KEY (id),
KEY idx_category (category),
KEY idx_create_time (create_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表';
-- 插入测试数据
INSERT INTO product (id, product_name, category, price, description, tags, image_url, stock, create_time, sales)
VALUES
('p001', 'Apple iPhone 15 Pro', '手机', 9999.00, 'iPhone 15 Pro搭载A17 Pro芯片,6.1英寸Super Retina XDR显示屏', 'Apple,iPhone,5G', 'https://example.com/images/iphone15pro.jpg', 100, '2023-09-15 08:00:00', 500),
('p002', 'Samsung Galaxy S23 Ultra', '手机', 8999.00, 'Galaxy S23 Ultra配备2亿像素摄像头,支持S Pen', 'Samsung,Galaxy,5G', 'https://example.com/images/s23ultra.jpg', 80, '2023-02-17 08:00:00', 350),
('p003', '华为Mate 60 Pro', '手机', 6999.00, '华为Mate 60 Pro支持卫星通话,搭载麒麟9000s芯片', '华为,Mate,5G', 'https://example.com/images/mate60pro.jpg', 120, '2023-08-29 08:00:00', 600),
('p004', '小米14', '手机', 4999.00, '小米14搭载骁龙8 Gen3处理器,徕卡Summilux镜头', '小米,14,5G', 'https://example.com/images/mi14.jpg', 150, '2023-12-01 08:00:00', 420),
('p005', 'OPPO Find X7 Ultra', '手机', 6499.00, 'OPPO Find X7 Ultra配备双潜望四摄,哈苏影像', 'OPPO,Find,5G', 'https://example.com/images/findx7ultra.jpg', 90, '2024-03-15 08:00:00', 280);
在搜索中,同义词处理可以提高搜索的召回率。例如,"手机" 和 "电话机" 应该被视为同义词。
配置步骤:
server/solr/product/conf/synonyms.txt:手机,电话机
电脑,计算机
笔记本,笔记本电脑
<fieldType name="text_ik" class="solr.TextField">
<analyzer type="index">
<tokenizer class="org.wltea.analyzer.lucene.IKTokenizerFactory" useSmart="false"/>
<filter class="solr.SynonymGraphFilterFactory" synonyms="synonyms.txt" ignoreCase="true" expand="true"/>
<filter class="solr.LowerCaseFilterFactory"/>
</analyzer>
<analyzer type="query">
<tokenizer class="org.wltea.analyzer.lucene.IKTokenizerFactory" useSmart="true"/>
<filter class="solr.SynonymGraphFilterFactory" synonyms="synonyms.txt" ignoreCase="true" expand="true"/>
<filter class="solr.LowerCaseFilterFactory"/>
</analyzer>
</fieldType>
Solr 的拼写检查功能可以为用户提供拼写建议,提高搜索体验。
配置步骤:
server/solr/product/conf/solrconfig.xml中添加拼写检查配置:<requestHandler name="/spell" class="solr.SearchHandler">
<lst name="defaults">
<str name="spellcheck">on</str>
<str name="spellcheck.dictionary">default</str>
<str name="spellcheck.count">5</str>
</lst>
<arr name="last-components">
<str>spellcheck</str>
</arr>
</requestHandler>
<searchComponent name="spellcheck" class="solr.SpellCheckComponent">
<lst name="spellchecker">
<str name="name">default</str>
<str name="field">product_keywords</str>
<str name="classname">solr.DirectSolrSpellChecker</str>
<str name="distanceMeasure">internal</str>
<float name="accuracy">0.5</float>
<int name="maxEdits">2</int>
<int name="minPrefix">1</int>
<int name="maxInspections">5</int>
<int name="minQueryLength">4</int>
<float name="maxQueryFrequency">0.01</float>
</lst>
</searchComponent>
curl "http://localhost:8983/solr/product/spell?q=iphon&wt=json&indent=true"
Solr 默认使用相关性得分进行排序,我们也可以自定义评分规则。
例如,我们希望价格低的商品评分更高:
# 使用函数查询自定义评分
curl "http://localhost:8983/solr/product/select?q={!boost b=log(price)}product_name:手机&wt=json&indent=true"
更复杂的评分策略可以在 solrconfig.xml 中配置:
<requestHandler name="/select" class="solr.SearchHandler">
<lst name="defaults">
<str name="echoParams">explicit</str>
<int name="rows">10</int>
<str name="df">product_keywords</str>
<!-- 配置自定义评分函数 -->
<str name="bf">
log(sales + 1) * 0.3 # 销量越高,评分越高
+ log(stock + 1) * 0.1 # 库存越多,评分略高
+ recip(ms(create_time), 3.16e-11, 1, 1) * 0.2 # 时间越近,评分越高
+ 0.4 # 基础分
</str>
</lst>
</requestHandler>
对于大规模数据,单节点 Solr 可能无法满足性能需求,此时需要部署 SolrCloud 集群。

# 节点1
bin/solr start -cloud -z localhost:2181,localhost:2182,localhost:2183 -p 8983 -force
# 节点2
bin/solr start -cloud -z localhost:2181,localhost:2182,localhost:2183 -p 8984 -force
# 节点3
bin/solr start -cloud -z localhost:2181,localhost:2182,localhost:2183 -p 8985 -force
bin/solr create -c product -shards 2 -replicationFactor 2 -force
@Bean
public SolrClient solrClient() {
List<String> zkHosts = Arrays.asList("localhost:2181", "localhost:2182", "localhost:2183");
String zkChroot = "/solr";
return new CloudSolrClient.Builder(zkHosts, Optional.of(zkChroot)).build();
}
合理设计字段:
indexed="false"stored="false"优化分词器:
批量索引:
索引优化参数:
<indexConfig>
<useCompoundFile>false</useCompoundFile>
<mergeFactor>10</mergeFactor>
<ramBufferSizeMB>128</ramBufferSizeMB>
<maxBufferedDocs>10000</maxBufferedDocs>
</indexConfig>
使用过滤查询:
curl "http://localhost:8983/solr/product/select?q=手机&fq=category:手机&fq=price:[5000 TO 10000]&wt=json"
fq参数,Solr 会缓存过滤结果限制返回字段:
curl "http://localhost:8983/solr/product/select?q=*:*&fl=id,product_name,price&wt=json"
fl参数只返回需要的字段合理设置缓存:
<queryCache class="solr.LRUCache" size="512" initialSize="512" autowarmCount="0"/>
<queryResultCache class="solr.LRUCache" size="512" initialSize="512" autowarmCount="32"/>
<filterCache class="solr.LRUCache" size="1024" initialSize="1024" autowarmCount="0"/>
<documentCache class="solr.LRUCache" size="512" initialSize="512" autowarmCount="0"/>
避免通配符前缀查询:
*phone这样的查询,会导致性能问题内存配置:
bin/solr.in.sh中的SOLR_HEAP="4g"磁盘选择:
JVM 优化:
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:ParallelGCThreads=8
-XX:ConcGCThreads=2
-XX:InitiatingHeapOccupancyPercent=70
原因:商品数据在数据库更新后,未及时同步到 Solr。
解决方案:
/**
* 商品服务中同步更新Solr的示例
*/
@Service
public class ProductServiceImpl implements ProductService {
private final ProductMapper productMapper;
private final ProductSearchService productSearchService;
// 构造函数注入...
@Override
@Transactional(rollbackFor = Exception.class)
public void updateProduct(Product product) {
// 更新数据库
productMapper.updateById(product);
// 同步更新Solr
productSearchService.updateProductIndex(product);
}
}
原因:
解决方案:
原因:
解决方案:
<indexConfig>
<mergePolicy class="org.apache.lucene.index.TieredMergePolicy">
<int name="maxMergeAtOnce">10</int>
<int name="segmentsPerTier">10</int>
<double name="maxMergeMB">512</double>
</mergePolicy>
</indexConfig>
希望本文能帮助你快速掌握 Solr 的核心技术,并应用到实际项目中。如果你有任何问题或建议,欢迎留言讨论。