Solr 概述

什么是 Solr

Apache Solr 是一个基于 Lucene 的企业级搜索服务器提供了全文搜索命中高亮分面搜索动态聚类数据库集成等功能

  • 核心功能
    • 全文搜索支持复杂的全文检索
    • 分面搜索多维度分类统计
    • 高亮显示搜索结果关键词高亮
    • 拼写检查自动纠正拼写错误
    • 更多类似推荐相似文档
  • 主要优势
    • 成熟稳定经过多年企业级应用验证
    • 功能丰富内置多种搜索功能
    • 易于扩展支持插件机制
    • 管理界面提供 Web 管理控制台

Solr vs Elasticsearch

特性 Solr Elasticsearch
实时性 近实时NRT 近实时NRT
分布式 支持SolrCloud 原生支持
查询语言 丰富的查询语法 DSLJSON
适用场景 传统搜索静态数据 日志分析实时数据
社区活跃度 较低 非常高
学习曲线 较陡 相对平缓
1
2
3
4
细节注意
1. Solr 适合传统的搜索场景如电商商品搜索
2. Elasticsearch 更适合日志分析实时监控等场景
3. 两者都基于 Lucene核心原理相似

Solr 的核心概念

基本概念

  • Core核心
    • 相当于关系数据库中的数据库
    • 包含索引和配置文件
  • Document文档
    • 最小的数据单元
    • 由多个 Field 组成
  • Field字段
    • 文档中的属性
    • 有类型和属性定义
  • Schema模式
    • 定义字段类型和属性
    • 相当于数据库的表结构
  • Index索引
    • 倒排索引结构
    • 用于快速检索

架构组件

  • SolrCloud
    • 分布式集群模式
    • 使用 ZooKeeper 协调
  • ZooKeeper
    • 分布式协调服务
    • 管理集群配置和状态
  • Shard分片
    • 索引的水平分割
    • 提高性能和可扩展性
  • Replica副本
    • 分片的复制
    • 提供高可用性

Solr 的工作原理

索引流程

1
2
3
4
5
1. 客户端发送文档到 Solr
2. Solr 解析文档并提取字段
3. 对文本字段进行分词处理
4. 构建倒排索引
5. 提交索引到磁盘

搜索流程

1
2
3
4
5
1. 客户端发送查询请求
2. Solr 解析查询语句
3. 在倒排索引中查找匹配的文档
4. 计算相关度评分
5. 排序并返回结果

环境搭建

安装 Solr

Docker 安装推荐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 拉取镜像
docker pull solr:8.11.0

# 运行容器
docker run -d \
--name solr \
-p 8983:8983 \
solr:8.11.0

# 创建 Core
docker exec -it solr solr create_core -c mycore

# 访问 Solr Admin
# http://localhost:8983

Linux 安装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 下载 Solr
wget https://archive.apache.org/dist/lucene/solr/8.11.0/solr-8.11.0.tgz

# 解压
tar -xzf solr-8.11.0.tgz
cd solr-8.11.0

# 启动
./bin/solr start

# 创建 Core
./bin/solr create -c mycore

# 停止
./bin/solr stop -all

Windows 安装

下载地址https://lucene.apache.org/solr/downloads.html

步骤

  1. 下载压缩包并解压
  2. 进入 bin 目录
  3. 运行 solr.cmd start
  4. 创建 Coresolr.cmd create -c mycore

macOS 安装

1
2
3
4
5
6
7
8
# 使用 Homebrew
brew install solr

# 启动
brew services start solr

# 创建 Core
solr create -c mycore

目录结构

1
2
3
4
5
6
7
8
9
10
11
12
solr-8.11.0/
├── bin/ # 可执行脚本
├── server/ # 服务器文件
│ ├── solr/ # Solr 主目录
│ │ └── mycore/ # Core 目录
│ │ ├── conf/ # 配置文件
│ │ │ ├── solrconfig.xml
│ │ │ └── schema.xml
│ │ └── data/ # 索引数据
│ └── logs/ # 日志文件
├── example/ # 示例文件
└── docs/ # 文档

