首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >从 0 到 1 精通 Solr 实操:电商搜索实战全攻略

从 0 到 1 精通 Solr 实操:电商搜索实战全攻略

作者头像
果酱带你啃AI
发布于 2026-04-14 13:07:00
发布于 2026-04-14 13:07:00
4250
举报

引言:为什么 Solr 是企业级搜索的首选?

在当今信息爆炸的时代,用户对搜索体验的要求越来越高。从电商平台的商品搜索到内容管理系统的文档检索,高效、精准的搜索功能已成为企业核心竞争力之一。Apache Solr 作为一款成熟的开源搜索平台,凭借其强大的全文检索能力、分布式架构支持和丰富的功能特性,被 Netflix、eBay、Instagram 等众多巨头企业广泛采用。

本文将以实战为导向,从环境搭建到分布式部署,从基础查询到性能优化,全方位剖析 Solr 的核心技术与最佳实践。无论你是刚接触搜索引擎的新手,还是需要解决实际问题的开发工程师,都能从中获得可直接应用于生产环境的知识和代码。

一、Solr 核心概念与工作原理

1.1 什么是 Solr?

Apache Solr 是一个基于 Lucene 的开源企业级搜索平台,它提供了全文检索、高亮显示、分面搜索、动态聚类、数据库集成等功能,支持分布式部署,具有高可靠性和可扩展性。

1.2 Solr 与 Lucene 的关系

很多人会混淆 Solr 和 Lucene 的关系,简单来说:

  • Lucene 是一个 Java 编写的全文检索引擎核心库,提供了索引和搜索的底层 API
  • Solr 是基于 Lucene 构建的企业级搜索服务器,提供了 RESTful API、管理界面等更高层的功能

1.3 Solr 的核心组件

Solr 的核心组件包括:

  1. Solr Core:一个独立的索引和搜索单元,每个 Core 可以有自己的配置和数据
  2. Schema:定义了文档的结构,包括字段类型、字段属性等
  3. 索引 (Index):类似于数据库中的表,存储文档的倒排索引
  4. 文档 (Document):类似于数据库中的行,由多个字段 (Field) 组成
  5. 请求处理程序 (Request Handler):处理客户端的搜索和索引请求
  6. 分析器 (Analyzer):对文本进行分词、过滤等处理
  7. 查询解析器 (Query Parser):解析查询语句

1.4 倒排索引:Solr 快速搜索的秘密

倒排索引是 Solr 实现高效搜索的核心数据结构,它将文档中的词语映射到包含该词语的文档列表。

举个例子,有以下文档:

  • 文档 1:Solr 是一个开源搜索引擎
  • 文档 2:Lucene 是 Solr 的核心
  • 文档 3:搜索引擎需要倒排索引

倒排索引会存储:

  • Solr → [文档 1, 文档 2]
  • 开源 → [文档 1]
  • 搜索引擎 → [文档 1, 文档 3]
  • Lucene → [文档 2]
  • 核心 → [文档 2]
  • 需要 → [文档 3]
  • 倒排索引 → [文档 3]

这种结构使得 Solr 能在毫秒级时间内找到包含特定词语的所有文档。

二、Solr 环境搭建与配置

2.1 环境准备

在开始之前,确保你的系统满足以下要求:

  • JDK 11+(推荐 JDK 17)
  • 至少 2GB 内存
  • 网络连接(用于下载依赖)

2.2 安装 Solr

我们将安装最新稳定版 Solr 9.4.0:

代码语言:javascript
复制
# 下载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
代码语言:javascript
复制

成功启动后,访问 http://localhost:8983 即可看到 Solr 的管理界面。

2.3 创建 Solr Core

Core 是 Solr 存储索引和配置的基本单元,创建一个名为 "product" 的 core 用于电商商品搜索:

代码语言:javascript
复制
# 创建core
bin/solr create -c product -force

# 查看已创建的core
bin/solr list -c
代码语言:javascript
复制

2.4 Solr 目录结构解析

了解 Solr 的目录结构有助于更好地配置和管理 Solr:

代码语言:javascript
复制
solr-9.4.0/
├── bin/            # 可执行脚本
├── conf/           # 全局配置文件
├── contrib/        # 扩展模块
├── dist/           # 编译后的jar包
├── docs/           # 文档
├── example/        # 示例
├── server/         # 服务器相关文件
│   ├── solr/       # Solr主目录
│   │   ├── configsets/ # 配置集
│   │   └── product/    # 我们创建的product core
│   │       ├── conf/   # core配置文件
│   │       └── data/   # 索引数据
│   └── webapps/    # Web应用
└── licenses/       # 许可证
代码语言:javascript
复制

