作者:来自 Elastic David Pilato
本教程将Apache Lucene作为进程内搜索索引嵌入,用于搜索领域 Bean —— 这里使用的是 Rekordbox 风格音乐库中的Track记录。同样的模式也适用于任何 Java Bean。
Lucene 作为一个派生缓存位于你的对象旁边,而不是事实来源。将数据库作为系统的权威记录:将 Bean 映射为Document,执行搜索,将命中的 id 与原始列表关联起来,并在成功写入后重新构建或 upsert Lucene。这篇文章只介绍映射 —— 先介绍 analyzer,然后介绍字段。
将 Lucene 添加到 Maven
一个项目,两个 artifact,使用相同版本。实现时,请在 Maven Central 上查找最新的稳定 Lucene 版本;本系列使用10.5.1。
<!-- Index, search, documents, queries --> <dependency> <groupId>org.apache.lucene</groupId> <artifactId>lucene-core</artifactId> <version>10.5.1</version> </dependency> <!-- Tokenizers / filters --> <dependency> <groupId>org.apache.lucene</groupId> <artifactId>lucene-analysis-common</artifactId> <version>10.5.1</version> </dependency>Lucene 是纯 Java:它可以被打包到 fat-jar 中,无需任何本地库。
从你现有的 Bean 开始
补充现有 Bean 的 Java 代码翻译并补全后续示例内容说明 fat-jar 和本地库
public record Track( String id, String title, Artist artist, Genre genre, MusicalKey key, double bpm, int ratingStars, int year // … album, comment, paths, … ) {}为需要查找文档的内容建立索引;完整的 Bean 保存在其他地方,并在搜索后通过 id 进行关联。
稳定的 id—
Track.id,用于 upsert 和删除。全文搜索— 用户输入的字符串(title、artist)。
过滤 / 范围查询— 精确的 keyword 或数值(genre、rating、bpm、year)。
选择 analyzer
Around The World经过 StandardTokenizer → LowerCaseFilter → ASCIIFoldingFilter。
对于TextField,analyzer 会在索引时运行,并且应该与查询时生成的 token 保持一致:
补充查询时的 analyzer 示例说明各个 token filter 的作用完善 Java 代码示例
Analyzer analyzer = new Analyzer() { @Override protected TokenStreamComponents createComponents(String fieldName) { Tokenizer source = new StandardTokenizer(); TokenStream filter = new LowerCaseFilter(source); filter = new ASCIIFoldingFilter(filter); return new TokenStreamComponents(source, filter); } }; // Analyze a text TokenStream ts = analyzer.tokenStream("title", "Around The World");不进行 stemming(artist 名称保持完整),不使用停用词(Around The World仍然可以被搜索)。ASCII folding 会将café/François转换为cafe/francois:
Café del Mar — Around The World (François Kevorkian Mix)— tokenizer → lowercase → ASCII folding;重音符号会在最后一个阶段进行折叠。
最终的 token 会以排序后的形式进入索引(around、cafe、del……)——就像一本书最后的索引一样。字母顺序让人们无需阅读每一页就能快速找到某个词条;Lucene 采用了相同的思路,因此查找时可以直接跳转到所需的 term,而不是扫描整个词典。
在输入和输出时使用相同的analyzer。
将 Bean 映射为 LuceneDocument
补充完整 Bean 映射示例统一术语与格式风格澄清 analyzer 的使用时机
选择一条 track;Lucene 会存储一个可用于搜索的Document(TextField / StringField / 数值字段)。
| 模式 | 示例 | Lucene 类型 |
|---|---|---|
| 分析后的文本 | title、artist | TextField |
| 精确 keyword | id、genre.raw | StringField |
| 数值 | bpm、rating、year | DoubleField/IntField |
TextField会进行 token 化(用于搜索)。StringField不会进行 token 化(用于 id、过滤条件)。数值字段用于范围过滤和排序——暂时还不用于直方图。存储你需要用来呈现匹配结果的数据(Field.Store.YES);无论如何都要存储 id。
这就是一个可用于搜索的Document:
Document doc = new Document(); // stored join key back to the Track bean doc.add(new StringField("id", "172523747", Store.YES)); // title: TextField is analyzed (MUST). .raw keeps the original for display. .raw.normalized is the exact FILTER. doc.add(new TextField("title", "Around The World", Store.YES)); doc.add(new StringField("title.raw", "Around The World", Store.YES)); doc.add(new StringField("title.raw.normalized", "around the world", Store.YES)); // artist: TextField is analyzed (MUST). .raw keeps the original for display. .raw.normalized is the exact FILTER. doc.add(new TextField("artist", "Daft Punk", Store.YES)); doc.add(new StringField("artist.raw", "Daft Punk", Store.YES)); doc.add(new StringField("artist.raw.normalized", "daft punk", Store.YES)); // genre: analyzed text + keyword FILTER (.raw.normalized) doc.add(new TextField("genre", "Club", Store.YES)); doc.add(new StringField("genre.raw", "Club", Store.YES)); doc.add(new StringField("genre.raw.normalized", "club", Store.YES)); // numeric range / sort. numericValue() is IEEE 754 bits; read storedValue().getDoubleValue() doc.add(new DoubleField("bpm", 121.29, Store.YES)); // Camelot key — exact FILTER / MUST_NOT (lowercased) doc.add(new StringField("key.code", "9a", Store.YES)); // rating: numeric filter / sort doc.add(new IntField("rating", 5, Store.YES)); // year: numeric filter / sort doc.add(new IntField("year", 1997, Store.YES)); // album: analyzed free text only — no keyword twin doc.add(new TextField("album", "", Store.YES)); // label: analyzed free text only — no keyword twin doc.add(new TextField("label", "", Store.YES)); // comment: analyzed free text only — no keyword twin doc.add(new TextField("comment", "09A - Energy 7", Store.YES))完整演示代码位于 GitHub:lucene-search-tracks。
原文:Search your beans with Lucene — Mapping | David Pilato