验证安装

1
2
3
4
5
6
7
8
# 检查 Solr 状态
curl http://localhost:8983/solr/admin/info/system?wt=json

# 查看 Core 列表
curl http://localhost:8983/solr/admin/cores?action=STATUS&wt=json

# 访问 Web 界面
# http://localhost:8983

基本操作

Core 操作

创建 Core

1
2
3
4
5
6
7
8
# 命令行创建
solr create -c products

# API 创建
curl "http://localhost:8983/solr/admin/cores?action=CREATE&name=products&instanceDir=products"

# 带配置创建
solr create -c products -d basic_configs

查看 Core

1
2
3
4
5
# 查看所有 Core
curl http://localhost:8983/solr/admin/cores?action=STATUS&wt=json

# 查看指定 Core
curl http://localhost:8983/solr/products/admin/ping?wt=json

删除 Core

1
2
3
4
5
# 命令行删除
solr delete -c products

# API 删除
curl "http://localhost:8983/solr/admin/cores?action=UNLOAD&core=products&deleteIndex=true&deleteDataDir=true&deleteInstanceDir=true"

重载 Core

1
curl "http://localhost:8983/solr/admin/cores?action=RELOAD&core=products"

文档操作

添加文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 单个文档
curl -X POST "http://localhost:8983/solr/products/update?commit=true" \
-H 'Content-Type: application/json' \
-d '[
{
"id": "1",
"name": "iPhone 13",
"price": 5999,
"category": "手机",
"brand": "Apple",
"description": "苹果手机A15芯片"
}
]'

# 批量文档
curl -X POST "http://localhost:8983/solr/products/update?commit=true" \
-H 'Content-Type: application/json' \
-d '[
{"id": "2", "name": "华为Mate40", "price": 4999, "category": "手机", "brand": "华为"},
{"id": "3", "name": "小米11", "price": 3999, "category": "手机", "brand": "小米"}
]'

# XML 格式
curl -X POST "http://localhost:8983/solr/products/update?commit=true" \
-H 'Content-Type: text/xml' \
-d '<add>
<doc>
<field name="id">4</field>
<field name="name">OPPO Find X3</field>
<field name="price">4499</field>
</doc>
</add>'

查询文档

1
2
3
4
5
6
7
8
9
10
11
# 查询所有
curl "http://localhost:8983/solr/products/select?q=*:*&wt=json"

# 条件查询
curl "http://localhost:8983/solr/products/select?q=name:iPhone&wt=json"

# 分页查询
curl "http://localhost:8983/solr/products/select?q=*:*&start=0&rows=10&wt=json"

# 排序查询
curl "http://localhost:8983/solr/products/select?q=*:*&sort=price desc&wt=json"

更新文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 全量更新覆盖
curl -X POST "http://localhost:8983/solr/products/update?commit=true" \
-H 'Content-Type: application/json' \
-d '[
{
"id": "1",
"name": "iPhone 13 Pro",
"price": 7999
}
]'

# 部分更新
curl -X POST "http://localhost:8983/solr/products/update?commit=true" \
-H 'Content-Type: application/json' \
-d '[
{
"id": "1",
"price": {"set": 6999}
}
]'

# 原子更新操作
# set: 设置值
# inc: 增加
# dec: 减少
curl -X POST "http://localhost:8983/solr/products/update?commit=true" \
-H 'Content-Type: application/json' \
-d '[
{
"id": "1",
"price": {"inc": 100}
}
]'

删除文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 根据 ID 删除
curl -X POST "http://localhost:8983/solr/products/update?commit=true" \
-H 'Content-Type: application/json' \
-d '{"delete": {"id": "1"}}'

# 根据条件删除
curl -X POST "http://localhost:8983/solr/products/update?commit=true" \
-H 'Content-Type: application/json' \
-d '{"delete": {"query": "brand:Apple"}}'