2.5 配置 managed-schema

schema 定义了文档的结构,Solr 9 默认使用 managed-schema(可通过 API 动态修改),位于server/solr/product/conf/managed-schema。

我们为电商商品搜索定义以下字段:

代码语言:javascript
复制
<!-- 商品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"/>
代码语言:javascript
复制

2.6 配置中文分词器

Solr 默认的分词器对中文支持不好,我们需要集成 IK 分词器。最新版本的 IK 分词器可以从 GitHub 获取:

代码语言:javascript
复制
# 下载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/
代码语言:javascript
复制

在 managed-schema 中添加 IK 分词器配置:

代码语言:javascript
复制
<!-- 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>
代码语言:javascript
复制

重启 Solr 使配置生效:

代码语言:javascript
复制
bin/solr restart -p 8983 -force
代码语言:javascript
复制

三、Solr 基本操作:索引与搜索

3.1 索引操作详解

3.1.1 添加文档

可以通过 Solr 的 REST API 添加文档:

代码语言:javascript
复制
# 添加单个文档
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
代码语言:javascript
复制

3.1.2 更新文档

Solr 通过 id 字段识别文档,更新操作会覆盖原有文档:

代码语言:javascript
复制
# 更新文档(全量更新)
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
代码语言:javascript
复制

3.1.3 删除文档
代码语言:javascript
复制
# 按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
代码语言:javascript
复制

3.1.4 清空索引
代码语言:javascript
复制
# 清空所有文档
curl -X POST http://localhost:8983/solr/product/update -d '<delete><query>*:*</query></delete>'
# 提交更改
curl http://localhost:8983/solr/product/update?commit=true
代码语言:javascript
复制

3.2 搜索操作详解

3.2.1 基本搜索
代码语言:javascript
复制
# 搜索所有商品
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"
代码语言:javascript
复制

3.2.2 排序
代码语言:javascript
复制
# 按价格升序排列
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"
代码语言:javascript
复制

3.2.3 分页
代码语言:javascript
复制
# 分页查询,每页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"
代码语言:javascript
复制

3.2.4 字段过滤
代码语言:javascript
复制
# 只返回id、product_name和price字段
curl "http://localhost:8983/solr/product/select?q=*:*&fl=id,product_name,price&wt=json&indent=true"
代码语言:javascript
复制

3.2.5 高亮显示
代码语言:javascript
复制
# 高亮显示搜索关键词
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"
代码语言:javascript
复制

3.2.6 分面搜索(Facet)

分面搜索可以按分类统计结果数量,非常适合电商的筛选功能:

代码语言:javascript
复制
# 按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"
代码语言:javascript
复制

四、Java 集成 Solr 实战

4.1 项目搭建

我们将使用 Spring Boot 3.2.0 集成 Solr,实现一个电商商品搜索服务。

4.1.1 创建 Maven 项目

pom.xml 配置:

代码语言:javascript
复制
<?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>
代码语言:javascript
复制

4.1.2 配置文件

application.yml:

代码语言:javascript
复制
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
代码语言:javascript
复制

4.1.3 Solr 配置类
代码语言:javascript
复制
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);
    }
}
代码语言:javascript
复制

4.2 实体类定义

4.2.1 商品实体类
代码语言:javascript
复制
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;
}
代码语言:javascript
复制

4.2.2 搜索请求参数类
代码语言:javascript
复制
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;
}
代码语言:javascript
复制

4.2.3 搜索结果类
代码语言:javascript
复制
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;
}
代码语言:javascript
复制

4.3 数据访问层

4.3.1 数据库 DAO 层
代码语言:javascript
复制
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> {
}
代码语言:javascript
复制

4.3.2 Solr DAO 层
代码语言:javascript
复制
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();
}
代码语言:javascript
复制

Solr DAO 实现类:

代码语言:javascript
复制
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;
    }
}
代码语言:javascript
复制

4.4 服务层

代码语言:javascript
复制
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);
}
代码语言:javascript
复制

服务实现类:

代码语言:javascript
复制
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);
    }
}
代码语言:javascript
复制

