下面给出一段 可直接跑通 的 Lucene 8.5.0 示例代码,演示如何
1. 建索引时为两个字段启用 DocValues(一个 `NumericDocValues`,一个 `SortedDocValues`);
2. 用 `IndexSearcher` 按 DocValues 排序;
3. 用 `FacetsCollector` 做分组统计(相当于 SQL 的 `GROUP BY`)。
```java
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.codecs.lucene50.Lucene50Codec;
import org.apache.lucene.document.*;
import org.apache.lucene.index.*;
import org.apache.lucene.search.*;
import org.apache.lucene.store.ByteBuffersDirectory;
import org.apache.lucene.util.NumericUtils;
import org.apache.lucene.facet.*;
import org.apache.lucene.facet.sortedset.SortedSetDocValuesFacetCounts;
import org.apache.lucene.facet.sortedset.SortedSetDocValuesFacetField;
public class DocValuesDemo {
public static void main(String[] args) throws Exception {
/* ---------- 1. 创建内存目录 ---------- */
Directory dir = new ByteBuffersDirectory();
IndexWriterConfig cfg = new IndexWriterConfig(new StandardAnalyzer());
// 使用默认 codec 即可,DocValues 默认开启
cfg.setCodec(new Lucene50Codec());
IndexWriter writer = new IndexWriter(dir, cfg);
/* ---------- 2. 写入三条示范文档 ---------- */
Document doc1 = new Document();
doc1.add(new StringField("id", "1", Field.Store.YES));
// 商品价:NumericDocValues,可排序、可聚合
doc1.add(new NumericDocValuesField("price", 2999));
// 商品品牌:SortedDocValues,可做 faceting
doc1.add(new SortedSetDocValuesFacetField("brand", "小米"));
Document doc2 = new Document();
doc2.add(new StringField("id", "2", Field.Store.YES));
doc2.add(new NumericDocValuesField("price", 3999));
doc2.add(new SortedSetDocValuesFacetField("brand", "苹果"));
Document doc3 = new Document();
doc3.add(new StringField("id", "3", Field.Store.YES));
doc3.add(new NumericDocValuesField("price", 1999));
doc3.add(new SortedSetDocValuesFacetField("brand", "小米"));
writer.addDocument(doc1);
writer.addDocument(doc2);
writer.addDocument(doc3);
writer.commit();
writer.close();
/* ---------- 3. 打开搜索器 ---------- */
DirectoryReader reader = DirectoryReader.open(dir);
IndexSearcher searcher = new IndexSearcher(reader);
/* ---------- 4. 按价格升序排序 ---------- */
Sort sort = new Sort(new SortField("price", SortField.Type.LONG));
TopDocs topDocs = searcher.search(new MatchAllDocsQuery(), 10, sort);
for (ScoreDoc sd : topDocs.scoreDocs) {
Document d = searcher.doc(sd.doc);
System.out.println("id=" + d.get("id"));
}
// 输出:
// id=3 (1999)
// id=1 (2999)
// id=2 (3999)
/* ---------- 5. 用 Facets 统计品牌分布 ---------- */
FacetsCollector fc = new FacetsCollector();
searcher.search(new MatchAllDocsQuery(), fc);
Facets facets = new SortedSetDocValuesFacetCounts(fc);
FacetResult result = facets.getTopChildren(10, "brand");
for (LabelAndValue lav : result.labelValues) {
System.out.println(lav.label + " : " + lav.value);
}
// 输出:
// 小米 : 2
// 苹果 : 1
reader.close();
dir.close();
}
}
```
---
关键 API 回顾
目标 用到的 Lucene 类/方法
声明数值 DocValues `NumericDocValuesField`
声明字符串 DocValues `SortedSetDocValuesFacetField`
排序 `SortField("price", Type.LONG)`
分组/聚合 `FacetsCollector + SortedSetDocValuesFacetCounts`
这样就完整展示了 在 Lucene 8.5.0 中如何启用、读取并利用 DocValues 进行排序和聚合 的完整链路。
下面给出一份 可直接拷贝到 IDE 运行的 Lucene 8.5.0 完整示例,演示如何
1. 索引阶段:
- 用 NumericDocValues 存价格(可排序/聚合)
- 用 SortedSetDocValuesFacetField 存品牌(可做分面统计)
2. 查询阶段:
- 按价格排序
- 统计品牌分布
- 钻取(drill-down)过滤后再统计
---
✅ Maven 依赖(lucene-core + lucene-facet)
```xml
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-core</artifactId>
<version>8.5.0</version>
</dependency>
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-facet</artifactId>
<version>8.5.0</version>
</dependency>
```
---
✅ 代码示例
```java
package demo;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.*;
import org.apache.lucene.facet.*;
import org.apache.lucene.facet.sortedset.*;
import org.apache.lucene.index.*;
import org.apache.lucene.search.*;
import org.apache.lucene.store.ByteBuffersDirectory;
import org.apache.lucene.store.Directory;
import java.util.List;
public class DocValuesDemo {
public static void main(String[] args) throws Exception {
Directory dir = new ByteBuffersDirectory();
IndexWriterConfig cfg = new IndexWriterConfig(new StandardAnalyzer());
IndexWriter writer = new IndexWriter(dir, cfg);
FacetsConfig config = new FacetsConfig(); // 必须
// 准备 3 条测试数据
addDoc(writer, config, "1", 1999, "小米");
addDoc(writer, config, "2", 3999, "苹果");
addDoc(writer, config, "3", 2999, "小米");
writer.commit();
writer.close();
/* ---------- 查询 ---------- */
DirectoryReader reader = DirectoryReader.open(dir);
IndexSearcher searcher = new IndexSearcher(reader);
/* 1. 按价格排序(NumericDocValues) */
Sort sort = new Sort(new SortField("price", SortField.Type.LONG));
TopDocs topDocs = searcher.search(new MatchAllDocsQuery(), 10, sort);
System.out.println("按价格排序:");
for (ScoreDoc sd : topDocs.scoreDocs) {
Document doc = searcher.doc(sd.doc);
System.out.println("id=" + doc.get("id") +
", 价格=" + doc.get("price") +
", 品牌=" + doc.get("brand"));
}
/* 2. 品牌分面统计(SortedSetDocValuesFacetField) */
SortedSetDocValuesReaderState state =
new DefaultSortedSetDocValuesReaderState(reader);
FacetsCollector fc = new FacetsCollector();
FacetsCollector.search(searcher, new MatchAllDocsQuery(), 10, fc);
Facets facets = new SortedSetDocValuesFacetCounts(state, fc);
FacetResult brandResult = facets.getTopChildren(10, "brand");
System.out.println("\n品牌统计:");
for (LabelAndValue lv : brandResult.labelValues) {
System.out.println(lv.label + " : " + lv.value);
}
/* 3. 钻取:只看 2010 年出版的书(示例维度) */
DrillDownQuery dq = new DrillDownQuery(config);
dq.add("brand", "小米"); // 钻取小米
FacetsCollector fc2 = new FacetsCollector();
FacetsCollector.search(searcher, dq, 10, fc2);
Facets facets2 = new SortedSetDocValuesFacetCounts(state, fc2);
FacetResult afterDrill = facets2.getTopChildren(10, "brand");
System.out.println("\n钻取后品牌统计:");
System.out.println(afterDrill);
reader.close();
dir.close();
}
private static void addDoc(IndexWriter w,
FacetsConfig config,
String id,
int price,
String brand) throws Exception {
Document doc = new Document();
doc.add(new StringField("id", id, Field.Store.YES)); // 普通字段
doc.add(new NumericDocValuesField("price", price)); // 数值 DocValues
doc.add(new SortedSetDocValuesFacetField("brand", brand)); // 分面 DocValues
w.addDocument(config.build(doc)); // 必须用 FacetsConfig.build
}
}
```
---
✅ 运行结果示例
```
按价格排序:
id=1, 价格=1999, 品牌=小米
id=3, 价格=2999, 品牌=小米
id=2, 价格=3999, 品牌=苹果
品牌统计:
小米 : 2
苹果 : 1
钻取后品牌统计:
dim=brand path=[小米] value=2 childCount=0
```
---
✅ 小结
- NumericDocValues → 排序、范围聚合
- SortedSetDocValuesFacetField + FacetsConfig → 分面统计、钻取
- 代码直接基于 Lucene 8.5.0,无需额外 codec 设置。
在 Lucene 中,`doc.get("price")` 返回 `null` 是因为 `price` 字段被定义为 `NumericDocValuesField`,而 `NumericDocValuesField` 不会存储字段值。
它只用于 DocValues 索引,供排序、聚合等操作使用,但不会把值存入 `Document`。
---
✅ 解释
- `NumericDocValuesField`
- 用途:用于快速排序、聚合等操作,数据存储在 `.dvd` 文件中。
- 特点:不存储字段值,只能通过 `IndexSearcher` 的 `DocValues` 接口访问。
- `StoredField`
- 用途:用于存储字段值,可以在 `Document` 中直接访问。
- 特点:数据存储在 `.fdt` 文件中。
---
✅ 解决方法
如果你想在 `Document` 中直接访问字段值,需要同时添加一个 `StoredField`:
```java
doc.add(new NumericDocValuesField("price", price)); // 用于 DocValues 索引
doc.add(new StoredField("price", price)); // 用于存储字段值
```
---
✅ 完整示例
```java
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.*;
import org.apache.lucene.facet.*;
import org.apache.lucene.facet.sortedset.*;
import org.apache.lucene.index.*;
import org.apache.lucene.search.*;
import org.apache.lucene.store.ByteBuffersDirectory;
public class DocValuesDemo {
public static void main(String[] args) throws Exception {
Directory dir = new ByteBuffersDirectory();
IndexWriterConfig cfg = new IndexWriterConfig(new StandardAnalyzer());
IndexWriter writer = new IndexWriter(dir, cfg);
FacetsConfig config = new FacetsConfig();
// 添加文档
addDoc(writer, config, "1", 1999, "小米");
addDoc(writer, config, "2", 3999, "苹果");
addDoc(writer, config, "3", 2999, "小米");
writer.commit();
writer.close();
// 打开索引
DirectoryReader reader = DirectoryReader.open(dir);
IndexSearcher searcher = new IndexSearcher(reader);
// 按价格排序
Sort sort = new Sort(new SortField("price", SortField.Type.LONG));
TopDocs topDocs = searcher.search(new MatchAllDocsQuery(), 10, sort);
System.out.println("按价格排序:");
for (ScoreDoc sd : topDocs.scoreDocs) {
Document doc = searcher.doc(sd.doc);
System.out.println("id=" + doc.get("id") +
", 价格=" + doc.get("price") + // 这里可以访问 price
", 品牌=" + doc.get("brand"));
}
reader.close();
dir.close();
}
private static void addDoc(IndexWriter w,
FacetsConfig config,
String id,
int price,
String brand) throws Exception {
Document doc = new Document();
doc.add(new StringField("id", id, Field.Store.YES));
doc.add(new NumericDocValuesField("price", price)); // 用于 DocValues 索引
doc.add(new StoredField("price", price)); // 用于存储字段值
doc.add(new SortedSetDocValuesFacetField("brand", brand));
w.addDocument(config.build(doc));
}
}
```
---
✅ 输出结果
```
按价格排序:
id=1, 价格=1999, 品牌=小米
id=3, 价格=2999, 品牌=小米
id=2, 价格=3999, 品牌=苹果
```
---
✅ 总结
- `NumericDocValuesField`:用于 DocValues 索引,不存储字段值。
- `StoredField`:用于存储字段值,可以在 `Document` 中直接访问。
- 如果需要同时支持 DocValues 索引和字段值访问,需要同时添加这两个字段。
也就是说docvalues是一种索引,不是存内容的只有storefield设为yes才是存原文的
对,你总结得很准确!
✅ DocValues 的本质
DocValues 是一种索引结构,用于高效地存储和访问列式数据(如数值、字符串等),主要用于以下场景:
1. 排序:快速按字段值排序(如价格、日期等)。
2. 聚合:快速计算字段值的统计信息(如最大值、最小值、平均值等)。
3. 分面统计:快速统计字段值的分布(如品牌、类别等)。
DocValues 不存储原始文档内容,而是将字段值编码到索引文件中(如 `.dvd` 和 `.dvm` 文件),供快速读取和计算。
---
✅ `StoredField` 的作用
`StoredField` 用于存储原始字段值,以便在检索时可以直接访问这些值。它的特点如下:
- 存储原始值:将字段值存储在 `.fdt` 文件中。
- 可直接访问:通过 `Document` 对象访问存储的字段值。
- 不参与索引:不会影响搜索、排序或聚合操作。
---
✅ 对比
特性 DocValues StoredField
用途 索引结构,用于排序、聚合、分面统计 存储结构,用于存储原始字段值
存储位置 `.dvd` 和 `.dvm` 文件 `.fdt` 文件
是否可直接访问 不可直接访问,需通过 `DocValues` 接口 可直接通过 `Document` 访问
是否参与索引 参与索引,用于快速读取和计算 不参与索引,仅存储原始值
---
✅ 示例代码
```java
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.*;
import org.apache.lucene.index.*;
import org.apache.lucene.search.*;
import org.apache.lucene.store.ByteBuffersDirectory;
public class DocValuesDemo {
public static void main(String[] args) throws Exception {
Directory dir = new ByteBuffersDirectory();
IndexWriterConfig cfg = new IndexWriterConfig(new StandardAnalyzer());
IndexWriter writer = new IndexWriter(dir, cfg);
// 添加文档
Document doc1 = new Document();
doc1.add(new StringField("id", "1", Field.Store.YES));
doc1.add(new NumericDocValuesField("price", 1999)); // DocValues 索引
doc1.add(new StoredField("price", 1999)); // 存储原始值
writer.addDocument(doc1);
Document doc2 = new Document();
doc2.add(new StringField("id", "2", Field.Store.YES));
doc2.add(new NumericDocValuesField("price", 3999)); // DocValues 索引
doc2.add(new StoredField("price", 3999)); // 存储原始值
writer.addDocument(doc2);
writer.commit();
writer.close();
// 打开索引
DirectoryReader reader = DirectoryReader.open(dir);
IndexSearcher searcher = new IndexSearcher(reader);
// 按价格排序
Sort sort = new Sort(new SortField("price", SortField.Type.LONG));
TopDocs topDocs = searcher.search(new MatchAllDocsQuery(), 10, sort);
System.out.println("按价格排序:");
for (ScoreDoc sd : topDocs.scoreDocs) {
Document doc = searcher.doc(sd.doc);
System.out.println("id=" + doc.get("id") +
", 价格=" + doc.get("price")); // 可以直接访问存储的字段值
}
reader.close();
dir.close();
}
}
```
---
✅ 输出结果
```
按价格排序:
id=1, 价格=1999
id=2, 价格=3999
```
---
✅ 总结
- DocValues:用于索引,支持高效排序、聚合和分面统计,但不存储原始值。
- StoredField:用于存储原始字段值,可在检索时直接访问,但不参与索引。
通过同时使用 `NumericDocValuesField` 和 `StoredField`,可以兼顾索引性能和原始值访问的需求。