# 删除所有
curl -X POST "http://localhost:8983/solr/products/update?commit=true" \
-H 'Content-Type: application/json' \
-d '{"delete": {"query": "*:*"}}'

Schema 设计

字段类型

类型 说明 示例
string 字符串不分词 "ABC123"
text_general 文本会分词 "Hello World"
tint 整数 42
tlong 长整数 9223372036854775807
tfloat 浮点数 3.14
tdouble 双精度 3.1415926
tdate 日期 "2024-01-01T00:00:00Z"
boolean 布尔值 true/false
location 地理位置 "39.9042,116.4074"

配置 Schema

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<!-- schema.xml 或 managed-schema -->
<schema name="products" version="1.6">

<!-- 唯一键 -->
<uniqueKey>id</uniqueKey>

<!-- 字段定义 -->
<fields>
<field name="id" type="string" indexed="true" stored="true" required="true"/>
<field name="name" type="text_ik" indexed="true" stored="true"/>
<field name="price" type="tint" indexed="true" stored="true"/>
<field name="category" type="string" indexed="true" stored="true"/>
<field name="brand" type="string" indexed="true" stored="true"/>
<field name="description" type="text_ik" indexed="true" stored="false"/>
<field name="create_time" type="tdate" indexed="true" stored="true"/>
</fields>

<!-- 复制字段用于多字段搜索 -->
<copyField source="name" dest="text"/>
<copyField source="description" dest="text"/>

<!-- 动态字段 -->
<dynamicField name="*_s" type="string" indexed="true" stored="true"/>
<dynamicField name="*_i" type="tint" indexed="true" stored="true"/>
<dynamicField name="*_t" type="text_ik" indexed="true" stored="true"/>

</schema>

字段属性

属性 说明 默认值
indexed 是否建立索引 false
stored 是否存储原始值 false
required 是否必填 false
multiValued 是否多值 false
docValues 是否启用列缓存 false

查询语法

基本查询

简单查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 查询所有
q=*:*

# 精确匹配
q=name:iPhone

# 短语查询
q=name:"iPhone 13"

# 通配符查询
q=name:iPh*
q=name:?Phone

# 范围查询
q=price:[1000 TO 5000]
q=price:{1000 TO 5000} # 不包含边界

布尔查询

1
2
3
4
5
6
7
8
9
10
11
# AND
q=name:iPhone AND brand:Apple

# OR
q=name:iPhone OR name:Huawei

# NOT
q=name:iPhone NOT brand:Samsung

# 组合查询
q=(name:iPhone OR name:Huawei) AND price:[3000 TO 8000]

字段查询

1
2
3
4
5
6
7
8
9
10
11
# 单字段
q=name:iPhone

# 多字段
q=(name:iPhone OR description:苹果)

# 所有字段
q=iPhone

# 排除字段
q=-brand:Samsung

高级查询

模糊查询

1
2
3
4
5
6
# 编辑距离
q=name:iphon~1
q=name:iphon~2

# 相似度0-1
q=name:"iPhone 13"~0.8

正则查询

1
2
q=name:/i[ph]+one/
q=email:/.*@example\.com/

邻近查询

1
2
# 词语之间的距离
q=name:"iPhone 13"~5

提升权重

1
2
# 提升某个字段的权重
q=name:iPhone^2 OR description:苹果^0.5

特殊查询

存在查询

1
2
3
4
5
# 字段存在
q=_exists_:price

# 字段不存在
q=-_exists_:discount

分组查询

1
2
# 按字段分组
facet=true&facet.field=brand&facet.field=category

查询参数

常用参数

参数 说明 示例
q 查询语句 q=name:iPhone
fq 过滤查询 fq=price:[1000 TO 5000]
sort 排序 sort=price desc
start 起始位置 start=0
rows 返回条数 rows=10
fl 返回字段 fl=id,name,price
df 默认字段 df=name
wt 响应格式 wt=json

完整查询示例