4.5 控制器

代码语言:javascript
复制
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);
    }
}
代码语言:javascript
复制

4.6 启动类

代码语言:javascript
复制
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);
    }
}
代码语言:javascript
复制

4.7 数据库脚本

代码语言:javascript
复制
-- 创建电商数据库
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);
代码语言:javascript
复制

五、Solr 高级功能

5.1 同义词处理

在搜索中,同义词处理可以提高搜索的召回率。例如,"手机" 和 "电话机" 应该被视为同义词。

配置步骤:

  1. 创建同义词文件server/solr/product/conf/synonyms.txt:
代码语言:javascript
复制
手机,电话机
电脑,计算机
笔记本,笔记本电脑
代码语言:javascript
复制

  1. 在 managed-schema 中配置同义词过滤器:
代码语言:javascript
复制
<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>
代码语言:javascript
复制

  1. 重启 Solr 使配置生效

5.2 拼写检查

Solr 的拼写检查功能可以为用户提供拼写建议,提高搜索体验。

配置步骤:

  1. 在server/solr/product/conf/solrconfig.xml中添加拼写检查配置:
代码语言:javascript
复制
<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>
代码语言:javascript
复制

  1. 重启 Solr
  2. 使用拼写检查:
代码语言:javascript
复制
curl "http://localhost:8983/solr/product/spell?q=iphon&wt=json&indent=true"
代码语言:javascript
复制

5.3 相关性排序

Solr 默认使用相关性得分进行排序,我们也可以自定义评分规则。

例如,我们希望价格低的商品评分更高:

代码语言:javascript
复制
# 使用函数查询自定义评分
curl "http://localhost:8983/solr/product/select?q={!boost b=log(price)}product_name:手机&wt=json&indent=true"
代码语言:javascript
复制

更复杂的评分策略可以在 solrconfig.xml 中配置:

代码语言:javascript
复制
<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>
代码语言:javascript
复制

5.4 分布式部署

对于大规模数据,单节点 Solr 可能无法满足性能需求,此时需要部署 SolrCloud 集群。

5.4.1 SolrCloud 架构
5.4.2 部署步骤
  1. 启动 ZooKeeper 集群(至少 3 个节点)
  2. 启动 Solr 节点并加入集群:
代码语言:javascript
复制
# 节点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
代码语言:javascript
复制

  1. 创建分布式集合:
代码语言:javascript
复制
bin/solr create -c product -shards 2 -replicationFactor 2 -force
代码语言:javascript
复制

  1. 配置 Java 客户端连接 SolrCloud:
代码语言:javascript
复制
@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();
}
代码语言:javascript
复制

六、Solr 性能优化

6.1 索引优化

合理设计字段:

  • 不需要搜索的字段设置indexed="false"
  • 不需要存储的字段设置stored="false"
  • 合理使用复制字段减少查询复杂度

优化分词器:

  • 根据业务场景选择合适的分词器
  • 避免过度分词导致索引膨胀

批量索引:

  • 使用批量添加接口减少网络开销
  • 合理设置提交间隔

索引优化参数:

代码语言:javascript
复制
<indexConfig>
  <useCompoundFile>false</useCompoundFile>
  <mergeFactor>10</mergeFactor>
  <ramBufferSizeMB>128</ramBufferSizeMB>
  <maxBufferedDocs>10000</maxBufferedDocs>
</indexConfig>
代码语言:javascript
复制

6.2 查询优化

使用过滤查询:

代码语言:javascript
复制
curl "http://localhost:8983/solr/product/select?q=手机&fq=category:手机&fq=price:[5000 TO 10000]&wt=json"
代码语言:javascript
复制

  • 对于频繁使用的过滤条件,使用fq参数,Solr 会缓存过滤结果

限制返回字段:

代码语言:javascript
复制
curl "http://localhost:8983/solr/product/select?q=*:*&fl=id,product_name,price&wt=json"
代码语言:javascript
复制

  • 使用fl参数只返回需要的字段

合理设置缓存:

代码语言:javascript
复制
<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"/>
代码语言:javascript
复制

避免通配符前缀查询:

  • 避免使用*phone这样的查询,会导致性能问题

6.3 硬件优化

内存配置:

  • 为 Solr 分配足够的内存(至少 4GB)
  • 修改bin/solr.in.sh中的SOLR_HEAP="4g"