1
2
3
4
5
6
7
8
curl "http://localhost:8983/solr/products/select?\
q=name:iPhone&\
fq=price:[3000 TO 8000]&\
sort=price desc&\
start=0&\
rows=10&\
fl=id,name,price,brand&\
wt=json"

高亮显示

1
2
3
4
5
6
7
curl "http://localhost:8983/solr/products/select?\
q=name:iPhone&\
hl=true&\
hl.fl=name,description&\
hl.simple.pre=<em>&\
hl.simple.post=</em>&\
wt=json"

分面搜索

1
2
3
4
5
6
7
8
9
10
curl "http://localhost:8983/solr/products/select?\
q=*:*&\
facet=true&\
facet.field=brand&\
facet.field=category&\
facet.range=price&\
f.price.facet.range.start=0&\
f.price.facet.range.end=10000&\
f.price.facet.range.gap=1000&\
wt=json"

Spring Boot 整合

添加依赖

1
2
3
4
5
<!-- Spring Data Solr -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-solr</artifactId>
</dependency>

配置文件

1
2
3
4
5
# application.yml
spring:
data:
solr:
host: http://localhost:8983/solr

创建实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.example.entity;

import lombok.Data;
import org.apache.solr.client.solrj.beans.Field;
import org.springframework.data.solr.core.mapping.SolrDocument;

import java.math.BigDecimal;
import java.time.LocalDateTime;

@Data
@SolrDocument(solrCoreName = "products")
public class Product {

@Field
private String id;

@Field
private String name;

@Field
private BigDecimal price;

@Field
private String category;

@Field
private String brand;

@Field
private String description;

@Field
private LocalDateTime createTime;
}

创建 Repository

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.example.repository;

import com.example.entity.Product;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.solr.repository.Query;
import org.springframework.data.solr.repository.SolrCrudRepository;
import org.springframework.stereotype.Repository;

import java.math.BigDecimal;
import java.util.List;

@Repository
public interface ProductRepository extends SolrCrudRepository<Product, String> {

// 方法名查询
List<Product> findByName(String name);

Page<Product> findByCategory(String category, Pageable pageable);

List<Product> findByPriceBetween(BigDecimal minPrice, BigDecimal maxPrice);

// 自定义查询
@Query("name:?0 AND brand:?1")
List<Product> searchByNameAndBrand(String name, String brand);

// 复杂查询
@Query("name:?0 OR description:?0")
Page<Product> searchByKeyword(String keyword, Pageable pageable);
}

创建 Service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
package com.example.service;

import com.example.entity.Product;
import com.example.repository.ProductRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.solr.core.SolrTemplate;
import org.springframework.data.solr.core.query.Criteria;
import org.springframework.data.solr.core.query.SimpleQuery;
import org.springframework.data.solr.core.query.result.FacetPage;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.util.List;

@Service
public class ProductService {

@Autowired
private ProductRepository productRepository;

@Autowired
private SolrTemplate solrTemplate;

// 保存文档
public Product save(Product product) {
return productRepository.save(product);
}

// 批量保存
public Iterable<Product> saveAll(List<Product> products) {
return productRepository.saveAll(products);
}

// 根据 ID 查询
public Product findById(String id) {
return productRepository.findById(id).orElse(null);
}

// 删除文档
public void delete(String id) {
productRepository.deleteById(id);
}

// 分页查询
public Page<Product> page(int pageNum, int pageSize) {
return productRepository.findAll(PageRequest.of(pageNum - 1, pageSize));
}

// 条件查询
public List<Product> search(String keyword) {
Criteria criteria = new Criteria("name").contains(keyword)
.or(new Criteria("description").contains(keyword));

SimpleQuery query = new SimpleQuery(criteria);
return solrTemplate.queryForPage(query, Product.class).getContent();
}

// 价格范围查询
public List<Product> findByPriceRange(BigDecimal minPrice, BigDecimal maxPrice) {
return productRepository.findByPriceBetween(minPrice, maxPrice);
}

// 分面查询
public FacetPage<Product> facetByCategory() {
SimpleQuery query = new SimpleQuery(new Criteria("*:*"));
query.addFacetOnField("category");
return solrTemplate.queryForFacetPage(query, Product.class);
}
}

创建 Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package com.example.controller;

import com.example.entity.Product;
import com.example.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.web.bind.annotation.*;

import java.math.BigDecimal;
import java.util.List;

@RestController
@RequestMapping("/products")
public class ProductController {

@Autowired
private ProductService productService;

@PostMapping
public Product create(@RequestBody Product product) {
return productService.save(product);
}

@PostMapping("/batch")
public Iterable<Product> batchCreate(@RequestBody List<Product> products) {
return productService.saveAll(products);
}

@GetMapping("/{id}")
public Product get(@PathVariable String id) {
return productService.findById(id);
}

@DeleteMapping("/{id}")
public void delete(@PathVariable String id) {
productService.delete(id);
}

@GetMapping("/page")
public Page<Product> page(
@RequestParam(defaultValue = "1") int pageNum,
@RequestParam(defaultValue = "10") int pageSize) {
return productService.page(pageNum, pageSize);
}

@GetMapping("/search")
public List<Product> search(@RequestParam String keyword) {
return productService.search(keyword);
}

@GetMapping("/price")
public List<Product> findByPrice(
@RequestParam BigDecimal minPrice,
@RequestParam BigDecimal maxPrice) {
return productService.findByPriceRange(minPrice, maxPrice);
}
}

高级功能

中文分词IK Analyzer

安装 IK 分词器

1
2
3
4
5
6
# 1. 下载 IK 分词器
# https://github.com/magese/ik-analyzer-solr

# 2. 将 jar 包复制到 server/solr-webapp/webapp/WEB-INF/lib/

# 3. 在 schema.xml 中配置
1
2
3
4
5
6
7
8
9
10
11
12
<!-- schema.xml -->
<fieldType name="text_ik" class="solr.TextField">
<analyzer type="index">
<tokenizer class="org.wltea.analyzer.lucene.IKTokenizerFactory" useSmart="false"/>
</analyzer>
<analyzer type="query">
<tokenizer class="org.wltea.analyzer.lucene.IKTokenizerFactory" useSmart="true"/>
</analyzer>
</fieldType>

<field name="name" type="text_ik" indexed="true" stored="true"/>
<field name="description" type="text_ik" indexed="true" stored="true"/>

拼音分词

1
2
3
4
5
6
7
8
9
10
11
<!-- 需要安装 pinyin4j -->
<fieldType name="text_pinyin" class="solr.TextField">
<analyzer type="index">
<tokenizer class="org.wltea.analyzer.lucene.IKTokenizerFactory"/>
<filter class="com.github.magese.pinyin.PinyinTokenFilterFactory"/>
</analyzer>
<analyzer type="query">
<tokenizer class="org.wltea.analyzer.lucene.IKTokenizerFactory"/>
<filter class="com.github.magese.pinyin.PinyinTokenFilterFactory"/>
</analyzer>
</fieldType>

拼写检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<!-- solrconfig.xml -->
<searchComponent name="spellcheck" class="solr.SpellCheckComponent">
<str name="queryAnalyzerFieldType">text_ik</str>
<lst name="spellchecker">
<str name="name">default</str>
<str name="field">name</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>

<requestHandler name="/spell" class="solr.SearchHandler" startup="lazy">
<lst name="defaults">
<str name="df">name</str>
<str name="spellcheck">true</str>
<str name="spellcheck.extendedResults">true</str>
<str name="spellcheck.count">10</str>
<str name="spellcheck.alternativeTermCount">5</str>
<str name="spellcheck.maxResultsForSuggest">5</str>
<str name="spellcheck.collate">true</str>
<str name="spellcheck.collateExtendedResults">true</str>
<str name="spellcheck.maxCollationTries">10</str>
<str name="spellcheck.maxCollations">5</str>
</lst>
<arr name="last-components">
<str>spellcheck</str>
</arr>
</requestHandler>
1
2
# 使用拼写检查
curl "http://localhost:8983/solr/products/spell?q=iphon&wt=json"