磁盘选择:

  • 使用 SSD 提高索引和查询性能
  • 确保有足够的磁盘空间

JVM 优化:

代码语言:javascript
复制
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:ParallelGCThreads=8
-XX:ConcGCThreads=2
-XX:InitiatingHeapOccupancyPercent=70
代码语言:javascript
复制

七、常见问题与解决方案

7.1 索引与数据库数据不一致

原因:商品数据在数据库更新后,未及时同步到 Solr。

解决方案:

  1. 实现数据库变更监听(如使用 Canal 监听 MySQL binlog)
  2. 定时全量同步 + 增量同步结合
  3. 在业务代码中,更新数据库后立即更新 Solr
代码语言:javascript
复制
/**
 * 商品服务中同步更新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);
    }
}
代码语言:javascript
复制

7.2 搜索结果相关性不高

原因:

  1. 分词器配置不当
  2. 没有合理使用复制字段
  3. 评分规则不符合业务需求

解决方案:

  1. 优化分词器配置,添加自定义词典
  2. 合理设计复制字段,确保相关字段都参与搜索
  3. 自定义评分函数,提高重要字段的权重

7.3 大数量索引时性能下降

原因:

  1. 批量提交设置不合理
  2. 内存配置不足
  3. 索引合并策略不当

解决方案:

  1. 增大批量提交的文档数量
  2. 增加 Solr 的堆内存
  3. 优化索引合并参数
代码语言:javascript
复制
<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>
代码语言:javascript
复制

八、总结

希望本文能帮助你快速掌握 Solr 的核心技术,并应用到实际项目中。如果你有任何问题或建议,欢迎留言讨论。

附录:参考资料

  1. Apache Solr 官方文档:https://solr.apache.org/guide/solr/latest/
  2. 《Solr 实战》(Trey Grainger 等著)
  3. 《Apache Solr 企业搜索引擎开发》(刘刚等著)
  4. IK 分词器项目:https://github.com/magese/ik-analyzer-solr
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-10-12,如有侵权请联系 cloudcommunity@tencent.com 删除
目录
  • 引言:为什么 Solr 是企业级搜索的首选?
  • 一、Solr 核心概念与工作原理
    • 1.1 什么是 Solr?
    • 1.2 Solr 与 Lucene 的关系
    • 1.3 Solr 的核心组件
    • 1.4 倒排索引:Solr 快速搜索的秘密
  • 二、Solr 环境搭建与配置
    • 2.1 环境准备
    • 2.2 安装 Solr
    • 2.3 创建 Solr Core
    • 2.4 Solr 目录结构解析
    • 2.5 配置 managed-schema
    • 2.6 配置中文分词器
  • 三、Solr 基本操作:索引与搜索
    • 3.1 索引操作详解
      • 3.1.1 添加文档
      • 3.1.2 更新文档
      • 3.1.3 删除文档
      • 3.1.4 清空索引
    • 3.2 搜索操作详解
      • 3.2.1 基本搜索
      • 3.2.2 排序
      • 3.2.3 分页
      • 3.2.4 字段过滤
      • 3.2.5 高亮显示
      • 3.2.6 分面搜索(Facet)
  • 四、Java 集成 Solr 实战
    • 4.1 项目搭建
      • 4.1.1 创建 Maven 项目
      • 4.1.2 配置文件
      • 4.1.3 Solr 配置类
    • 4.2 实体类定义
      • 4.2.1 商品实体类
      • 4.2.2 搜索请求参数类
      • 4.2.3 搜索结果类
    • 4.3 数据访问层
      • 4.3.1 数据库 DAO 层
      • 4.3.2 Solr DAO 层
    • 4.4 服务层
    • 4.5 控制器
    • 4.6 启动类
    • 4.7 数据库脚本
  • 五、Solr 高级功能
    • 5.1 同义词处理
    • 5.2 拼写检查
    • 5.3 相关性排序
    • 5.4 分布式部署
      • 5.4.1 SolrCloud 架构
      • 5.4.2 部署步骤
  • 六、Solr 性能优化
    • 6.1 索引优化
    • 6.2 查询优化
    • 6.3 硬件优化
  • 七、常见问题与解决方案
    • 7.1 索引与数据库数据不一致
    • 7.2 搜索结果相关性不高
    • 7.3 大数量索引时性能下降
  • 八、总结
  • 附录:参考资料
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档