更多类似

1
2
3
4
5
6
7
# 查找相似文档
curl "http://localhost:8983/solr/products/mlt?\
q=id:1&\
mlt.fl=name,description&\
mlt.mindf=1&\
mlt.mintf=1&\
wt=json"

地理位置搜索

1
2
<!-- schema.xml -->
<field name="location" type="location" indexed="true" stored="true"/>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 添加位置数据
curl -X POST "http://localhost:8983/solr/products/update?commit=true" \
-H 'Content-Type: application/json' \
-d '[
{
"id": "100",
"name": "北京店",
"location": "39.9042,116.4074"
}
]'

# 附近搜索
curl "http://localhost:8983/solr/products/select?\
q=*:*&\
fq={!geofilt pt=39.9042,116.4074 sfield=location d=10}&\
wt=json"

性能优化

索引优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- solrconfig.xml -->
<!-- 1. 调整提交策略 -->
<autoCommit>
<maxTime>15000</maxTime>
<openSearcher>false</openSearcher>
</autoCommit>

<autoSoftCommit>
<maxTime>1000</maxTime>
</autoSoftCommit>

<!-- 2. 合并段文件 -->
<mergePolicy class="org.apache.lucene.index.TieredMergePolicy">
<int name="maxMergeAtOnce">10</int>
<int name="segmentsPerTier">10</int>
</mergePolicy>

查询优化

1
2
3
4
5
6
7
8
9
10
11
12
13
// 1. 使用过滤器缓存
Criteria criteria = new Criteria("category").is("手机");
SimpleQuery query = new SimpleQuery(criteria);
query.addFilterQuery(new SimpleQuery(
new Criteria("price").between(1000, 5000)
));

// 2. 限制返回字段
query.setFields("id", "name", "price");

// 3. 合理分页
query.setOffset(0);
query.setRows(10);

缓存配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- solrconfig.xml -->
<cache name="filterCache"
class="solr.FastLRUCache"
size="512"
initialSize="512"
autowarmCount="0"/>

<cache name="queryResultCache"
class="solr.LRUCache"
size="512"
initialSize="512"
autowarmCount="0"/>

<cache name="documentCache"
class="solr.LRUCache"
size="512"
initialSize="512"
autowarmCount="0"/>

批量导入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 使用 SolrJ 批量导入
SolrClient client = new HttpSolrClient.Builder("http://localhost:8983/solr/products").build();

List<SolrInputDocument> docs = new ArrayList<>();
for (Product product : products) {
SolrInputDocument doc = new SolrInputDocument();
doc.addField("id", product.getId());
doc.addField("name", product.getName());
doc.addField("price", product.getPrice());
docs.add(doc);
}

client.add(docs);
client.commit();

最佳实践

命名规范

1
2
3
4
5
6
7
8
9
10
Core 命名
- 小写字母
- 使用下划线或连字符
- 语义化命名
- 示例product_indexuser_search

字段命名
- 使用驼峰命名
- 类型后缀_s(string)_i(int)_t(text)
- 示例productNameprice_idescription_t

Schema 设计建议

1
2
3
4
5
6
7
8
9
10
11
<!-- 1. 合理使用字段类型 -->
<field name="id" type="string" indexed="true" stored="true"/>
<field name="name" type="text_ik" indexed="true" stored="true"/>
<field name="price" type="tint" indexed="true" stored="true" docValues="true"/>

<!-- 2. 使用复制字段 -->
<copyField source="name" dest="text"/>
<copyField source="description" dest="text"/>

<!-- 3. 使用动态字段 -->
<dynamicField name="*_s" type="string" indexed="true" stored="true"/>

查询优化建议

1
2
3
4
5
6
7
8
9
10
11
// 1. 使用 filter 代替 query可缓存
SimpleQuery query = new SimpleQuery(new Criteria("*:*"));
query.addFilterQuery(new SimpleQuery(new Criteria("status").is("active")));

// 2. 避免深分页
// 使用 cursorMark 代替 start/rows
CursorMark cursorMark = new CursorMark("*");
query.setCursorMark(cursorMark);

// 3. 限制返回字段
query.setFields("id", "name");

监控建议

1
2
3
4
5
6
7
8
9
10
11
# 1. 监控 Core 状态
curl http://localhost:8983/solr/admin/cores?action=STATUS&wt=json

# 2. 监控查询性能
curl http://localhost:8983/solr/admin/metrics?wt=json

# 3. 查看日志
tail -f server/logs/solr.log

# 4. 使用 Solr Admin UI
# 访问 http://localhost:8983

常见问题

中文分词问题

1
2
3
4
5
6
7
问题中文无法正确分词

解决方案
1. 安装 IK 分词器
2. 配置 fieldType 使用 IK
3. 重启 Solr
4. 重新索引数据

内存溢出

1
2
3
4
5
6
7
8
问题OutOfMemoryError

解决方案
1. 增加 JVM 堆内存
SOLR_JAVA_MEM="-Xms2g -Xmx4g"
2. 优化查询减少返回数据量
3. 调整缓存大小
4. 定期清理旧数据

索引速度慢

1
2
3
4
5
6
7
问题批量导入速度慢

解决方案
1. 使用批量提交
2. 关闭自动提交
3. 并行导入
4. 优化 Schema减少不必要的字段
1
2
3
4
5
6
7
8
9
# 批量导入优化
curl -X POST "http://localhost:8983/solr/products/update?commit=false" \
-H 'Content-Type: application/json' \
-d @large_batch.json

# 最后统一提交
curl -X POST "http://localhost:8983/solr/products/update?commit=true" \
-H 'Content-Type: application/json' \
-d '{"commit": {}}'

报错处理

💗💗 Solr 报错Unknown field

1
2
3
4
5
6
7
8
9
10
错误信息
Unknown field 'xxx'

错误原因
字段未在 schema.xml 中定义

解决方案
1. 在 schema.xml 中添加字段定义
2. 使用动态字段xxx_s
3. 重载 Corecurl "http://localhost:8983/solr/admin/cores?action=RELOAD&core=mycore"

💗💗 Solr 报错Document is missing mandatory uniqueKey field

1
2
3
4
5
6
7
8
9
10
错误信息
Document is missing mandatory uniqueKey field: id

错误原因
缺少唯一键字段

解决方案
1. 确保每个文档都有 id 字段
2. 检查 uniqueKey 配置是否正确
3. 使用自动生成 ID

💗💗 Solr 报错Too many clauses

1
2
3
4
5
6
7
8
9
10
11
错误信息
too many Boolean clauses

错误原因
查询条件过多

解决方案
1. 优化查询减少条件数量
2. 增加 maxBooleanClauses 限制
<maxBooleanClauses>2048</maxBooleanClauses>
3. 使用 filter 代替 query

学习资源

  • 视频
    • 黑马程序员Solr从基础到实战https://www.bilibili.com/video/BV1dh411Q7Qu
  • 官方文档
    • Solr 官方文档https://solr.apache.org/guide/
    • Solr GitHubhttps://github.com/apache/solr
  • 书籍
    • Solr 实战Erik Hatcher 著
    • Apache Solr EssentialsGaurav Chopra 著
  • 教程
    • Solr 入门教程https://www.runoob.com/w3cnote/solr-tutorial.html
    • Solr 官方示例https://solr.apache.org/guide/solr/latest/getting-started/solr-tutorial.html
  • 工具
    • Solr Admin UIWeb 管理界面
    • PostmanAPI 测试工具
    • SolrJJava 客户端库
  • 社区
    • Solr 用户邮件列表https://solr.apache.org/community.html
    • Stack Overflow Solr 标签https://stackoverflow.com/questions/tagged/solr